diff --git a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs index 75740c6376..83022ca58a 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs +++ b/apps/presentation/dashboard/src/features/personal-workspace/personal-workspace-contract.test.mjs @@ -5,7 +5,7 @@ import { localizedCapabilityFieldCopy } from "./capability-localization.ts"; import { taskSessionStatus } from "./goal-activity.ts"; import "./monitor-readback.test.mjs"; -const source = (name) => readFileSync(new URL(name, import.meta.url), "utf8"); +const source = (name) => readFileSync(new URL(name, import.meta.url), "utf8").replace(/\r\n/g, "\n"); const projectionAgent = { agentId: "status-only", adapterKind: "status_projection", available: true, label: "Status" }; const missingAgent = { agentId: "codex", available: false, label: "Codex" }; const availableAgent = { agentId: "claude", available: true, label: "Claude" }; @@ -118,7 +118,8 @@ assert.doesNotMatch(page.match(/function operationProposalFields[\s\S]*?\n\}/)?. assert.match(page, /t\("proposal\.primary\.operationGroup"\)/, "Operation confirmation routes users to the bound group"); assert.match(chatData, /result_delivery:/, "Dashboard retains operation result-delivery readback"); assert.match(chatData, /return_delivery\??:/, "Chat messages retain manager return-delivery readback"); -assert.match(dashboard, /reconcileConversationReturns\(/, "Manager return polling uses the shared conversation-return read model"); +assert.match(dashboard, /readConversationReturns\(/, "Manager return polling uses the shared conversation-return observation"); +assert.match(dashboard, /reconcileConversationHistory\(/, "Manager return polling reconciles the shared conversation history"); assert.match(conversationReturns, /source\.return_delivery \?\? row\.returnDelivery/, "Manager return polling refreshes delivery state after the message arrives"); assert.match(timeline + page, /ReturnDeliveryStatus/, "Both manager conversation surfaces render return delivery state"); for (const state of ["delivered", "verification_required", "explicit_unverified"]) { diff --git a/apps/presentation/dashboard/src/features/personal-workspace/report-reviews.css b/apps/presentation/dashboard/src/features/personal-workspace/report-reviews.css index 134dc56504..a37606e34b 100644 --- a/apps/presentation/dashboard/src/features/personal-workspace/report-reviews.css +++ b/apps/presentation/dashboard/src/features/personal-workspace/report-reviews.css @@ -10,13 +10,13 @@ .goal-report-reviews :is([data-result-catalog], [data-result-reader]):focus { outline: 2px solid var(--pw-blue-ink); outline-offset: 4px; } .report-review-tools, .report-review-tools > button, .goal-report-reviews .result-catalog-return { display: inline-flex; align-items: center; gap: 8px; } .report-review-reader { min-width: 0; margin-top: 8px; } -.report-review-masthead { padding: 8px 0; border-bottom: 1px solid var(--pw-border); } +.report-review-masthead { padding: 8px 0; border-bottom: 1px solid var(--pw-line); } .report-review-masthead h2 { margin: 8px 0; font-size: var(--text-title); line-height: var(--leading-title); overflow-wrap: anywhere; } .report-review-masthead time, .report-review-masthead span { font-size: var(--text-caption); line-height: var(--leading-caption); color: var(--pw-muted); } .report-review-actions { display: flex; flex-wrap: wrap; align-items: start; gap: 8px; padding: 8px 0 16px; } .report-review-actions .personal-decision-bar { flex: 1; } .report-review-actions .personal-compact-menu { flex: 0 0 auto; } -.report-review-reader iframe { display: block; width: 100%; height: min(1000px, 80vh); min-height: 560px; border: 1px solid var(--pw-border); border-radius: 6px; background: var(--pw-surface); } +.report-review-reader iframe { display: block; width: 100%; height: min(1000px, 80vh); min-height: 560px; border: 1px solid var(--pw-line); border-radius: 6px; background: var(--pw-card); } .report-review-prose { font-size: var(--text-lead); line-height: var(--leading-prose); } .report-review-reader > details { padding: 16px 0; overflow-wrap: anywhere; } @media (max-width: 640px) { .report-review-masthead > div { flex-wrap: wrap; } .report-review-reader iframe { min-height: 640px; } .report-review-actions .personal-decision-bar { flex-basis: 100%; } } diff --git a/docs/assets/qualification/report-reader-after-desktop.png b/docs/assets/qualification/report-reader-after-desktop.png new file mode 100644 index 0000000000..f1961d1170 Binary files /dev/null and b/docs/assets/qualification/report-reader-after-desktop.png differ diff --git a/docs/assets/qualification/report-reader-after-mobile.png b/docs/assets/qualification/report-reader-after-mobile.png new file mode 100644 index 0000000000..6d96799445 Binary files /dev/null and b/docs/assets/qualification/report-reader-after-mobile.png differ diff --git a/docs/assets/qualification/report-reader-before-desktop.png b/docs/assets/qualification/report-reader-before-desktop.png new file mode 100644 index 0000000000..5d5a449d31 Binary files /dev/null and b/docs/assets/qualification/report-reader-before-desktop.png differ diff --git a/docs/assets/qualification/report-reader-before-mobile.png b/docs/assets/qualification/report-reader-before-mobile.png new file mode 100644 index 0000000000..9b3a8cbf01 Binary files /dev/null and b/docs/assets/qualification/report-reader-before-mobile.png differ diff --git a/examples/personal-workspace-browser/conversation-activity.mjs b/examples/personal-workspace-browser/conversation-activity.mjs index 2eba84897f..7392f98e29 100644 --- a/examples/personal-workspace-browser/conversation-activity.mjs +++ b/examples/personal-workspace-browser/conversation-activity.mjs @@ -216,7 +216,8 @@ export const conversationActivityScenario = { const answer = "检查完成,下一步已列出。"; const runtime = page.__loopxRuntime; runtime.sessions.set(next.sessionId, { ...runtime.sessions.get(next.sessionId), active_turn_id: null, status: "ready" }); - runtime.messages.get(next.sessionId).push({ message_id: `${next.turnId}-assistant`, turn_id: next.turnId, role: "assistant", text: answer }); + runtime.messages.get(next.sessionId).push({ message_id: `${next.turnId}-assistant`, turn_id: next.turnId, role: "assistant", text: answer, + created_at: await page.evaluate(() => new Date().toISOString()) }); response.end(event(5, "turn.completed", { response: { schema_version: "loopx_chat_agent_response_v0", message: answer, proposals: [], gate: null } })); await route.fulfill({ json: { ok: true, session_id: next.sessionId, turn_id: next.turnId, status: "completed" } }); }); @@ -246,7 +247,8 @@ export const conversationActivityScenario = { const runtime = page.__loopxRuntime; runtime.turnMessages.set(turnId, body.message); runtime.sessions.set(sessionId, {...runtime.sessions.get(sessionId), active_turn_id: turnId, status: "busy"}); - runtime.messages.get(sessionId).push({message_id: `${turnId}-user`, turn_id: turnId, role: "user", text: body.message}); + runtime.messages.get(sessionId).push({message_id: `${turnId}-user`, turn_id: turnId, role: "user", text: body.message, + created_at: await page.evaluate(() => new Date().toISOString())}); api.turnRequests.push({message: body.message, sessionId, turnId}); let holdFirstRead = true; await page.route(`**/api/chat/sessions/${sessionId}`, snapshot => { diff --git a/examples/personal-workspace-browser/turn-steps.mjs b/examples/personal-workspace-browser/turn-steps.mjs index 422e2285f0..f81c76fdac 100644 --- a/examples/personal-workspace-browser/turn-steps.mjs +++ b/examples/personal-workspace-browser/turn-steps.mjs @@ -87,7 +87,8 @@ export const turnStepsScenario = { const answer = "两位负责人都保留,中英文分别择时发布。"; const runtime = page.__loopxRuntime; runtime.sessions.set(turn.sessionId, { ...runtime.sessions.get(turn.sessionId), active_turn_id: null, status: "ready" }); - runtime.messages.get(turn.sessionId).push({ message_id: `${turn.turnId}-assistant`, turn_id: turn.turnId, role: "assistant", text: answer }); + runtime.messages.get(turn.sessionId).push({ message_id: `${turn.turnId}-assistant`, turn_id: turn.turnId, role: "assistant", text: answer, + created_at: await page.evaluate(() => new Date().toISOString()) }); live.end(event("turn.completed", { response: { schema_version: "loopx_chat_agent_response_v0", message: answer, proposals: [], gate: null } })); await page.getByRole("button", { name: "中断本轮", exact: true }).waitFor({ state: "detached" }); const done = page.locator(".personal-message .personal-turn-steps").last(); diff --git a/loopx/canary/module_metric_baseline.json b/loopx/canary/module_metric_baseline.json index a50ecdb3e2..a910b9ff92 100644 --- a/loopx/canary/module_metric_baseline.json +++ b/loopx/canary/module_metric_baseline.json @@ -62,11 +62,11 @@ "dict_any_count": 0 }, "loopx/control_plane/goals/goal_frontier/terminal.py": { - "any_count": 22, + "any_count": 15, "dict_any_count": 0 }, "loopx/control_plane/work_items/interaction_contract.py": { - "any_count": 107, + "any_count": 85, "dict_any_count": 0 }, "loopx/extensions/lark/goal_topic_runtime.py": { @@ -93,7 +93,7 @@ "dict_any_count": 0 }, "loopx/pr_review.py": { - "any_count": 50, + "any_count": 49, "dict_any_count": 0 }, "loopx/presentation/renderers/status_markdown.py": { @@ -106,7 +106,7 @@ "lines": 1999 }, "loopx/slash_command_install.py": { - "any_count": 12, + "any_count": 11, "dict_any_count": 0 }, "loopx/state_refresh.py": { diff --git a/loopx/control_plane/goals/goal_frontier/terminal.py b/loopx/control_plane/goals/goal_frontier/terminal.py index 9df22f04aa..0d7c3e0121 100644 --- a/loopx/control_plane/goals/goal_frontier/terminal.py +++ b/loopx/control_plane/goals/goal_frontier/terminal.py @@ -33,6 +33,7 @@ def _terminal_todo_source_state( done = summary.get("done_count") deferred = summary.get("deferred_count") watch_only = summary.get("watch_only_monitor_count", 0) + monitor_open_items = summary.get("monitor_open_items") if not ( type(total) is int and total >= 0 @@ -42,8 +43,8 @@ def _terminal_todo_source_state( and done + watch_only == total and _strict_zero(summary.get("convergence_open_count", summary.get("open_count"))) and _strict_zero(deferred) - and isinstance(summary.get("monitor_open_items"), list) - and len(cast(list[Any], summary.get("monitor_open_items"))) == watch_only + and isinstance(monitor_open_items, list) + and len(monitor_open_items) == watch_only ): return "invalid", False intent = summary.get("closure_intent") @@ -67,14 +68,14 @@ def _projection_has_empty_terminal_frontier(projection: dict[str, Any] | None) - frontier = projection.get("remaining_advancement_frontier") monitors = projection.get("monitor_only_lanes") successors = projection.get("deferred_successors") - if not all(isinstance(value, dict) for value in (normalized, frontier, monitors, successors)): + if not ( + isinstance(normalized, dict) + and isinstance(frontier, dict) + and isinstance(monitors, dict) + and isinstance(successors, dict) + ): return False - normalized = cast(dict[str, Any], normalized) - frontier = cast(dict[str, Any], frontier) - monitors = cast(dict[str, Any], monitors) - successors = cast(dict[str, Any], successors) - required_keys = ( (normalized, {"user_open_count", "agent_open_count", "agent_advancement_open_count", "agent_monitor_open_count", "agent_monitor_due_count"}), (frontier, {"current_agent_claimed_advancement_count", "unclaimed_advancement_count", "other_agent_claimed_advancement_count"}), @@ -114,7 +115,7 @@ def _terminal_todo_source_contract( ) -> tuple[dict[str, Any], bool]: user_status, user_intent = _terminal_todo_source_state(user_todo_summary, role="user") agent_status, agent_intent = _terminal_todo_source_state(agent_todo_summary, role="agent") - source_completeness: dict[str, Any] = { + source_completeness: dict[str, object] = { "schema_version": GOAL_TERMINAL_SOURCE_COMPLETENESS_SCHEMA_VERSION, "user_todos": user_status, "agent_todos": agent_status, @@ -180,9 +181,8 @@ def _terminal_no_followup_resolves_vision_checkpoint( def goal_frontier_is_terminal_no_followup(*, projection: dict[str, Any] | None) -> bool: - if not _projection_has_empty_terminal_frontier(projection): + if projection is None or not _projection_has_empty_terminal_frontier(projection): return False - projection = cast(dict[str, Any], projection) terminal_state = projection.get("terminal_state") completeness = projection.get("source_completeness") return bool( diff --git a/loopx/control_plane/work_items/interaction_contract.py b/loopx/control_plane/work_items/interaction_contract.py index 5c7869c37a..cd196b5693 100644 --- a/loopx/control_plane/work_items/interaction_contract.py +++ b/loopx/control_plane/work_items/interaction_contract.py @@ -207,11 +207,9 @@ def _user_gate_scope_projection_repair_active(payload: Mapping[str, object]) -> def _capability_resolution_user_actions(payload: dict[str, Any]) -> list[str]: - capability_gate = ( - cast(dict[str, Any], payload.get("capability_gate")) - if isinstance(payload.get("capability_gate"), dict) - else {} - ) + capability_gate = payload.get("capability_gate") + if not isinstance(capability_gate, dict): + capability_gate = {} if not capability_gate.get("owner_missing"): return [] owner_action = protocol_action_text(capability_gate.get("owner_action")) @@ -284,11 +282,9 @@ def projected_user_channel_actions( return capability_actions[:limit] if not user_channel_action_required(payload): return [] - capability_gate = ( - cast(dict[str, Any], payload.get("capability_gate")) - if isinstance(payload.get("capability_gate"), dict) - else {} - ) + capability_gate = payload.get("capability_gate") + if not isinstance(capability_gate, dict): + capability_gate = {} if capability_gate.get("owner_missing"): owner_action = protocol_action_text(capability_gate.get("owner_action")) if owner_action: @@ -433,12 +429,12 @@ def _turn_scoped_cli_settlement_context( ) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: if selection.action_portfolio_requires_explicit_selection(payload): return None, None - agent_identity = cast(dict[str, Any], payload.get("agent_identity")) if isinstance(payload.get("agent_identity"), dict) else {} - selected_todo = ( - cast(dict[str, Any], payload.get("selected_todo")) - if isinstance(payload.get("selected_todo"), dict) - else {} - ) + agent_identity = payload.get("agent_identity") + if not isinstance(agent_identity, dict): + agent_identity = {} + selected_todo = payload.get("selected_todo") + if not isinstance(selected_todo, dict): + selected_todo = {} goal_id = str(payload.get("goal_id") or "").strip() agent_id = str(agent_identity.get("agent_id") or "").strip() selected_todo_id = normalize_todo_id(selected_todo.get("todo_id")) @@ -551,7 +547,9 @@ def _selection_recovery_command( turn_instance_id: str | None, runtime_root: str | None, registry_path: str | None = None, ) -> str: - identity = cast(dict[str, Any], payload.get("agent_identity")) if isinstance(payload.get("agent_identity"), dict) else {} + identity = payload.get("agent_identity") + if not isinstance(identity, dict): + identity = {} try: scheduler_args = render_scheduler_execution_args( scheduler_execution_context=scheduler_execution_context, @@ -653,11 +651,9 @@ def _automation_prompt_upgrade_cli_actions( payload: Mapping[str, object], *, command_prefix: str, goal_id: str, ) -> list[str]: """Render the existing prompt-upgrade commands without selecting work.""" - automation_prompt_upgrade = ( - cast(dict[str, Any], payload.get("automation_prompt_upgrade")) - if isinstance(payload.get("automation_prompt_upgrade"), dict) - else {} - ) + automation_prompt_upgrade = payload.get("automation_prompt_upgrade") + if not isinstance(automation_prompt_upgrade, dict): + automation_prompt_upgrade = {} actions = [ str(item.get("command") or "").strip() for item in automation_prompt_upgrade.get("agent_example_commands") or [] @@ -690,7 +686,9 @@ def _admitted_interaction_next_cli_actions( ) -> list[str]: goal_id = str(payload.get("goal_id") or "") command_prefix = selection.render_cli_command_prefix(runtime_root=runtime_root, registry_path=registry_path) - agent_identity = cast(dict[str, Any], payload.get("agent_identity")) if isinstance(payload.get("agent_identity"), dict) else {} + agent_identity = ( + identity if isinstance(identity := payload.get("agent_identity"), dict) else {} + ) scoped_cli_args = _scoped_cli_args( agent_identity, available_capabilities=available_capabilities, @@ -759,11 +757,9 @@ def _admitted_interaction_next_cli_actions( in APP_HEARTBEAT_SETTLEMENT_RUNTIME_PROFILES ) if mode == "governed_capability_intent": - projection = ( - cast(Mapping[str, Any], payload.get("pending_capability_intent")) - if isinstance(payload.get("pending_capability_intent"), Mapping) - else {} - ) + projection = payload.get("pending_capability_intent") + if not isinstance(projection, Mapping): + projection = {} command = protocol_action_text(projection.get("command"), limit=1200) return [command] if command else [] quota_spend_action = build_quota_spend_action( @@ -833,21 +829,17 @@ def _admitted_interaction_next_cli_actions( ): return [typed_quota_guard] if mode == AgentScopeFrontierAction.SUCCESSOR_REPLAN_REQUIRED.value: - agent_scope_frontier = ( - cast(dict[str, Any], payload.get("agent_scope_frontier")) - if isinstance(payload.get("agent_scope_frontier"), dict) - else {} - ) + agent_scope_frontier = payload.get("agent_scope_frontier") + if not isinstance(agent_scope_frontier, dict): + agent_scope_frontier = {} if cycle_actions := _dependency_cycle_cli_actions( agent_scope_frontier, command_prefix=command_prefix, goal_id=goal_id, lifecycle_actor_args=lifecycle_actor_args, typed_quota_guard=typed_quota_guard, ): return cycle_actions - monitor_candidates = ( - cast(list[Any], agent_scope_frontier.get("monitor_blocked_resume_candidates")) - if isinstance(agent_scope_frontier.get("monitor_blocked_resume_candidates"), list) - else [] - ) + monitor_candidates = agent_scope_frontier.get("monitor_blocked_resume_candidates") + if not isinstance(monitor_candidates, list): + monitor_candidates = [] if monitor_candidates: first_candidate = ( monitor_candidates[0] @@ -864,22 +856,18 @@ def _admitted_interaction_next_cli_actions( f"{command_prefix} refresh-state --goal-id {goal_id} --classification standing_monitor_gate_repair_recorded --delivery-batch-scale single_surface --delivery-outcome outcome_progress{settlement_args}{scoped_cli_args}", quota_spend_action, ] - route_candidates = ( - cast(list[Any], agent_scope_frontier.get("route_continuation_replan_candidates")) - if isinstance(agent_scope_frontier.get("route_continuation_replan_candidates"), list) - else [] - ) + route_candidates = agent_scope_frontier.get("route_continuation_replan_candidates") + if not isinstance(route_candidates, list): + route_candidates = [] if route_candidates: return [ f"{command_prefix} todo add --goal-id {goal_id} --role agent --text ''", f"{command_prefix} refresh-state --goal-id {goal_id} --classification route_continuation_replan_recorded --delivery-batch-scale single_surface --delivery-outcome outcome_progress{settlement_args}{scoped_cli_args}", quota_spend_action, ] - candidates = ( - cast(list[Any], agent_scope_frontier.get("deferred_resume_candidates")) - if isinstance(agent_scope_frontier.get("deferred_resume_candidates"), list) - else [] - ) + candidates = agent_scope_frontier.get("deferred_resume_candidates") + if not isinstance(candidates, list): + candidates = [] first_candidate = candidates[0] if candidates and isinstance(candidates[0], dict) else {} todo_id = str(first_candidate.get("todo_id") or "") reopen = ( @@ -1095,11 +1083,9 @@ def _interaction_spend_policy( def _blocked_priority_fallback_user_reason(payload: dict[str, Any]) -> str | None: - fallback = ( - cast(dict[str, Any], payload.get("blocked_priority_fallback")) - if isinstance(payload.get("blocked_priority_fallback"), dict) - else {} - ) + fallback = payload.get("blocked_priority_fallback") + if not isinstance(fallback, dict): + fallback = {} if not fallback: return None if ( @@ -1193,11 +1179,9 @@ def _build_interaction_agent_channel( "quiet_noop_allowed": quiet_noop_allowed, } channel.update(build_primary_action_projection(payload, mode=mode, primary_action=primary_action)) - work_lane = ( - cast(Mapping[str, Any], payload.get("work_lane_contract")) - if isinstance(payload.get("work_lane_contract"), Mapping) - else {} - ) + work_lane = payload.get("work_lane_contract") + if not isinstance(work_lane, Mapping): + work_lane = {} if isinstance(work_lane.get("auxiliary_monitor_poll"), Mapping): channel["auxiliary_monitor_poll"] = dict( work_lane["auxiliary_monitor_poll"] @@ -1354,16 +1338,12 @@ def _build_interaction_cli_channel( spend_after_validation=spend_after_selection, ), } - work_lane = ( - cast(Mapping[str, Any], payload.get("work_lane_contract")) - if isinstance(payload.get("work_lane_contract"), Mapping) - else {} - ) - auxiliary_monitor = ( - cast(Mapping[str, Any], work_lane.get("auxiliary_monitor_poll")) - if isinstance(work_lane.get("auxiliary_monitor_poll"), Mapping) - else None - ) + work_lane = payload.get("work_lane_contract") + if not isinstance(work_lane, Mapping): + work_lane = {} + auxiliary_monitor = work_lane.get("auxiliary_monitor_poll") + if not isinstance(auxiliary_monitor, Mapping): + auxiliary_monitor = None if auxiliary_monitor is not None: selected_monitor_id = normalize_todo_id( auxiliary_monitor.get("selected_todo_id") @@ -1378,11 +1358,9 @@ def _build_interaction_cli_channel( command_prefix = selection.render_cli_command_prefix( runtime_root=runtime_root, registry_path=registry_path, ) - agent_identity = ( - cast(dict[str, Any], payload.get("agent_identity")) - if isinstance(payload.get("agent_identity"), dict) - else {} - ) + agent_identity = payload.get("agent_identity") + if not isinstance(agent_identity, dict): + agent_identity = {} safe_turn_instance_id = str(turn_instance_id or "").strip() auxiliary_projection: dict[str, Any] = { **dict(auxiliary_monitor), @@ -1487,11 +1465,9 @@ def _build_interaction_cli_channel( capability_reentry, selection_required=selection.action_portfolio_requires_explicit_selection(payload), ) - selected_todo = ( - cast(Mapping[str, Any], payload.get("selected_todo")) - if isinstance(payload.get("selected_todo"), Mapping) - else {} - ) + selected_todo = payload.get("selected_todo") + if not isinstance(selected_todo, Mapping): + selected_todo = {} if spend_after_selection and selected_todo.get("todo_id"): channel["delivery_workspace_causality"] = { "schema_version": "delivery_workspace_causality_v0", @@ -1518,11 +1494,9 @@ def _attach_interaction_post_writeback_actions( preparation = payload.get("execution_preparation") if isinstance(preparation, Mapping) and preparation.get("state") != "ready": return - goal_boundary = ( - cast(Mapping[str, Any], payload.get("goal_boundary")) - if isinstance(payload.get("goal_boundary"), Mapping) - else {} - ) + goal_boundary = payload.get("goal_boundary") + if not isinstance(goal_boundary, Mapping): + goal_boundary = {} actions = goal_boundary.get("post_writeback_actions") if isinstance(actions, list) and actions: contract["cli_channel"]["post_writeback_actions"] = actions @@ -1532,19 +1506,15 @@ def _attach_interaction_vision_continuation_audit( contract: dict[str, Any], payload: dict[str, Any], ) -> None: - vision_continuation_audit = ( - cast(dict[str, Any], payload.get("vision_continuation_audit")) - if isinstance(payload.get("vision_continuation_audit"), dict) - else {} - ) + vision_continuation_audit = payload.get("vision_continuation_audit") + if not isinstance(vision_continuation_audit, dict): + vision_continuation_audit = {} if not vision_continuation_audit.get("required"): return contract["agent_channel"]["vision_continuation_audit"] = vision_continuation_audit - vision_gap_judge = ( - cast(dict[str, Any], vision_continuation_audit.get("vision_gap_judge")) - if isinstance(vision_continuation_audit.get("vision_gap_judge"), dict) - else {} - ) + vision_gap_judge = vision_continuation_audit.get("vision_gap_judge") + if not isinstance(vision_gap_judge, dict): + vision_gap_judge = {} contract["cli_channel"]["vision_continuation_audit"] = { "required": True, "required_before_closeout": vision_continuation_audit.get( @@ -1575,11 +1545,9 @@ def _attach_interaction_vision_wait_state( contract: dict[str, Any], payload: dict[str, Any], ) -> None: - vision_wait_state = ( - cast(dict[str, Any], payload.get("vision_wait_state")) - if isinstance(payload.get("vision_wait_state"), dict) - else {} - ) + vision_wait_state = payload.get("vision_wait_state") + if not isinstance(vision_wait_state, dict): + vision_wait_state = {} if vision_wait_state.get("state") != "waiting": return contract["agent_channel"]["vision_wait_state"] = vision_wait_state @@ -1636,16 +1604,12 @@ def _build_interaction_contract_projection( runtime_root: str | None = None, registry_path: str | None = None, ) -> tuple[InteractionContractPacket, SchedulerArbitration]: - execution_obligation = ( - cast(dict[str, Any], payload.get("execution_obligation")) - if isinstance(payload.get("execution_obligation"), dict) - else {} - ) - heartbeat_recommendation = ( - cast(dict[str, Any], payload.get("heartbeat_recommendation")) - if isinstance(payload.get("heartbeat_recommendation"), dict) - else {} - ) + execution_obligation = payload.get("execution_obligation") + if not isinstance(execution_obligation, dict): + execution_obligation = {} + heartbeat_recommendation = payload.get("heartbeat_recommendation") + if not isinstance(heartbeat_recommendation, dict): + heartbeat_recommendation = {} monitor_only = payload.get("agent_work_mode") == "monitor_only" user_required = False if monitor_only else user_channel_action_required(payload) decision = _project_interaction_execution( diff --git a/loopx/pr_review.py b/loopx/pr_review.py index 15b2213361..2238eacf4b 100644 --- a/loopx/pr_review.py +++ b/loopx/pr_review.py @@ -871,8 +871,9 @@ def evaluate(review: Mapping[str, Any]) -> dict[str, Any]: "submitted_at": review.get("submittedAt"), "conclusion": conclusion} for review, conclusion in zip(reviews, conclusions)]}) - return cast(dict[str, Any], conclusions[selection["index"]] | ({"independent_review": selection["independent_review"]} - if "independent_review" in selection else {})) + index = cast(int, selection["index"]) + return conclusions[index] | ({"independent_review": selection["independent_review"]} + if "independent_review" in selection else {}) def _review_sequence_entry(item: dict[str, Any], *, rank: int) -> dict[str, Any]: diff --git a/loopx/slash_command_install.py b/loopx/slash_command_install.py index 48ce38034f..7426fbd82a 100644 --- a/loopx/slash_command_install.py +++ b/loopx/slash_command_install.py @@ -933,11 +933,9 @@ def _merge_managed_hooks_json( marker_payload = json.loads(marker_path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): marker_payload = {} - recorded_hooks = ( - cast(dict[str, Any], marker_payload.get("hooks")) - if isinstance(marker_payload, dict) and isinstance(marker_payload.get("hooks"), dict) - else {} - ) + recorded_hooks = marker_payload.get("hooks") if isinstance(marker_payload, dict) else None + if not isinstance(recorded_hooks, dict): + recorded_hooks = {} recorded = recorded_hooks.get(MANAGED_MCP_KEY) # Native named hooks live at the top level. Earlier proposals wrote an # ignored `hooks.loopx` wrapper; migrate it only with exact ownership proof.