diff --git a/agentcore/adr/ADR-017-commander-opencode-context-memory.md b/agentcore/adr/ADR-017-commander-opencode-context-memory.md index 5ef8e801..e8f2f69e 100644 --- a/agentcore/adr/ADR-017-commander-opencode-context-memory.md +++ b/agentcore/adr/ADR-017-commander-opencode-context-memory.md @@ -186,7 +186,7 @@ Future OpenCode-to-Commander communication should use explicit records rather th - human escalation status - created time -Human escalation remains explicit. Branch 9A stores question and human-control policy metadata only; it does not implement the question flow. Branch 9G implements the durable OpenCode-to-Commander question/request side by recording bounded pending `OpenCodeCommanderQuestion` events from explicit operator input, 9E progress evidence, and 9F watchdog/forced-report evidence. It does not answer questions, call Commander providers, inject guidance, send prompts to OpenCode, control processes, run wake supervision, write `research.db`, or mutate missions. Branch 9H records bounded `CommanderGuidance` answers and marks linked questions answered by projection only. It does not generate answers with providers, deliver guidance to OpenCode, send prompts, control processes, run wake supervision, write `research.db`, or mutate missions. Branch 9I adds the explicit delivery gate. In the real runtime path it records bounded operator handoff metadata and projects guidance to `pending_delivery`; it does not claim delivered status, send a prompt to OpenCode, call providers/MCPs, control processes, run wake supervision, write `research.db`, or mutate missions. Branch 9J records durable human-control metadata for launched sessions. Human pause/resume/stop/correction/override/report/note records are authoritative human input for future Commander/wake review, but they do not pause, kill, stop, or resume OS processes, send prompts to OpenCode, call providers/MCPs, write `research.db`, mutate missions, or mark mission/session success or failure. Branch 9L records explicit wake-supervisor execution ticks and capped batches from 9K previews. These records persist what the supervisor saw and recommended with `action_execution_status=not_executed`; they do not execute recommended commands, call providers/MCPs, send OpenCode prompts, control processes, write `research.db`, mutate missions, create checkpoints, or ingest results. Branch 9M adds a separate wake recommended-action execution gate for safe metadata-only actions. It consumes one 9L execution record and may call only typed allowlisted metadata APIs for watchdog records, forced-report requests, Commander question creation, or explicit operator-handoff delivery; it blocks arbitrary commands, provider calls, OpenCode prompts, process control, result review, `research.db` writes, mission mutation, checkpoints, and result ingestion. +Human escalation remains explicit. Branch 9A stores question and human-control policy metadata only; it does not implement the question flow. Branch 9G implements the durable OpenCode-to-Commander question/request side by recording bounded pending `OpenCodeCommanderQuestion` events from explicit operator input, 9E progress evidence, and 9F watchdog/forced-report evidence. It does not answer questions, call Commander providers, inject guidance, send prompts to OpenCode, control processes, run wake supervision, write `research.db`, or mutate missions. Branch 9H records bounded `CommanderGuidance` answers and marks linked questions answered by projection only. It does not generate answers with providers, deliver guidance to OpenCode, send prompts, control processes, run wake supervision, write `research.db`, or mutate missions. Branch 9I adds the explicit delivery gate. In the real runtime path it records bounded operator handoff metadata and projects guidance to `pending_delivery`; it does not claim delivered status, send a prompt to OpenCode, call providers/MCPs, control processes, run wake supervision, write `research.db`, or mutate missions. Branch 9J records durable human-control metadata for launched sessions. Human pause/resume/stop/correction/override/report/note records are authoritative human input for future Commander/wake review, but they do not pause, kill, stop, or resume OS processes, send prompts to OpenCode, call providers/MCPs, write `research.db`, mutate missions, or mark mission/session success or failure. Branch 9L records explicit wake-supervisor execution ticks and capped batches from 9K previews. These records persist what the supervisor saw and recommended with `action_execution_status=not_executed`; they do not execute recommended commands, call providers/MCPs, send OpenCode prompts, control processes, write `research.db`, mutate missions, create checkpoints, or ingest results. Branch 9M adds a separate wake recommended-action execution gate for safe metadata-only actions. It consumes one 9L execution record and may call only typed allowlisted metadata APIs for watchdog records, forced-report requests, Commander question creation, or explicit operator-handoff delivery; it blocks arbitrary commands, provider calls, OpenCode prompts, process control, result review, `research.db` writes, mission mutation, checkpoints, and result ingestion. Branch 9N records bounded OpenCode result reports for completion, partial, failure, inconclusive, blocked, and status outcomes. These reports are executor evidence for future Commander review only; they do not complete missions or sessions, accept/reject results, write or ingest `research.db`, create checkpoints, call providers/MCPs, send OpenCode prompts, control processes, or mutate missions/proposals/reviews/apply records. ## Session Continuity Modes @@ -227,4 +227,4 @@ Branch 9A does not: - create checkpoints - submit results -Branch 9B0 builds on this contract with an OpenCode native context compatibility audit. Branch 9B1 adds the read-only model capability and context budget registry used to answer what a model/context can afford. Branch 9B2 adds the read-only context packet compiler skeleton for Commander, OpenCode executor, wake supervisor, research retrieval, and open-question contexts. Branch 9B3 materializes bounded per-session OpenCode instruction/config artifacts under `.nxl/opencode/sessions//` with `launch_ready=false`; it does not launch OpenCode, mutate global repo memory, call providers, call MCPs, or query `research.db`. Branch 9B4 adds read-only research memory retrieval and novelty-check previews that return bounded source refs, duplicate-risk, novelty-score, and repetition-justification metadata. It does not decide research direction, call providers/MCPs, browse online sources, write `research.db`, ingest research, create missions/proposals, launch OpenCode, or run Commander cycle/synthesis. Branch 9C adds a read-only launch-readiness preview that verifies planned-session, instruction-pack, manifest/config, packet/budget, advisory novelty, and static native launch assumptions. It does not grant launch authority, start OpenCode, call providers/MCPs, write files, write `research.db`, mutate missions, or run wake/scheduler behavior. Branch 9D adds the first explicit high-impact launch gate for one ready planned session and records bounded launch metadata. It still does not supervise progress, enforce timeout, inject Commander guidance, implement OpenCode questions, call providers/MCPs from NexusLoop, write `research.db`, mutate missions/proposals/reviews/apply records, or run wake/scheduler behavior. Branch 9E adds typed heartbeat, progress, blocker, and question report metadata anchored to 9D launch records. It does not poll OpenCode, enforce timeouts, answer questions, inject Commander guidance, write `research.db`, mutate missions/proposals/reviews/apply records, or run wake/scheduler behavior. Branch 9F adds timeout watchdog assessments and forced-report request metadata anchored to 9A timeout policy, 9D launches, and 9E progress. It does not pause/kill/stop/resume OpenCode, send prompts, answer questions, inject Commander guidance, run wake supervision, call providers/MCPs, write `research.db`, mutate missions/proposals/reviews/apply records, or mark mission/session success or failure. Branch 9G adds durable pending `OpenCodeCommanderQuestion` records from explicit operator input, progress question/blocker evidence, watchdog assessments, and forced-report requests. It does not answer questions, inject guidance, call providers/MCPs, send OpenCode prompts, control processes, run wake supervision, write `research.db`, mutate missions/proposals/reviews/apply records, or mark mission/session success or failure. Branch 9H adds durable `CommanderGuidance` answer records and answered-question projection events. Delivery remains not delivered; it does not call providers/MCPs, send OpenCode prompts, control processes, run wake supervision, write `research.db`, mutate missions/proposals/reviews/apply records, or mark mission/session success or failure. Branch 9I adds the explicit delivery gate and records operator-handoff delivery requests as `pending_delivery` only. Because no safe running-session adapter send path exists yet, it does not mark real runtime guidance as delivered or send prompts to OpenCode. Branch 9J adds durable human-control metadata for launched sessions and projects pause/resume/stop/correction/override/report/note intent while always recording that no process control, OpenCode prompt send, or mission mutation occurred. Branch 9K adds a read-only wake supervisor preview that aggregates launched-session evidence across 9A/9D/9E/9F/9G/9H/9I/9J, produces bounded context sections and recommended commands, and appends no wake events, sends no prompts, controls no processes, calls no providers/MCPs, writes no `research.db`, and mutates no missions. Branch 9L adds scheduled wake supervisor execution metadata for explicit single-session and capped batch ticks. It appends bounded `opencode_wake_supervisor_execution_recorded` and batch records with `action_execution_status=not_executed`; it does not execute recommended commands, call providers/MCPs, send OpenCode prompts, control processes, write `research.db`, mutate missions, create checkpoints, or ingest results. Branch 9M adds gated execution for safe metadata-only wake recommendations. It can append action-execution records and call typed existing metadata services for watchdog, forced-report, question, or operator-handoff delivery actions only when the requested action matches the 9L recorded recommendation; it blocks mismatched overrides, arbitrary command execution, answer generation, real OpenCode delivery, result review, provider/MCP calls, process control, `research.db` writes, mission mutation, checkpoints, and result ingestion. Later branches should add result reporting and any broader gated action execution explicitly. +Branch 9B0 builds on this contract with an OpenCode native context compatibility audit. Branch 9B1 adds the read-only model capability and context budget registry used to answer what a model/context can afford. Branch 9B2 adds the read-only context packet compiler skeleton for Commander, OpenCode executor, wake supervisor, research retrieval, and open-question contexts. Branch 9B3 materializes bounded per-session OpenCode instruction/config artifacts under `.nxl/opencode/sessions//` with `launch_ready=false`; it does not launch OpenCode, mutate global repo memory, call providers, call MCPs, or query `research.db`. Branch 9B4 adds read-only research memory retrieval and novelty-check previews that return bounded source refs, duplicate-risk, novelty-score, and repetition-justification metadata. It does not decide research direction, call providers/MCPs, browse online sources, write `research.db`, ingest research, create missions/proposals, launch OpenCode, or run Commander cycle/synthesis. Branch 9C adds a read-only launch-readiness preview that verifies planned-session, instruction-pack, manifest/config, packet/budget, advisory novelty, and static native launch assumptions. It does not grant launch authority, start OpenCode, call providers/MCPs, write files, write `research.db`, mutate missions, or run wake/scheduler behavior. Branch 9D adds the first explicit high-impact launch gate for one ready planned session and records bounded launch metadata. It still does not supervise progress, enforce timeout, inject Commander guidance, implement OpenCode questions, call providers/MCPs from NexusLoop, write `research.db`, mutate missions/proposals/reviews/apply records, or run wake/scheduler behavior. Branch 9E adds typed heartbeat, progress, blocker, and question report metadata anchored to 9D launch records. It does not poll OpenCode, enforce timeouts, answer questions, inject Commander guidance, write `research.db`, mutate missions/proposals/reviews/apply records, or run wake/scheduler behavior. Branch 9F adds timeout watchdog assessments and forced-report request metadata anchored to 9A timeout policy, 9D launches, and 9E progress. It does not pause/kill/stop/resume OpenCode, send prompts, answer questions, inject Commander guidance, run wake supervision, call providers/MCPs, write `research.db`, mutate missions/proposals/reviews/apply records, or mark mission/session success or failure. Branch 9G adds durable pending `OpenCodeCommanderQuestion` records from explicit operator input, progress question/blocker evidence, watchdog assessments, and forced-report requests. It does not answer questions, inject guidance, call providers/MCPs, send OpenCode prompts, control processes, run wake supervision, write `research.db`, mutate missions/proposals/reviews/apply records, or mark mission/session success or failure. Branch 9H adds durable `CommanderGuidance` answer records and answered-question projection events. Delivery remains not delivered; it does not call providers/MCPs, send OpenCode prompts, control processes, run wake supervision, write `research.db`, mutate missions/proposals/reviews/apply records, or mark mission/session success or failure. Branch 9I adds the explicit delivery gate and records operator-handoff delivery requests as `pending_delivery` only. Because no safe running-session adapter send path exists yet, it does not mark real runtime guidance as delivered or send prompts to OpenCode. Branch 9J adds durable human-control metadata for launched sessions and projects pause/resume/stop/correction/override/report/note intent while always recording that no process control, OpenCode prompt send, or mission mutation occurred. Branch 9K adds a read-only wake supervisor preview that aggregates launched-session evidence across 9A/9D/9E/9F/9G/9H/9I/9J, produces bounded context sections and recommended commands, and appends no wake events, sends no prompts, controls no processes, calls no providers/MCPs, writes no `research.db`, and mutates no missions. Branch 9L adds scheduled wake supervisor execution metadata for explicit single-session and capped batch ticks. It appends bounded `opencode_wake_supervisor_execution_recorded` and batch records with `action_execution_status=not_executed`; it does not execute recommended commands, call providers/MCPs, send OpenCode prompts, control processes, write `research.db`, mutate missions, create checkpoints, or ingest results. Branch 9M adds gated execution for safe metadata-only wake recommendations. It can append action-execution records and call typed existing metadata services for watchdog, forced-report, question, or operator-handoff delivery actions only when the requested action matches the 9L recorded recommendation; it blocks mismatched overrides, arbitrary command execution, answer generation, real OpenCode delivery, result review, provider/MCP calls, process control, `research.db` writes, mission mutation, checkpoints, and result ingestion. Branch 9N adds result-report metadata for launched sessions. It records bounded executor-reported dispositions and review-needed state, but it does not accept/reject results, complete missions or sessions, ingest research, create checkpoints, call providers/MCPs, send OpenCode prompts, control processes, or mutate missions/proposals/reviews/apply records. Later branches should add Commander result review and any broader gated action execution explicitly. diff --git a/agentcore/runtime/src/authority/command-authority-registry.ts b/agentcore/runtime/src/authority/command-authority-registry.ts index e325866b..77bbb271 100644 --- a/agentcore/runtime/src/authority/command-authority-registry.ts +++ b/agentcore/runtime/src/authority/command-authority-registry.ts @@ -42,6 +42,7 @@ const profiles = { opencodeWakeSupervisor: profile(["tests/e2e_user/scenarios/test_opencode_wake_supervisor_preview_tui.py"], ["tests/e2e_user/scenarios/test_command_authority_inventory_tui.py", "tests/e2e_user/scenarios/test_opencode_human_control_tui.py", "tests/e2e_user/scenarios/test_commander_guidance_delivery_tui.py", "tests/e2e_user/scenarios/test_opencode_timeout_watchdog_tui.py"]), opencodeWakeSupervisorExecution: profile(["tests/e2e_user/scenarios/test_opencode_wake_supervisor_execution_tui.py"], ["tests/e2e_user/scenarios/test_command_authority_inventory_tui.py", "tests/e2e_user/scenarios/test_opencode_wake_supervisor_preview_tui.py", "tests/e2e_user/scenarios/test_opencode_human_control_tui.py", "tests/e2e_user/scenarios/test_opencode_timeout_watchdog_tui.py"]), opencodeWakeActionExecution: profile(["tests/e2e_user/scenarios/test_opencode_wake_action_execution_tui.py"], ["tests/e2e_user/scenarios/test_command_authority_inventory_tui.py", "tests/e2e_user/scenarios/test_opencode_wake_supervisor_execution_tui.py", "tests/e2e_user/scenarios/test_opencode_timeout_watchdog_tui.py", "tests/e2e_user/scenarios/test_opencode_asks_commander_tui.py"]), + opencodeResultReport: profile(["tests/e2e_user/scenarios/test_opencode_result_report_tui.py"], ["tests/e2e_user/scenarios/test_command_authority_inventory_tui.py", "tests/e2e_user/scenarios/test_opencode_wake_action_execution_tui.py", "tests/e2e_user/scenarios/test_opencode_progress_heartbeat_tui.py", "tests/e2e_user/scenarios/test_opencode_wake_supervisor_execution_tui.py"]), researchMemory: profile(["tests/e2e_user/scenarios/test_research_memory_novelty_tui.py"], ["tests/e2e_user/scenarios/test_command_authority_inventory_tui.py", "tests/e2e_user/scenarios/test_context_packet_compiler_tui.py", "tests/e2e_user/scenarios/test_context_budget_registry_tui.py"]), executorReview: profile(["tests/e2e_user/scenarios/test_commander_executor_review_tui.py"], ["tests/e2e_user/scenarios/test_opencode_result_review_packet_tui.py", "tests/e2e_user/scenarios/test_opencode_handoff_readiness_tui.py"]), executorReviewDraft: profile(["tests/e2e_user/scenarios/test_executor_review_proposal_draft_tui.py"], ["tests/e2e_user/scenarios/test_commander_executor_review_tui.py", "tests/e2e_user/scenarios/test_opencode_result_review_packet_tui.py"]), @@ -413,6 +414,13 @@ export const COMMAND_AUTHORITY_REGISTRY: CommandAuthorityRecord[] = [ read("/opencode-wake-action-latest", "runtime.latest_opencode_wake_action_execution", "opencode_handoff", "none", profiles.opencodeWakeActionExecution, ["/wake-action-latest"]), read("/opencode-wake-action-show", "runtime.get_opencode_wake_action_execution", "opencode_handoff", "none", profiles.opencodeWakeActionExecution), read("/opencode-wake-action-summary", "runtime.opencode_wake_action_execution_summary", "opencode_handoff", "none", profiles.opencodeWakeActionExecution, ["/wake-action-summary"]), + record({ slash_command: "/opencode-result-report-preview", runtime_command: "runtime.preview_opencode_result_report", risk: "safe_read", gate: "none", owner: "opencode_handoff", mutates_events: false, creates_external_process: false, calls_provider: false, requires_active_runtime: false, requires_run_lock: false, requires_approval: false, current_phase_status: "implemented", validation_profile: profiles.opencodeResultReport, recommended_reads: ["/opencode-launches", "/opencode-progress-latest", "/opencode-result-reports"], aliases: ["/result-report-preview"], notes: ["Read-only OpenCode result-report preview. It validates launched-session and linked evidence metadata only; no mission completion, Commander acceptance/rejection, research.db ingestion, provider call, MCP/online research, OpenCode prompt send, process control, or checkpoint creation occurs."], out_of_scope: ["mission/session completion", "Commander result review acceptance/rejection", "research.db writes/ingestion", "provider/MiniMax calls", "MCP/online research", "OpenCode prompt send", "process pause/kill/stop/resume", "mission/proposal/review/apply mutation", "checkpoint creation"] }), + record({ slash_command: "/opencode-result-report-dry-run", runtime_command: "runtime.record_opencode_result_report", risk: "safe_read", gate: "none", owner: "opencode_handoff", mutates_events: false, creates_external_process: false, calls_provider: false, requires_active_runtime: false, requires_run_lock: false, requires_approval: false, current_phase_status: "implemented", validation_profile: profiles.opencodeResultReport, recommended_reads: ["/opencode-result-report-preview"], aliases: ["/result-report-dry-run"], notes: ["Dry-run OpenCode result-report metadata write validates report eligibility and duplicate hash behavior without appending events, completing missions, writing research.db, creating checkpoints, calling providers, or prompting OpenCode."], out_of_scope: ["event append", "mission/session completion", "Commander result review acceptance/rejection", "research.db writes/ingestion", "provider/MiniMax calls", "MCP/online research", "OpenCode prompt send", "process pause/kill/stop/resume", "mission/proposal/review/apply mutation", "checkpoint creation"] }), + record({ slash_command: "/opencode-result-report", runtime_command: "runtime.record_opencode_result_report", risk: "medium_risk_write", gate: "opencode_runtime", owner: "opencode_handoff", mutates_events: true, creates_external_process: false, calls_provider: false, requires_active_runtime: false, requires_run_lock: true, requires_approval: false, current_phase_status: "implemented", validation_profile: profiles.opencodeResultReport, expected_event_kinds: ["opencode_result_report_recorded"], recommended_reads: ["/opencode-result-report-preview", "/opencode-result-reports", "/opencode-result-report-latest"], aliases: ["/result-report"], notes: ["Appends bounded OpenCode result-report metadata only. The report is executor evidence, not mission/session completion, Commander acceptance/rejection, research.db ingestion, checkpoint creation, provider output, OpenCode prompt delivery, or process control."], out_of_scope: ["mission/session completion", "Commander result review acceptance/rejection", "research.db writes/ingestion", "provider/MiniMax calls", "MCP/online research", "OpenCode prompt send", "process pause/kill/stop/resume", "mission/proposal/review/apply mutation", "checkpoint creation", "result ingestion"] }), + read("/opencode-result-reports", "runtime.list_opencode_result_reports", "opencode_handoff", "none", profiles.opencodeResultReport, ["/result-reports"]), + read("/opencode-result-report-latest", "runtime.latest_opencode_result_report", "opencode_handoff", "none", profiles.opencodeResultReport, ["/result-report-latest"]), + read("/opencode-result-report-show", "runtime.get_opencode_result_report", "opencode_handoff", "none", profiles.opencodeResultReport), + read("/opencode-result-report-summary", "runtime.opencode_result_report_summary", "opencode_handoff", "none", profiles.opencodeResultReport, ["/result-report-summary"]), record({ slash_command: "/research-memory-summary", runtime_command: "runtime.research_memory_summary", risk: "safe_read", gate: "none", owner: "research", mutates_events: false, creates_external_process: false, calls_provider: false, requires_active_runtime: false, requires_run_lock: false, requires_approval: false, current_phase_status: "implemented", validation_profile: profiles.researchMemory, notes: ["Read-only research memory summary. It does not call providers, MCP/online research, write research.db, create missions/proposals, launch OpenCode, or decide research direction."], out_of_scope: ["provider calls", "MCP/online research", "research.db writes", "mission/proposal creation", "OpenCode launch", "Commander cycle execution"] }), record({ slash_command: "/research-memory-search", runtime_command: "runtime.preview_research_memory_retrieval", risk: "safe_read", gate: "none", owner: "research", mutates_events: false, creates_external_process: false, calls_provider: false, requires_active_runtime: false, requires_run_lock: false, requires_approval: false, current_phase_status: "implemented", validation_profile: profiles.researchMemory, aliases: ["/research-search", "/memory-search"], recommended_reads: ["/research-memory-summary", "/research-novelty-preview"], notes: ["Read-only bounded research memory retrieval preview. It returns source refs/previews only and does not call providers, MCP/online research, write research.db, create missions/proposals, launch OpenCode, or decide research direction."], out_of_scope: ["provider calls", "MCP/online research", "research.db writes", "research synthesis execution", "mission/proposal creation", "OpenCode launch", "topic allowlist/blocklist"] }), record({ slash_command: "/research-memory-preview", runtime_command: "runtime.preview_research_memory_retrieval", risk: "safe_read", gate: "none", owner: "research", mutates_events: false, creates_external_process: false, calls_provider: false, requires_active_runtime: false, requires_run_lock: false, requires_approval: false, current_phase_status: "implemented", validation_profile: profiles.researchMemory, notes: ["Alias-shaped read-only research memory retrieval preview. It does not call providers, MCP/online research, launch OpenCode, append events, or write research.db rows."], out_of_scope: ["provider calls", "MCP/online research", "research.db writes", "mission/proposal creation", "OpenCode launch"] }), diff --git a/agentcore/runtime/src/index.ts b/agentcore/runtime/src/index.ts index d383a040..9db56f66 100644 --- a/agentcore/runtime/src/index.ts +++ b/agentcore/runtime/src/index.ts @@ -92,6 +92,8 @@ export { OpenCodeWakeSupervisorExecutionService } from "./opencode-session/openc export * from "./opencode-session/opencode-wake-supervisor-execution-types" export { OpenCodeWakeActionExecutionService } from "./opencode-session/opencode-wake-action-execution-service" export * from "./opencode-session/opencode-wake-action-execution-types" +export { OpenCodeResultReportService } from "./opencode-session/opencode-result-report-service" +export * from "./opencode-session/opencode-result-report-types" export { ContextBudgetService } from "./context/context-budget-service" export { ModelCapabilityRegistry } from "./context/model-capability-registry" export { ContextPacketCompilerService } from "./context/context-packet-compiler-service" diff --git a/agentcore/runtime/src/opencode-session/opencode-result-report-service.ts b/agentcore/runtime/src/opencode-session/opencode-result-report-service.ts new file mode 100644 index 00000000..7770f25e --- /dev/null +++ b/agentcore/runtime/src/opencode-session/opencode-result-report-service.ts @@ -0,0 +1,609 @@ +import { createHash } from "node:crypto" +import type { EventStore } from "../events/event-store" +import type { JsonlEvent } from "../events/event-types" +import { redactValue } from "../security/redaction" +import type { CommanderGuidanceService } from "./opencode-commander-guidance-service" +import type { OpenCodeCommanderQuestionService } from "./opencode-commander-question-service" +import type { CommanderGuidanceDeliveryService } from "./opencode-guidance-delivery-service" +import type { OpenCodeLaunchGateService } from "./opencode-launch-gate-service" +import type { OpenCodeLaunchRecord, OpenCodeLaunchResult } from "./opencode-launch-gate-types" +import type { OpenCodeProgressService } from "./opencode-progress-service" +import type { OpenCodeSessionService } from "./opencode-session-service" +import type { OpenCodeTimeoutWatchdogService } from "./opencode-timeout-watchdog-service" +import type { OpenCodeWakeActionExecutionService } from "./opencode-wake-action-execution-service" +import type { OpenCodeWakeSupervisorExecutionService } from "./opencode-wake-supervisor-execution-service" +import type { + OpenCodeResultDisposition, + OpenCodeResultReportCommand, + OpenCodeResultReportKind, + OpenCodeResultReportPreview, + OpenCodeResultReportPreviewInput, + OpenCodeResultReportRecord, + OpenCodeResultReportRecordInput, + OpenCodeResultReportResult, + OpenCodeResultReportSummary, + OpenCodeResultReviewState, +} from "./opencode-result-report-types" + +const EVENT_KIND = "opencode_result_report_recorded" +const MAX_LIST = 100 +const MAX_ARRAY = 16 +const MAX_TEXT = 360 +const LAUNCHED_STATUSES = new Set(["launch_started", "launched"]) +const RAW_PATTERNS = [ + /\n.{100,}\n.{100,}/s, + /(stdout|stderr|traceback|stack trace|bun test v|npm error|diff --git|@@ |\+\+\+ |--- ).{0,120}\n/i, + /(\[[0-9]{2}:[0-9]{2}:[0-9]{2}\].*\n){3,}/i, + /(full research\.db|research\.db|full event log|events\.jsonl|raw opencode output|provider output|file contents)/i, +] + +export type OpenCodeResultReportServiceOptions = { + eventStore: EventStore + opencodeSessionService: OpenCodeSessionService + launchGateService: OpenCodeLaunchGateService + progressService: OpenCodeProgressService + watchdogService: OpenCodeTimeoutWatchdogService + questionService: OpenCodeCommanderQuestionService + guidanceService: CommanderGuidanceService + guidanceDeliveryService: CommanderGuidanceDeliveryService + wakeExecutionService: OpenCodeWakeSupervisorExecutionService + wakeActionExecutionService: OpenCodeWakeActionExecutionService + now?: () => Date + idFactory?: () => string +} + +type SequencedReport = { record: OpenCodeResultReportRecord; event_index: number } +type Evidence = { + session_id?: string + launch_id?: string + progress?: Awaited> +} + +export class OpenCodeResultReportService { + private readonly now: () => Date + private readonly idFactory: () => string + private recordQueue: Promise = Promise.resolve() + + constructor(private readonly options: OpenCodeResultReportServiceOptions) { + this.now = options.now ?? (() => new Date()) + this.idFactory = options.idFactory ?? (() => `opencode_result_report_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`) + } + + async preview(input: OpenCodeResultReportPreviewInput = {}): Promise { + return this.buildPreview(input) + } + + async record(input: OpenCodeResultReportRecordInput = {}): Promise { + const preview = await this.buildPreview(input) + const reportId = this.idFactory() + const recordedAt = this.now().toISOString() + const recordedBy = bound(input.recorded_by ?? "operator") ?? "operator" + if (!preview.can_record) return resultFromPreview(preview, { report_id: reportId, status: "blocked", recorded_at: recordedAt, recorded_by: recordedBy, error: preview.blockers[0] ?? "OpenCode result report is blocked" }) + const duplicate = await this.findDuplicate(preview.report_hash, preview.session_id, preview.launch_id) + if (duplicate) return resultFromPreview(preview, { report_id: reportId, status: "blocked", recorded_at: recordedAt, recorded_by: recordedBy, error: "duplicate OpenCode result report already exists for this session/launch" }) + if (input.dry_run === true) return resultFromPreview(preview, { report_id: reportId, status: "dry_run", recorded_at: recordedAt, recorded_by: recordedBy }) + return this.serializeRecord(async () => { + const rebuilt = await this.buildPreview(input) + if (!rebuilt.can_record) return resultFromPreview(rebuilt, { report_id: reportId, status: "blocked", recorded_at: recordedAt, recorded_by: recordedBy, error: rebuilt.blockers[0] ?? "OpenCode result report is blocked" }) + const duplicateAfterLock = await this.findDuplicate(rebuilt.report_hash, rebuilt.session_id, rebuilt.launch_id) + if (duplicateAfterLock) return resultFromPreview(rebuilt, { report_id: reportId, status: "blocked", recorded_at: recordedAt, recorded_by: recordedBy, error: "duplicate OpenCode result report already exists for this session/launch" }) + const result = resultFromPreview(rebuilt, { report_id: reportId, status: "recorded", recorded_at: recordedAt, recorded_by: recordedBy }) + await this.options.eventStore.append(eventPayload(result) as JsonlEvent) + return redactValue(result) + }) + } + + async list(input: { limit?: number; session_id?: string; launch_id?: string; result_kind?: string; result_disposition?: string; review_state?: string } = {}): Promise { + const limit = positiveInteger(input.limit, 20, MAX_LIST) + return (await this.sequencedRecords()) + .filter((item) => !input.session_id || item.record.session_id === input.session_id) + .filter((item) => !input.launch_id || item.record.launch_id === input.launch_id) + .filter((item) => !input.result_kind || item.record.result_kind === input.result_kind) + .filter((item) => !input.result_disposition || item.record.result_disposition === input.result_disposition) + .filter((item) => !input.review_state || item.record.review_state === input.review_state) + .sort(compareSequencedDesc) + .map((item) => item.record) + .slice(0, limit) + } + + async get(reportId: string): Promise { + const event = (await this.options.eventStore.readAll()).filter(isReportEvent).reverse().find((item) => item.report_id === reportId) + return event ? resultFromEvent(event) : null + } + + async latest(input: { session_id?: string; launch_id?: string } = {}): Promise { + const latest = (await this.list({ ...input, limit: 1 }))[0] + return latest ? this.get(latest.report_id) : null + } + + async summary(input: { limit?: number } = {}): Promise { + const records = (await this.sequencedRecords()).sort(compareSequencedDesc).map((item) => item.record) + const limit = positiveInteger(input.limit, 10, MAX_LIST) + return redactValue({ + total_reports: records.length, + session_count: new Set(records.map((record) => record.session_id)).size, + completion_count: records.filter((record) => record.result_kind === "completion_report").length, + partial_count: records.filter((record) => record.result_kind === "partial_report").length, + failure_count: records.filter((record) => record.result_kind === "failure_report").length, + inconclusive_count: records.filter((record) => record.result_kind === "inconclusive_report").length, + blocked_count: records.filter((record) => record.result_kind === "blocked_report").length, + needs_commander_review_count: records.filter((record) => record.review_state === "needs_commander_review").length, + needs_human_review_count: records.filter((record) => record.review_state === "needs_human_review").length, + latest_reports: records.slice(0, limit), + generated_at: this.now().toISOString(), + }) + } + + private async buildPreview(input: OpenCodeResultReportPreviewInput): Promise { + const generatedAt = this.now().toISOString() + const blockers: string[] = [] + const warnings = [ + "result reports are executor evidence only; they do not complete missions or accept results", + "no provider, MCP, OpenCode prompt, process control, research.db write, checkpoint, Commander review, or mission mutation occurs", + ] + const evidence = await this.resolveEvidence(input, blockers) + const sessionIdInput = bound(input.session_id) + const launchIdInput = bound(input.launch_id) + let sessionId = sessionIdInput ?? evidence.session_id ?? "" + let launchId = launchIdInput ?? evidence.launch_id + if (!sessionId && !launchId) blockers.push("session_id, launch_id, or linked evidence is required") + let launch: OpenCodeLaunchResult | OpenCodeLaunchRecord | null = null + if (launchId) { + launch = await this.options.launchGateService.get(launchId) + if (!launch) blockers.push("launch_id does not resolve to an OpenCode launch record") + } + if (!launch && sessionId) { + const launches = await this.options.launchGateService.list({ session_id: sessionId, limit: MAX_LIST }) + const candidate = launches.find((item) => LAUNCHED_STATUSES.has(item.status)) ?? launches[0] ?? null + launch = candidate ? (await this.options.launchGateService.get(candidate.launch_id)) ?? candidate : null + if (!launch) blockers.push("OpenCode result report requires a launch record for the session") + } + if (launch && !sessionId) sessionId = launch.session_id + if (sessionId) { + const session = await this.options.opencodeSessionService.get(sessionId) + if (!session) blockers.push("session_id does not resolve to a planned OpenCode session") + } + if (launch && sessionId && launch.session_id !== sessionId) blockers.push("launch_id does not belong to session_id") + if (launch && !LAUNCHED_STATUSES.has(launch.status)) blockers.push(`OpenCode result report requires launch_started or launched status; current status is ${launch.status}`) + sessionId = sessionId || launch?.session_id || "" + launchId = launchId || launch?.launch_id + + const resultKind = readResultKind(input.result_kind, evidence.progress?.kind) + const disposition = dispositionForKind(resultKind) + let reviewState = readReviewState(input.review_state, defaultReviewState(resultKind), blockers) + if (resultKind !== "status_report" && reviewState !== "needs_commander_review" && reviewState !== "needs_human_review") { + blockers.push("terminal OpenCode result reports require Commander or human review") + reviewState = defaultReviewState(resultKind) + } + const rawBlocked = inputLooksRaw(input) + if (rawBlocked) blockers.push("raw logs, full diffs, file contents, provider output, raw OpenCode output, full event logs, and research.db dumps are out of scope for result reports") + + const summary = rawBlocked ? "raw result payload omitted" : bound(input.summary ?? evidence.progress?.report_summary_preview) + const outcome = rawBlocked ? undefined : bound(input.outcome ?? defaultOutcomeFromProgress(evidence.progress)) + const changedFiles = rawBlocked ? [] : boundArray(input.changed_files ?? evidence.progress?.files_touched_preview) + const testsRun = rawBlocked ? [] : boundArray(input.tests_run ?? evidence.progress?.tests_run_preview) + const testResults = rawBlocked ? [] : boundArray(input.test_results) + const artifacts = rawBlocked ? [] : boundArray(input.artifacts ?? evidence.progress?.artifacts_preview) + const metrics = rawBlocked ? [] : boundArray(input.metrics) + const claims = rawBlocked ? [] : boundArray(input.claims) + const knownFailures = rawBlocked ? [] : boundArray(input.known_failures ?? evidence.progress?.blockers_preview) + const followups = rawBlocked ? [] : boundArray(input.followups) + const confidence = readConfidence(input.confidence ?? evidence.progress?.confidence) + + if (!summary) blockers.push("summary is required") + if (resultKind === "completion_report" && !outcome && claims.length === 0) blockers.push("completion_report requires outcome or claims") + if (resultKind === "partial_report" && followups.length === 0 && knownFailures.length === 0) blockers.push("partial_report requires followups or known_failures") + if (resultKind === "failure_report" && !outcome && knownFailures.length === 0) blockers.push("failure_report requires outcome or known_failures") + if (resultKind === "inconclusive_report" && !outcome && knownFailures.length === 0 && followups.length === 0) blockers.push("inconclusive_report requires outcome, known_failures, or followups") + if (resultKind === "blocked_report" && knownFailures.length === 0 && followups.length === 0) blockers.push("blocked_report requires known_failures or followups") + if (resultKind === "unknown") blockers.push("result_kind is unsupported") + + const reportHash = hash(stableJson({ + session_id: sessionId, + launch_id: launchId, + result_kind: resultKind, + result_disposition: disposition, + review_state: reviewState, + summary, + outcome, + changedFiles, + testsRun, + testResults, + artifacts, + metrics, + claims, + knownFailures, + followups, + confidence, + links: linkedIds(input), + })) + + if (sessionId && await this.findDuplicate(reportHash, sessionId, launchId)) blockers.push("duplicate OpenCode result report already exists for this session/launch") + const canRecord = blockers.length === 0 + return redactValue({ + preview_id: `opencode_result_report_preview_${reportHash.slice(0, 16)}`, + status: canRecord ? "ready" : "blocked", + can_record: canRecord, + session_id: sessionId, + launch_id: launchId, + result_kind: resultKind, + result_disposition: disposition, + review_state: reviewState, + summary_preview: summary ?? "", + outcome_preview: outcome, + changed_files_preview: changedFiles, + tests_run_preview: testsRun, + test_results_preview: testResults, + artifacts_preview: artifacts, + metrics_preview: metrics, + claims_preview: claims, + known_failures_preview: knownFailures, + followups_preview: followups, + confidence, + ...linkedIds(input), + mission_mutated: false as const, + research_db_written: false as const, + checkpoint_created: false as const, + commander_review_created: false as const, + blockers: boundArray(unique(blockers)), + warnings: boundArray(unique(warnings)), + recommended_commands: recommendedCommands(sessionId, launchId), + generated_at: generatedAt, + redacted_summary_preview: canRecord ? `OpenCode result report ${resultKind} is ready for durable metadata recording` : blockers[0] ?? "OpenCode result report blocked", + report_hash: reportHash, + }) + } + + private async resolveEvidence(input: OpenCodeResultReportPreviewInput, blockers: string[]): Promise { + const resolved: Evidence = {} + const progressId = bound(input.progress_id) + if (progressId) { + const progress = await this.options.progressService.get(progressId) + if (!progress) blockers.push("progress_id does not resolve to OpenCode progress metadata") + else Object.assign(resolved, { progress, session_id: progress.session_id, launch_id: progress.launch_id }) + } + await this.resolveLinked(input.watchdog_id, "watchdog_id", blockers, resolved, async (id) => this.options.watchdogService.get(id)) + await this.resolveLinked(input.question_id, "question_id", blockers, resolved, async (id) => this.options.questionService.get(id)) + await this.resolveLinked(input.guidance_id, "guidance_id", blockers, resolved, async (id) => this.options.guidanceService.get(id)) + await this.resolveLinked(input.delivery_id, "delivery_id", blockers, resolved, async (id) => this.options.guidanceDeliveryService.get(id)) + await this.resolveLinked(input.wake_execution_id, "wake_execution_id", blockers, resolved, async (id) => this.options.wakeExecutionService.get(id)) + await this.resolveLinked(input.wake_action_execution_id, "wake_action_execution_id", blockers, resolved, async (id) => this.options.wakeActionExecutionService.get(id)) + const sessionId = bound(input.session_id) + const launchId = bound(input.launch_id) + if (sessionId && resolved.session_id && resolved.session_id !== sessionId) blockers.push("linked evidence does not belong to session_id") + if (launchId && resolved.launch_id && resolved.launch_id !== launchId) blockers.push("linked evidence does not belong to launch_id") + return resolved + } + + private async resolveLinked(id: string | undefined, label: string, blockers: string[], resolved: Evidence, get: (id: string) => Promise): Promise { + const value = bound(id) + if (!value) return + const record = await get(value) + if (!record) { + blockers.push(`${label} does not resolve to OpenCode metadata`) + return + } + if (record.session_id && resolved.session_id && resolved.session_id !== record.session_id) blockers.push(`${label} does not belong to the linked session`) + if (record.launch_id && resolved.launch_id && resolved.launch_id !== record.launch_id) blockers.push(`${label} does not belong to the linked launch`) + resolved.session_id ??= record.session_id + resolved.launch_id ??= record.launch_id + } + + private async findDuplicate(reportHash: string, sessionId: string, launchId?: string): Promise { + return (await this.sequencedRecords()) + .find((item) => item.record.session_id === sessionId && item.record.launch_id === launchId && item.record.report_hash === reportHash) + ?.record + } + + private async sequencedRecords(): Promise { + return (await this.options.eventStore.readAll()) + .map((event, index) => ({ event, index })) + .filter((item) => isReportEvent(item.event)) + .map((item) => ({ record: recordFromEvent(item.event), event_index: item.index })) + } + + private serializeRecord(operation: () => Promise): Promise { + const next = this.recordQueue.then(operation, operation) + this.recordQueue = next.then(() => undefined, () => undefined) + return next + } +} + +export function readOpenCodeResultReportPreviewInput(value: unknown): OpenCodeResultReportPreviewInput { + const input = isRecord(value) ? value : {} + return { + session_id: optional(input.sessionId ?? input.session_id ?? input.session), + launch_id: optional(input.launchId ?? input.launch_id ?? input.launch), + result_kind: optional(input.resultKind ?? input.result_kind ?? input.kind), + summary: optionalRaw(input.summary), + outcome: optionalRaw(input.outcome), + changed_files: optionalArray(input.changedFiles ?? input.changed_files), + tests_run: optionalArray(input.testsRun ?? input.tests_run), + test_results: optionalArray(input.testResults ?? input.test_results), + artifacts: optionalArray(input.artifacts), + metrics: optionalArray(input.metrics), + claims: optionalArray(input.claims), + known_failures: optionalArray(input.knownFailures ?? input.known_failures), + followups: optionalArray(input.followups), + confidence: input.confidence, + progress_id: optional(input.progressId ?? input.progress_id ?? input.progress), + watchdog_id: optional(input.watchdogId ?? input.watchdog_id ?? input.watchdog), + question_id: optional(input.questionId ?? input.question_id ?? input.question), + guidance_id: optional(input.guidanceId ?? input.guidance_id ?? input.guidance), + delivery_id: optional(input.deliveryId ?? input.delivery_id ?? input.delivery), + wake_execution_id: optional(input.wakeExecutionId ?? input.wakeExecution ?? input.wake_execution_id ?? input.wake_execution), + wake_action_execution_id: optional(input.wakeActionExecutionId ?? input.wakeAction ?? input.wake_action_execution_id ?? input.wake_action), + review_state: optional(input.reviewState ?? input.review_state), + } +} + +export function readOpenCodeResultReportRecordInput(value: unknown): OpenCodeResultReportRecordInput { + const input = isRecord(value) ? value : {} + return { + ...readOpenCodeResultReportPreviewInput(value), + dry_run: optionalBoolean(input.dryRun ?? input.dry_run), + recorded_by: optional(input.recordedBy ?? input.recorded_by), + } +} + +function resultFromPreview(preview: OpenCodeResultReportPreview, overrides: { report_id: string; status: OpenCodeResultReportResult["status"]; recorded_at: string; recorded_by: string; error?: string }): OpenCodeResultReportResult { + return redactValue({ + report_id: overrides.report_id, + status: overrides.status, + session_id: preview.session_id, + launch_id: preview.launch_id, + result_kind: preview.result_kind, + result_disposition: preview.result_disposition, + review_state: preview.review_state, + summary_preview: preview.summary_preview, + outcome_preview: preview.outcome_preview, + changed_files_preview: preview.changed_files_preview, + tests_run_preview: preview.tests_run_preview, + test_results_preview: preview.test_results_preview, + artifacts_preview: preview.artifacts_preview, + metrics_preview: preview.metrics_preview, + claims_preview: preview.claims_preview, + known_failures_preview: preview.known_failures_preview, + followups_preview: preview.followups_preview, + confidence: preview.confidence, + linked_progress_id: preview.linked_progress_id, + linked_watchdog_id: preview.linked_watchdog_id, + linked_question_id: preview.linked_question_id, + linked_guidance_id: preview.linked_guidance_id, + linked_delivery_id: preview.linked_delivery_id, + linked_wake_execution_id: preview.linked_wake_execution_id, + linked_wake_action_execution_id: preview.linked_wake_action_execution_id, + mission_mutated: false as const, + research_db_written: false as const, + checkpoint_created: false as const, + commander_review_created: false as const, + recorded_at: overrides.recorded_at, + recorded_by: bound(overrides.recorded_by) ?? "operator", + error: bound(overrides.error), + report_hash: preview.report_hash, + recommended_commands: preview.recommended_commands, + }) +} + +function eventPayload(result: OpenCodeResultReportResult): Record { + return redactValue({ kind: EVENT_KIND, ...result, status: undefined, recommended_commands: undefined, error: undefined }) +} + +function resultFromEvent(event: Record): OpenCodeResultReportResult { + return redactValue({ + report_id: String(event.report_id ?? ""), + status: "recorded" as const, + ...baseFromEvent(event), + recorded_at: String(event.recorded_at ?? ""), + recorded_by: String(event.recorded_by ?? "operator"), + report_hash: String(event.report_hash ?? ""), + recommended_commands: recommendedCommands(String(event.session_id ?? ""), typeof event.launch_id === "string" ? event.launch_id : undefined), + }) +} + +function recordFromEvent(event: Record): OpenCodeResultReportRecord { + const base = baseFromEvent(event) + return redactValue({ + report_id: String(event.report_id ?? ""), + session_id: base.session_id, + launch_id: base.launch_id, + result_kind: base.result_kind, + result_disposition: base.result_disposition, + review_state: base.review_state, + summary_preview: base.summary_preview, + recorded_at: String(event.recorded_at ?? ""), + recorded_by: String(event.recorded_by ?? "operator"), + confidence: base.confidence, + has_failures: base.known_failures_preview.length > 0, + has_artifacts: base.artifacts_preview.length > 0, + has_metrics: base.metrics_preview.length > 0, + report_hash: String(event.report_hash ?? ""), + }) +} + +function baseFromEvent(event: Record) { + return { + session_id: String(event.session_id ?? ""), + launch_id: optional(event.launch_id), + result_kind: readResultKind(event.result_kind), + result_disposition: readDisposition(event.result_disposition), + review_state: readReviewStateValue(event.review_state), + summary_preview: String(event.summary_preview ?? ""), + outcome_preview: optional(event.outcome_preview), + changed_files_preview: boundArray(event.changed_files_preview), + tests_run_preview: boundArray(event.tests_run_preview), + test_results_preview: boundArray(event.test_results_preview), + artifacts_preview: boundArray(event.artifacts_preview), + metrics_preview: boundArray(event.metrics_preview), + claims_preview: boundArray(event.claims_preview), + known_failures_preview: boundArray(event.known_failures_preview), + followups_preview: boundArray(event.followups_preview), + confidence: readConfidence(event.confidence), + linked_progress_id: optional(event.linked_progress_id), + linked_watchdog_id: optional(event.linked_watchdog_id), + linked_question_id: optional(event.linked_question_id), + linked_guidance_id: optional(event.linked_guidance_id), + linked_delivery_id: optional(event.linked_delivery_id), + linked_wake_execution_id: optional(event.linked_wake_execution_id), + linked_wake_action_execution_id: optional(event.linked_wake_action_execution_id), + mission_mutated: false as const, + research_db_written: false as const, + checkpoint_created: false as const, + commander_review_created: false as const, + } +} + +function linkedIds(input: OpenCodeResultReportPreviewInput): Partial { + return { + linked_progress_id: bound(input.progress_id), + linked_watchdog_id: bound(input.watchdog_id), + linked_question_id: bound(input.question_id), + linked_guidance_id: bound(input.guidance_id), + linked_delivery_id: bound(input.delivery_id), + linked_wake_execution_id: bound(input.wake_execution_id), + linked_wake_action_execution_id: bound(input.wake_action_execution_id), + } +} + +function readResultKind(value: unknown, progressKind?: string): OpenCodeResultReportKind { + const raw = String(value ?? progressKind ?? "status_report") + if (["completion_report", "partial_report", "failure_report", "inconclusive_report", "blocked_report", "status_report"].includes(raw)) return raw as OpenCodeResultReportKind + if (raw === "progress" || raw === "heartbeat" || raw === "question" || raw === "blocker" || raw === "checkpoint_note") return "status_report" + return "unknown" +} + +function dispositionForKind(kind: OpenCodeResultReportKind): OpenCodeResultDisposition { + if (kind === "completion_report") return "reported_done" + if (kind === "partial_report") return "reported_partial" + if (kind === "failure_report") return "reported_failed" + if (kind === "inconclusive_report") return "reported_inconclusive" + if (kind === "blocked_report") return "reported_blocked" + return "reported_status_only" +} + +function readDisposition(value: unknown): OpenCodeResultDisposition { + const raw = String(value ?? "") + if (["reported_done", "reported_partial", "reported_failed", "reported_inconclusive", "reported_blocked", "reported_status_only"].includes(raw)) return raw as OpenCodeResultDisposition + return "reported_status_only" +} + +function defaultReviewState(kind: OpenCodeResultReportKind): OpenCodeResultReviewState { + return kind === "status_report" ? "review_not_required" : "needs_commander_review" +} + +function readReviewState(value: unknown, fallback: OpenCodeResultReviewState, blockers: string[]): OpenCodeResultReviewState { + const raw = optional(value) + if (!raw) return fallback + if (raw === "accepted" || raw === "rejected" || raw === "approved") { + blockers.push("accepted/rejected result review states belong to a future Commander result review branch") + return fallback + } + return readReviewStateValue(raw, fallback) +} + +function readReviewStateValue(value: unknown, fallback: OpenCodeResultReviewState = "unknown"): OpenCodeResultReviewState { + const raw = String(value ?? "") + if (["needs_commander_review", "needs_human_review", "not_ready_for_review", "review_not_required", "unknown"].includes(raw)) return raw as OpenCodeResultReviewState + return fallback +} + +function defaultOutcomeFromProgress(progress: Evidence["progress"]): string | undefined { + if (!progress) return undefined + if (progress.kind === "completion_report") return progress.report_summary_preview + if (progress.kind === "failure_report") return progress.report_summary_preview + return undefined +} + +function readConfidence(value: unknown): string | number | undefined { + if (typeof value === "number" && Number.isFinite(value)) return Math.max(0, Math.min(1, value)) + const raw = optional(value) + if (!raw) return undefined + if (["low", "medium", "high", "unknown"].includes(raw)) return raw + const parsed = Number(raw) + return Number.isFinite(parsed) ? Math.max(0, Math.min(1, parsed)) : "unknown" +} + +function inputLooksRaw(input: OpenCodeResultReportPreviewInput): boolean { + const values = [input.summary, input.outcome, ...(input.changed_files ?? []), ...(input.tests_run ?? []), ...(input.test_results ?? []), ...(input.artifacts ?? []), ...(input.metrics ?? []), ...(input.claims ?? []), ...(input.known_failures ?? []), ...(input.followups ?? [])] + return values.some((value) => typeof value === "string" && (value.length > 2000 || RAW_PATTERNS.some((pattern) => pattern.test(value)))) +} + +function recommendedCommands(sessionId: string, launchId?: string): OpenCodeResultReportCommand[] { + return [ + { label: "List result reports", command: sessionId ? `/opencode-result-reports session=${sessionId}` : "/opencode-result-reports", command_type: "read", notes: "read bounded result-report metadata" }, + { label: "Latest result report", command: sessionId ? `/opencode-result-report-latest session=${sessionId}` : "/opencode-result-report-latest session=", command_type: "read", notes: "read latest result-report metadata" }, + { label: "Record result report", command: sessionId ? `/opencode-result-report session=${sessionId} kind=completion_report summary= outcome=` : launchId ? `/opencode-result-report launch=${launchId} kind=completion_report summary= outcome=` : "/opencode-result-report session= kind=completion_report summary= outcome=", command_type: "write", requires_active_runtime: true, notes: "manual metadata write only; does not complete mission or ingest research" }, + ] +} + +function isReportEvent(event: unknown): event is Record { + return isRecord(event) && event.kind === EVENT_KIND && typeof event.report_id === "string" && typeof event.session_id === "string" +} + +function compareSequencedDesc(a: SequencedReport, b: SequencedReport): number { + const time = b.record.recorded_at.localeCompare(a.record.recorded_at) + return time || b.event_index - a.event_index +} + +function positiveInteger(value: unknown, fallback: number, max: number): number { + const parsed = typeof value === "number" ? value : Number(value) + return Number.isFinite(parsed) ? Math.max(1, Math.min(Math.floor(parsed), max)) : fallback +} + +function bound(value: unknown): string | undefined { + if (typeof value !== "string") return undefined + const trimmed = value.trim() + return trimmed ? trimmed.slice(0, MAX_TEXT) : undefined +} + +function optional(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim().slice(0, MAX_TEXT) : undefined +} + +function optionalRaw(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined +} + +function optionalArray(value: unknown): string[] | undefined { + if (Array.isArray(value)) return value.filter((item): item is string => typeof item === "string") + if (typeof value === "string" && value.trim()) return value.split(",").map((item) => item.trim()).filter(Boolean) + return undefined +} + +function boundArray(value: unknown): string[] { + return (Array.isArray(value) ? value : optionalArray(value) ?? []) + .filter((item): item is string => typeof item === "string") + .map((item) => item.trim()) + .filter(Boolean) + .slice(0, MAX_ARRAY) + .map((item) => item.slice(0, MAX_TEXT)) +} + +function optionalBoolean(value: unknown): boolean | undefined { + if (typeof value === "boolean") return value + if (value === "true") return true + if (value === "false") return false + return undefined +} + +function unique(items: T[]): T[] { + return [...new Set(items)] +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null +} + +function stableJson(value: unknown): string { + return JSON.stringify(sortKeys(value)) +} + +function sortKeys(value: unknown): unknown { + if (Array.isArray(value)) return value.map(sortKeys) + if (isRecord(value)) return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortKeys(value[key])])) + return value +} + +function hash(value: string): string { + return createHash("sha256").update(value).digest("hex") +} diff --git a/agentcore/runtime/src/opencode-session/opencode-result-report-types.ts b/agentcore/runtime/src/opencode-session/opencode-result-report-types.ts new file mode 100644 index 00000000..81a45aab --- /dev/null +++ b/agentcore/runtime/src/opencode-session/opencode-result-report-types.ts @@ -0,0 +1,141 @@ +export type OpenCodeResultReportKind = + | "completion_report" + | "partial_report" + | "failure_report" + | "inconclusive_report" + | "blocked_report" + | "status_report" + | "unknown" + +export type OpenCodeResultReportStatus = "ready" | "blocked" | "dry_run" | "recorded" | "failed" + +export type OpenCodeResultDisposition = + | "reported_done" + | "reported_partial" + | "reported_failed" + | "reported_inconclusive" + | "reported_blocked" + | "reported_status_only" + +export type OpenCodeResultReviewState = + | "needs_commander_review" + | "needs_human_review" + | "not_ready_for_review" + | "review_not_required" + | "unknown" + +export type OpenCodeResultReportCommand = { + label: string + command: string + command_type: "read" | "write" + requires_active_runtime?: boolean + notes?: string +} + +export type OpenCodeResultReportPreview = { + preview_id: string + status: "ready" | "blocked" + can_record: boolean + session_id: string + launch_id?: string + result_kind: OpenCodeResultReportKind + result_disposition: OpenCodeResultDisposition + review_state: OpenCodeResultReviewState + summary_preview: string + outcome_preview?: string + changed_files_preview: string[] + tests_run_preview: string[] + test_results_preview: string[] + artifacts_preview: string[] + metrics_preview: string[] + claims_preview: string[] + known_failures_preview: string[] + followups_preview: string[] + confidence?: string | number + linked_progress_id?: string + linked_watchdog_id?: string + linked_question_id?: string + linked_guidance_id?: string + linked_delivery_id?: string + linked_wake_execution_id?: string + linked_wake_action_execution_id?: string + mission_mutated: false + research_db_written: false + checkpoint_created: false + commander_review_created: false + blockers: string[] + warnings: string[] + recommended_commands: OpenCodeResultReportCommand[] + generated_at: string + redacted_summary_preview: string + report_hash: string +} + +export type OpenCodeResultReportResult = Omit & { + report_id: string + status: "recorded" | "blocked" | "dry_run" | "failed" + recorded_at: string + recorded_by: string + error?: string +} + +export type OpenCodeResultReportRecord = { + report_id: string + session_id: string + launch_id?: string + result_kind: OpenCodeResultReportKind + result_disposition: OpenCodeResultDisposition + review_state: OpenCodeResultReviewState + summary_preview: string + recorded_at: string + recorded_by: string + confidence?: string | number + has_failures: boolean + has_artifacts: boolean + has_metrics: boolean + report_hash: string +} + +export type OpenCodeResultReportSummary = { + total_reports: number + session_count: number + completion_count: number + partial_count: number + failure_count: number + inconclusive_count: number + blocked_count: number + needs_commander_review_count: number + needs_human_review_count: number + latest_reports: OpenCodeResultReportRecord[] + generated_at: string +} + +export type OpenCodeResultReportPreviewInput = { + session_id?: string + launch_id?: string + result_kind?: string + summary?: string + outcome?: string + changed_files?: string[] + tests_run?: string[] + test_results?: string[] + artifacts?: string[] + metrics?: string[] + claims?: string[] + known_failures?: string[] + followups?: string[] + confidence?: unknown + progress_id?: string + watchdog_id?: string + question_id?: string + guidance_id?: string + delivery_id?: string + wake_execution_id?: string + wake_action_execution_id?: string + review_state?: string +} + +export type OpenCodeResultReportRecordInput = OpenCodeResultReportPreviewInput & { + dry_run?: boolean + recorded_by?: string +} diff --git a/agentcore/runtime/src/runtime.test.ts b/agentcore/runtime/src/runtime.test.ts index 05b2dd28..d3649c3a 100644 --- a/agentcore/runtime/src/runtime.test.ts +++ b/agentcore/runtime/src/runtime.test.ts @@ -996,6 +996,10 @@ describe("CommandAuthorityService", () => { expect(service.get("/opencode-smoke")).toMatchObject({ risk: "low_risk_write", gate: "opencode_runtime", creates_external_process: true, mutates_events: true, blocked_by_default: true }) expect(service.get("/result-review-packet")).toMatchObject({ risk: "safe_read", runtime_command: "runtime.preview_opencode_result_review_packet", owner: "opencode_handoff", mutates_events: false }) expect(service.get("/result-review-summary")).toMatchObject({ risk: "safe_read", runtime_command: "runtime.opencode_result_review_summary", owner: "opencode_handoff", mutates_events: false }) + expect(service.get("/opencode-result-report-preview")).toMatchObject({ risk: "safe_read", runtime_command: "runtime.preview_opencode_result_report", mutates_events: false }) + expect(service.get("/opencode-result-report-dry-run")).toMatchObject({ risk: "safe_read", runtime_command: "runtime.record_opencode_result_report", mutates_events: false }) + expect(service.get("/opencode-result-report")).toMatchObject({ risk: "medium_risk_write", gate: "opencode_runtime", owner: "opencode_handoff", mutates_events: true, expected_event_kinds: ["opencode_result_report_recorded"] }) + expect(service.get("/opencode-result-reports")).toMatchObject({ risk: "safe_read", runtime_command: "runtime.list_opencode_result_reports", mutates_events: false }) expect(service.get("/opencode-session-preview")).toMatchObject({ risk: "safe_read", runtime_command: "runtime.preview_opencode_session_plan", mutates_events: false, creates_external_process: false }) expect(service.get("/opencode-session-plan-dry-run")).toMatchObject({ risk: "safe_read", runtime_command: "runtime.create_opencode_session_plan", mutates_events: false, creates_external_process: false }) expect(service.get("/opencode-session-plan")).toMatchObject({ risk: "high_impact_write", gate: "opencode_runtime", owner: "opencode_handoff", mutates_events: true, creates_external_process: false, blocked_by_default: true }) @@ -1049,6 +1053,10 @@ describe("CommandAuthorityService", () => { expect(packet.targeted_e2e).toEqual(["tests/e2e_user/scenarios/test_opencode_result_review_packet_tui.py"]) expect(packet.optional_regression_e2e).toEqual(["tests/e2e_user/scenarios/test_opencode_handoff_readiness_tui.py", "tests/e2e_user/scenarios/test_opencode_handoff_tui.py", "tests/e2e_user/scenarios/test_opencode_handoff_followup_tui.py"]) + const resultReport = service.validationProfile("/opencode-result-report") + expect(resultReport.targeted_e2e).toEqual(["tests/e2e_user/scenarios/test_opencode_result_report_tui.py"]) + expect(resultReport.optional_regression_e2e).toEqual(["tests/e2e_user/scenarios/test_command_authority_inventory_tui.py", "tests/e2e_user/scenarios/test_opencode_wake_action_execution_tui.py", "tests/e2e_user/scenarios/test_opencode_progress_heartbeat_tui.py", "tests/e2e_user/scenarios/test_opencode_wake_supervisor_execution_tui.py"]) + const minimax = service.validationProfile("/minimax-live-validate") expect(minimax.targeted_e2e).toEqual(["tests/e2e_user/scenarios/test_minimax_live_validation_tui.py"]) expect(minimax.optional_regression_e2e).toEqual(["tests/e2e_user/scenarios/test_reasoning_provider_tui.py", "tests/e2e_user/scenarios/test_command_authority_inventory_tui.py"]) @@ -14873,6 +14881,23 @@ describe("RuntimeServerClient", () => { await client.shutdown() }) + test("OpenCode result report commands and dry-runs do not auto-start the runtime", async () => { + const dir = await tempProject() + await makeProject(dir, { approvedSpec: true }) + const server = new RuntimeServer({ projectDir: dir, adapter: new LongLivedAdapter() }) + const client = new RuntimeServerClient({ server, autoStart: true, ownsServer: true }) + + await expect(client.command("runtime.preview_opencode_result_report", { sessionId: "missing", summary: "missing" })).resolves.toMatchObject({ status: "blocked" }) + await expect(client.command("runtime.record_opencode_result_report", { sessionId: "missing", summary: "missing", dryRun: true })).resolves.toMatchObject({ status: "blocked" }) + await expect(client.command("runtime.list_opencode_result_reports")).resolves.toEqual([]) + await expect(client.command("runtime.get_opencode_result_report", { reportId: "missing" })).resolves.toBeNull() + await expect(client.command("runtime.latest_opencode_result_report", { sessionId: "missing" })).resolves.toBeNull() + await expect(client.command("runtime.opencode_result_report_summary")).resolves.toMatchObject({ total_reports: 0 }) + + expect(await readEventKinds(dir)).not.toContain("runtime_started") + await client.shutdown() + }) + test("commander executor review commands do not auto-start the runtime", async () => { const dir = await tempProject() await makeProject(dir, { approvedSpec: true }) @@ -18932,6 +18957,236 @@ describe("OpenCode launch readiness", () => { await server.shutdown() }) + test("result report records bounded executor evidence without mission or research side effects", async () => { + const dir = await tempProject() + const { server, sessionId, packId } = await readyLaunchFixture(dir) + const launched = await server.command("runtime.launch_opencode_session", { sessionId, packId, providerKind: "local", modelId: "local-medium" }) as { status: string; launch_id: string } + expect(launched.status).toBe("launched") + const progress = await server.command("runtime.record_opencode_progress", { sessionId, kind: "completion_report", summary: "done with candidate fix token=result-secret" }) as { progress_id: string } + let beforeReport = await server.eventStore.readAll() + + await expect(server.command("runtime.preview_opencode_result_report", {})).resolves.toMatchObject({ status: "blocked", can_record: false }) + await expect(server.command("runtime.preview_opencode_result_report", { sessionId: "missing", summary: "missing" })).resolves.toMatchObject({ status: "blocked", can_record: false }) + await expect(server.command("runtime.preview_opencode_result_report", { sessionId, kind: "completion_report", summary: "done" })).resolves.toMatchObject({ status: "blocked", can_record: false }) + await expect(server.command("runtime.preview_opencode_result_report", { sessionId, kind: "partial_report", summary: "partial" })).resolves.toMatchObject({ status: "blocked", can_record: false }) + await expect(server.command("runtime.preview_opencode_result_report", { sessionId, kind: "failure_report", summary: "failed" })).resolves.toMatchObject({ status: "blocked", can_record: false }) + await expect(server.command("runtime.preview_opencode_result_report", { sessionId, kind: "status_report", summary: "status only" })).resolves.toMatchObject({ status: "ready", can_record: true, result_disposition: "reported_status_only", review_state: "review_not_required" }) + await expect(server.command("runtime.preview_opencode_result_report", { sessionId, kind: "completion_report", summary: "done", outcome: "tests passed", reviewState: "accepted" })).resolves.toMatchObject({ status: "blocked", can_record: false }) + await expect(server.command("runtime.preview_opencode_result_report", { sessionId, kind: "completion_report", summary: "done", outcome: "tests passed", reviewState: "review_not_required" })).resolves.toMatchObject({ status: "blocked", can_record: false, review_state: "needs_commander_review" }) + await expect(server.command("runtime.preview_opencode_result_report", { sessionId, kind: "completion_report", summary: "done", outcome: "tests passed", reviewState: "unknown" })).resolves.toMatchObject({ status: "blocked", can_record: false, review_state: "needs_commander_review" }) + await expect(server.command("runtime.preview_opencode_result_report", { sessionId, kind: "failure_report", summary: "failed", knownFailures: ["test failed"], reviewState: "not_ready_for_review" })).resolves.toMatchObject({ status: "blocked", can_record: false, review_state: "needs_commander_review" }) + await expect(server.command("runtime.preview_opencode_result_report", { sessionId, kind: "completion_report", summary: "diff --git a/file b/file\n@@ -1 +1", outcome: "raw diff" })).resolves.toMatchObject({ status: "blocked", can_record: false }) + await expect(server.command("runtime.preview_opencode_result_report", { sessionId, kind: "completion_report", summary: "provider output says done", outcome: "file contents omitted" })).resolves.toMatchObject({ status: "blocked", can_record: false }) + await server.eventStore.append({ + kind: "opencode_session_launch_started", + launch_id: "launch_orphan_result_report", + status: "launch_started", + session_id: "missing_result_report_session", + adapter_kind: "fake", + launch_mode: "fresh", + started_at: "2026-07-06T10:00:30.000Z", + launch_hash: "launch_orphan_result_report_hash", + }) + await expect(server.command("runtime.preview_opencode_result_report", { launchId: "launch_orphan_result_report", kind: "status_report", summary: "orphan launch" })).resolves.toMatchObject({ + status: "blocked", + can_record: false, + session_id: "missing_result_report_session", + blockers: expect.arrayContaining(["session_id does not resolve to a planned OpenCode session"]), + }) + beforeReport = await server.eventStore.readAll() + + const preview = await server.command("runtime.preview_opencode_result_report", { sessionId, kind: "completion_report", summary: "implemented candidate fix token=result-secret", outcome: "tests passed", changedFiles: ["fileA.ts"], testsRun: ["bun test"], claims: ["fix works"], followups: ["commander review"], progressId: progress.progress_id }) as { + status: string + can_record: boolean + result_kind: string + result_disposition: string + review_state: string + mission_mutated: boolean + research_db_written: boolean + checkpoint_created: boolean + commander_review_created: boolean + report_hash: string + } + expect(preview).toMatchObject({ + status: "ready", + can_record: true, + result_kind: "completion_report", + result_disposition: "reported_done", + review_state: "needs_commander_review", + mission_mutated: false, + research_db_written: false, + checkpoint_created: false, + commander_review_created: false, + }) + expect(JSON.stringify(preview)).not.toContain("result-secret") + + await expect(server.command("runtime.record_opencode_result_report", { sessionId, kind: "completion_report", summary: "implemented candidate fix token=result-secret", outcome: "tests passed", changedFiles: ["fileA.ts"], testsRun: ["bun test"], claims: ["fix works"], followups: ["commander review"], progressId: progress.progress_id, dryRun: true })).resolves.toMatchObject({ status: "dry_run", result_kind: "completion_report" }) + expect(await server.eventStore.readAll()).toEqual(beforeReport) + + const report = await server.command("runtime.record_opencode_result_report", { sessionId, kind: "completion_report", summary: "implemented candidate fix token=result-secret", outcome: "tests passed", changedFiles: ["fileA.ts"], testsRun: ["bun test"], testResults: ["passed"], artifacts: ["artifact://result"], metrics: ["tests=1"], claims: ["fix works"], followups: ["commander review"], progressId: progress.progress_id, confidence: "high" }) as { + status: string + report_id: string + report_hash: string + mission_mutated: boolean + research_db_written: boolean + checkpoint_created: boolean + commander_review_created: boolean + } + expect(report).toMatchObject({ + status: "recorded", + mission_mutated: false, + research_db_written: false, + checkpoint_created: false, + commander_review_created: false, + }) + let events = await server.eventStore.readAll() + let newEvents = events.slice(beforeReport.length) + expect(newEvents.map((event) => event.kind)).toEqual(["opencode_result_report_recorded"]) + expect(newEvents[0]).toMatchObject({ + report_id: report.report_id, + session_id: sessionId, + launch_id: launched.launch_id, + result_kind: "completion_report", + result_disposition: "reported_done", + review_state: "needs_commander_review", + mission_mutated: false, + research_db_written: false, + checkpoint_created: false, + commander_review_created: false, + }) + expect(JSON.stringify(newEvents)).not.toContain("result-secret") + + await expect(server.command("runtime.record_opencode_result_report", { sessionId, kind: "completion_report", summary: "implemented candidate fix token=result-secret", outcome: "tests passed", changedFiles: ["fileA.ts"], testsRun: ["bun test"], testResults: ["passed"], artifacts: ["artifact://result"], metrics: ["tests=1"], claims: ["fix works"], followups: ["commander review"], progressId: progress.progress_id, confidence: "high", dryRun: true })).resolves.toMatchObject({ status: "blocked" }) + await expect(server.command("runtime.record_opencode_result_report", { sessionId, kind: "completion_report", summary: "implemented candidate fix token=result-secret", outcome: "tests passed", changedFiles: ["fileA.ts"], testsRun: ["bun test"], testResults: ["passed"], artifacts: ["artifact://result"], metrics: ["tests=1"], claims: ["fix works"], followups: ["commander review"], progressId: progress.progress_id, confidence: "high" })).resolves.toMatchObject({ status: "blocked" }) + expect(await server.eventStore.readAll()).toEqual(events) + + const failure = await server.command("runtime.record_opencode_result_report", { sessionId, kind: "failure_report", summary: "failed alternative", knownFailures: ["test still fails"] }) as { status: string; report_id: string; result_kind: string } + expect(failure).toMatchObject({ status: "recorded", result_kind: "failure_report" }) + events = await server.eventStore.readAll() + newEvents = events.slice(beforeReport.length) + expect(newEvents.filter((event) => event.kind === "opencode_result_report_recorded")).toHaveLength(2) + + await expect(server.command("runtime.list_opencode_result_reports", { sessionId })).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ report_id: report.report_id, result_kind: "completion_report" })])) + await expect(server.command("runtime.get_opencode_result_report", { reportId: report.report_id })).resolves.toMatchObject({ report_id: report.report_id, session_id: sessionId }) + await expect(server.command("runtime.latest_opencode_result_report", { sessionId })).resolves.toMatchObject({ report_id: failure.report_id, result_kind: "failure_report" }) + await expect(server.command("runtime.opencode_result_report_summary")).resolves.toMatchObject({ total_reports: 2, completion_count: 1, failure_count: 1, needs_commander_review_count: 2 }) + + const wakeExecution = await server.command("runtime.record_opencode_wake_supervisor_execution", { sessionId }) as { execution_id: string } + await expect(server.command("runtime.preview_opencode_result_report", { wakeExecution: wakeExecution.execution_id, kind: "status_report", summary: "linked from wake execution" })).resolves.toMatchObject({ + status: "ready", + session_id: sessionId, + linked_wake_execution_id: wakeExecution.execution_id, + }) + const wakeActionId = "wake_action_result_alias" + await server.eventStore.append({ + kind: "opencode_wake_action_execution_recorded", + action_execution_id: wakeActionId, + execution_id: wakeExecution.execution_id, + session_id: sessionId, + launch_id: launched.launch_id, + supervisor_status: "watch", + recommended_action: "read_latest_progress", + action_kind: "read_latest_progress", + status: "skipped", + effect_kind: "read_only_noop", + action_execution_status_before: "not_executed", + will_call_provider: false, + will_send_opencode_prompt: false, + will_control_process: false, + will_mutate_mission: false, + evidence_refs: [], + recorded_at: "2026-07-06T10:03:00.000Z", + recorded_by: "test", + action_hash: "wake_action_hash_result_alias", + }) + await expect(server.command("runtime.preview_opencode_result_report", { wakeAction: wakeActionId, kind: "status_report", summary: "linked from wake action" })).resolves.toMatchObject({ + status: "ready", + session_id: sessionId, + linked_wake_action_execution_id: wakeActionId, + }) + + for (let index = 0; index < 101; index += 1) { + await server.eventStore.append({ + kind: "opencode_result_report_recorded", + report_id: `report_padding_${index}`, + session_id: sessionId, + launch_id: launched.launch_id, + result_kind: "status_report", + result_disposition: "reported_status_only", + review_state: "review_not_required", + summary_preview: `padding report ${index}`, + changed_files_preview: [], + tests_run_preview: [], + test_results_preview: [], + artifacts_preview: [], + metrics_preview: [], + claims_preview: [], + known_failures_preview: [], + followups_preview: [], + mission_mutated: false, + research_db_written: false, + checkpoint_created: false, + commander_review_created: false, + recorded_at: `2026-07-06T10:${String(10 + index).padStart(2, "0")}:00.000Z`, + recorded_by: "test", + report_hash: `padding_hash_${index}`, + }) + } + events = await server.eventStore.readAll() + await expect(server.command("runtime.record_opencode_result_report", { sessionId, kind: "completion_report", summary: "implemented candidate fix token=result-secret", outcome: "tests passed", changedFiles: ["fileA.ts"], testsRun: ["bun test"], testResults: ["passed"], artifacts: ["artifact://result"], metrics: ["tests=1"], claims: ["fix works"], followups: ["commander review"], progressId: progress.progress_id, confidence: "high", dryRun: true })).resolves.toMatchObject({ status: "blocked" }) + expect(await server.eventStore.readAll()).toEqual(events) + + await server.eventStore.append({ + kind: "opencode_session_progress_recorded", + progress_id: "progress_unrelated_result_report", + session_id: "session_unrelated_result_report", + launch_id: "launch_unrelated_result_report", + progress_kind: "completion_report", + execution_state: "reported_done", + report_summary_preview: "unrelated done", + files_touched_preview: [], + commands_run_preview: [], + tests_run_preview: [], + artifacts_preview: [], + blockers_preview: [], + recorded_at: "2026-07-06T10:00:00.000Z", + recorded_by: "test", + source_kind: "manual", + has_blockers: false, + has_question: false, + progress_hash: "progress_hash_unrelated_result_report", + }) + await expect(server.command("runtime.preview_opencode_result_report", { sessionId, kind: "completion_report", summary: "mismatch", outcome: "done", progressId: "progress_unrelated_result_report" })).resolves.toMatchObject({ status: "blocked" }) + + await server.eventStore.append({ + kind: "opencode_session_launch_failed", + launch_id: launched.launch_id, + status: "launch_failed", + session_id: sessionId, + adapter_kind: "fake", + launch_mode: "fresh", + completed_at: "2026-07-06T11:00:00.000Z", + launch_hash: "failed_result_report_launch_hash", + }) + await expect(server.command("runtime.preview_opencode_result_report", { sessionId, kind: "status_report", summary: "after failed launch" })).resolves.toMatchObject({ + status: "blocked", + can_record: false, + blockers: ["OpenCode result report requires launch_started or launched status; current status is launch_failed"], + }) + + const eventKinds = (await server.eventStore.readAll()).map((event) => event.kind) + expect(eventKinds).not.toContain("mission_progress_recorded") + expect(eventKinds).not.toContain("commander_result_review_created") + expect(eventKinds).not.toContain("research_result_ingested") + expect(eventKinds).not.toContain("runtime_checkpoint_created") + expect(eventKinds).not.toContain("opencode_prompt_sent") + expect(eventKinds).not.toContain("opencode_session_process_paused") + const authority = await server.command("runtime.command_authority_get", { command: "/opencode-result-report" }) as { risk: string; mutates_events: boolean; notes: string[] } + expect(authority).toMatchObject({ risk: "medium_risk_write", mutates_events: true }) + expect(authority.notes.join(" ")).toContain("result-report metadata") + await server.shutdown() + }) + test("wake supervisor summary counts all matching launched sessions before applying card limit", async () => { const launches = Array.from({ length: 101 }, (_, offset) => { const index = offset + 1 diff --git a/agentcore/runtime/src/server.ts b/agentcore/runtime/src/server.ts index 8c284742..be0e9564 100644 --- a/agentcore/runtime/src/server.ts +++ b/agentcore/runtime/src/server.ts @@ -100,6 +100,8 @@ import { OpenCodeWakeSupervisorExecutionService, readOpenCodeWakeSupervisorBatch import type { OpenCodeWakeSupervisorBatchPreview, OpenCodeWakeSupervisorBatchResult, OpenCodeWakeSupervisorExecutionPreview, OpenCodeWakeSupervisorExecutionRecord, OpenCodeWakeSupervisorExecutionResult, OpenCodeWakeSupervisorExecutionSummary } from "./opencode-session/opencode-wake-supervisor-execution-types" import { OpenCodeWakeActionExecutionService, readOpenCodeWakeActionExecutionPreviewInput, readOpenCodeWakeActionExecutionRecordInput } from "./opencode-session/opencode-wake-action-execution-service" import type { OpenCodeWakeActionExecutionPreview, OpenCodeWakeActionExecutionRecord, OpenCodeWakeActionExecutionResult, OpenCodeWakeActionExecutionSummary } from "./opencode-session/opencode-wake-action-execution-types" +import { OpenCodeResultReportService, readOpenCodeResultReportPreviewInput, readOpenCodeResultReportRecordInput } from "./opencode-session/opencode-result-report-service" +import type { OpenCodeResultReportPreview, OpenCodeResultReportRecord, OpenCodeResultReportResult, OpenCodeResultReportSummary } from "./opencode-session/opencode-result-report-types" import { ContextBudgetService, readContextBudgetPreviewInput, readModelCapabilityGetInput, readModelCapabilityListInput } from "./context/context-budget-service" import type { ContextBudgetPreview, ContextBudgetSummary } from "./context/context-budget-types" import { ModelCapabilityRegistry } from "./context/model-capability-registry" @@ -346,6 +348,7 @@ export class RuntimeServer { private opencodeWakeSupervisorServiceInstance: OpenCodeWakeSupervisorService | null = null private opencodeWakeSupervisorExecutionServiceInstance: OpenCodeWakeSupervisorExecutionService | null = null private opencodeWakeActionExecutionServiceInstance: OpenCodeWakeActionExecutionService | null = null + private opencodeResultReportServiceInstance: OpenCodeResultReportService | null = null private contextBudgetServiceInstance: ContextBudgetService | null = null private contextPacketCompilerServiceInstance: ContextPacketCompilerService | null = null private researchMemoryServiceInstance: ResearchMemoryService | null = null @@ -1121,6 +1124,28 @@ export class RuntimeServer { }) case "runtime.opencode_wake_action_execution_summary": return this.openCodeWakeActionExecutionSummary({ limit: optionalPositiveInteger(payload.limit, "limit", 100) }) + case "runtime.preview_opencode_result_report": + return this.previewOpenCodeResultReport(readOpenCodeResultReportPreviewInput(payload)) + case "runtime.record_opencode_result_report": + return this.recordOpenCodeResultReport(readOpenCodeResultReportRecordInput(payload)) + case "runtime.list_opencode_result_reports": + return this.listOpenCodeResultReports({ + limit: optionalPositiveInteger(payload.limit, "limit", 100), + session_id: optionalString(payload.sessionId ?? payload.session_id ?? payload.session, "sessionId"), + launch_id: optionalString(payload.launchId ?? payload.launch_id ?? payload.launch, "launchId"), + result_kind: optionalString(payload.resultKind ?? payload.result_kind ?? payload.kind, "resultKind"), + result_disposition: optionalString(payload.resultDisposition ?? payload.result_disposition ?? payload.disposition, "resultDisposition"), + review_state: optionalString(payload.reviewState ?? payload.review_state, "reviewState"), + }) + case "runtime.get_opencode_result_report": + return this.getOpenCodeResultReport(requiredString(payload.reportId ?? payload.report_id, "reportId")) + case "runtime.latest_opencode_result_report": + return this.latestOpenCodeResultReport({ + session_id: optionalString(payload.sessionId ?? payload.session_id ?? payload.session, "sessionId"), + launch_id: optionalString(payload.launchId ?? payload.launch_id ?? payload.launch, "launchId"), + }) + case "runtime.opencode_result_report_summary": + return this.openCodeResultReportSummary({ limit: optionalPositiveInteger(payload.limit, "limit", 100) }) case "runtime.research_memory_summary": return this.researchMemorySummary() case "runtime.preview_research_memory_retrieval": @@ -2255,6 +2280,31 @@ export class RuntimeServer { return this.opencodeWakeActionExecutionService().summary(input) } + async previewOpenCodeResultReport(input: Parameters[0] = {}): Promise { + return this.opencodeResultReportService().preview(input) + } + + async recordOpenCodeResultReport(input: Parameters[0] = {}): Promise { + if (input.dry_run === true) return this.opencodeResultReportService().record(input) + return this.withOpenCodeLaunchWriteLock(() => this.opencodeResultReportService().record(input)) + } + + async listOpenCodeResultReports(input: Parameters[0] = {}): Promise { + return this.opencodeResultReportService().list(input) + } + + async getOpenCodeResultReport(reportId: string): Promise { + return this.opencodeResultReportService().get(reportId) + } + + async latestOpenCodeResultReport(input: Parameters[0] = {}): Promise { + return this.opencodeResultReportService().latest(input) + } + + async openCodeResultReportSummary(input: Parameters[0] = {}): Promise { + return this.opencodeResultReportService().summary(input) + } + researchMemorySummary(): ResearchMemorySummary { return this.researchMemoryService().summary() } @@ -3464,6 +3514,22 @@ export class RuntimeServer { return this.opencodeWakeActionExecutionServiceInstance } + private opencodeResultReportService(): OpenCodeResultReportService { + this.opencodeResultReportServiceInstance ??= new OpenCodeResultReportService({ + eventStore: this.eventStore, + opencodeSessionService: this.opencodeSessionService(), + launchGateService: this.opencodeLaunchGateService(), + progressService: this.opencodeProgressService(), + watchdogService: this.opencodeTimeoutWatchdogService(), + questionService: this.opencodeCommanderQuestionService(), + guidanceService: this.commanderGuidanceService(), + guidanceDeliveryService: this.commanderGuidanceDeliveryService(), + wakeExecutionService: this.opencodeWakeSupervisorExecutionService(), + wakeActionExecutionService: this.opencodeWakeActionExecutionService(), + }) + return this.opencodeResultReportServiceInstance + } + private createOpenCodeLaunchAdapter(): OpenCodeLaunchAdapter { if (this.openCodeAdapterConfig?.kind === "process") { return new ProcessOpenCodeLaunchAdapter({ diff --git a/agentcore/runtime/src/tui/runtime-client.ts b/agentcore/runtime/src/tui/runtime-client.ts index 64c8a56c..6ccb250a 100644 --- a/agentcore/runtime/src/tui/runtime-client.ts +++ b/agentcore/runtime/src/tui/runtime-client.ts @@ -41,6 +41,7 @@ import type { OpenCodeHumanControlPreview, OpenCodeHumanControlPreviewInput, Ope import type { OpenCodeWakeSupervisorPreview, OpenCodeWakeSupervisorPreviewInput, OpenCodeWakeSupervisorSummary, OpenCodeWakeSupervisorSummaryInput } from "../opencode-session/opencode-wake-supervisor-types" import type { OpenCodeWakeSupervisorBatchPreview, OpenCodeWakeSupervisorBatchPreviewInput, OpenCodeWakeSupervisorBatchRecordInput, OpenCodeWakeSupervisorBatchResult, OpenCodeWakeSupervisorExecutionPreview, OpenCodeWakeSupervisorExecutionPreviewInput, OpenCodeWakeSupervisorExecutionRecord, OpenCodeWakeSupervisorExecutionRecordInput, OpenCodeWakeSupervisorExecutionResult, OpenCodeWakeSupervisorExecutionSummary } from "../opencode-session/opencode-wake-supervisor-execution-types" import type { OpenCodeWakeActionExecutionPreview, OpenCodeWakeActionExecutionPreviewInput, OpenCodeWakeActionExecutionRecord, OpenCodeWakeActionExecutionRecordInput, OpenCodeWakeActionExecutionResult, OpenCodeWakeActionExecutionSummary } from "../opencode-session/opencode-wake-action-execution-types" +import type { OpenCodeResultReportPreview, OpenCodeResultReportPreviewInput, OpenCodeResultReportRecord, OpenCodeResultReportRecordInput, OpenCodeResultReportResult, OpenCodeResultReportSummary } from "../opencode-session/opencode-result-report-types" import type { ContextBudgetPreview, ContextBudgetPreviewInput, ContextBudgetSummary } from "../context/context-budget-types" import type { ModelCapability } from "../context/model-capability-types" import type { ContextPacketPreview, ContextPacketPreviewInput, ContextPacketSummary } from "../context/context-packet-types" @@ -418,6 +419,12 @@ export interface RuntimeClient { command(name: "runtime.get_opencode_wake_action_execution", payload: { action_execution_id: string } | { actionExecutionId: string }): Promise command(name: "runtime.latest_opencode_wake_action_execution", payload?: { execution_id?: string; executionId?: string; execution?: string; session_id?: string; sessionId?: string; session?: string; launch_id?: string; launchId?: string; launch?: string }): Promise command(name: "runtime.opencode_wake_action_execution_summary", payload?: { limit?: number }): Promise + command(name: "runtime.preview_opencode_result_report", payload?: OpenCodeResultReportPreviewInput | { sessionId?: string; session_id?: string; session?: string; launchId?: string; launch_id?: string; launch?: string; resultKind?: string; result_kind?: string; kind?: string; summary?: string; outcome?: string; changedFiles?: string[]; changed_files?: string[]; testsRun?: string[]; tests_run?: string[]; testResults?: string[]; test_results?: string[]; artifacts?: string[]; metrics?: string[]; claims?: string[]; knownFailures?: string[]; known_failures?: string[]; followups?: string[]; confidence?: number | string; progressId?: string; progress_id?: string; progress?: string; watchdogId?: string; watchdog_id?: string; watchdog?: string; questionId?: string; question_id?: string; question?: string; guidanceId?: string; guidance_id?: string; guidance?: string; deliveryId?: string; delivery_id?: string; delivery?: string; wakeExecutionId?: string; wake_execution_id?: string; wakeExecution?: string; wake_execution?: string; wakeActionExecutionId?: string; wake_action_execution_id?: string; wakeAction?: string; wake_action?: string; reviewState?: string; review_state?: string }): Promise + command(name: "runtime.record_opencode_result_report", payload?: OpenCodeResultReportRecordInput | { sessionId?: string; session_id?: string; session?: string; launchId?: string; launch_id?: string; launch?: string; resultKind?: string; result_kind?: string; kind?: string; summary?: string; outcome?: string; changedFiles?: string[]; changed_files?: string[]; testsRun?: string[]; tests_run?: string[]; testResults?: string[]; test_results?: string[]; artifacts?: string[]; metrics?: string[]; claims?: string[]; knownFailures?: string[]; known_failures?: string[]; followups?: string[]; confidence?: number | string; progressId?: string; progress_id?: string; progress?: string; watchdogId?: string; watchdog_id?: string; watchdog?: string; questionId?: string; question_id?: string; question?: string; guidanceId?: string; guidance_id?: string; guidance?: string; deliveryId?: string; delivery_id?: string; delivery?: string; wakeExecutionId?: string; wake_execution_id?: string; wakeExecution?: string; wake_execution?: string; wakeActionExecutionId?: string; wake_action_execution_id?: string; wakeAction?: string; wake_action?: string; reviewState?: string; review_state?: string; dryRun?: boolean; dry_run?: boolean; recordedBy?: string; recorded_by?: string }): Promise + command(name: "runtime.list_opencode_result_reports", payload?: { limit?: number; session_id?: string; sessionId?: string; session?: string; launch_id?: string; launchId?: string; launch?: string; result_kind?: string; resultKind?: string; kind?: string; result_disposition?: string; resultDisposition?: string; disposition?: string; review_state?: string; reviewState?: string }): Promise + command(name: "runtime.get_opencode_result_report", payload: { report_id: string } | { reportId: string }): Promise + command(name: "runtime.latest_opencode_result_report", payload?: { session_id?: string; sessionId?: string; session?: string; launch_id?: string; launchId?: string; launch?: string }): Promise + command(name: "runtime.opencode_result_report_summary", payload?: { limit?: number }): Promise command(name: "runtime.research_memory_summary", payload?: Record): Promise command(name: "runtime.preview_research_memory_retrieval", payload?: ResearchMemoryRetrievalInput | { query?: string; labels?: string[]; limit?: number; sourceKind?: string; source_kind?: string; missionId?: string; mission_id?: string; sessionId?: string; session_id?: string; includeFailures?: boolean; include_failures?: boolean; includeArtifacts?: boolean; include_artifacts?: boolean }): Promise command(name: "runtime.preview_research_novelty_check", payload?: ResearchNoveltyInput | { question?: string; method?: string; config?: string; labels?: string[]; limit?: number; missionId?: string; mission_id?: string; sessionId?: string; session_id?: string; repetitionReason?: string; repetition_reason?: string; reason?: string; includeFailures?: boolean; include_failures?: boolean }): Promise diff --git a/agentcore/runtime/src/tui/runtime-server-client.ts b/agentcore/runtime/src/tui/runtime-server-client.ts index 61b057ee..5d5f86d0 100644 --- a/agentcore/runtime/src/tui/runtime-server-client.ts +++ b/agentcore/runtime/src/tui/runtime-server-client.ts @@ -91,6 +91,12 @@ const noStartCommands = new Set([ "runtime.get_opencode_wake_action_execution", "runtime.latest_opencode_wake_action_execution", "runtime.opencode_wake_action_execution_summary", + "runtime.preview_opencode_result_report", + "runtime.record_opencode_result_report", + "runtime.list_opencode_result_reports", + "runtime.get_opencode_result_report", + "runtime.latest_opencode_result_report", + "runtime.opencode_result_report_summary", "runtime.research_memory_summary", "runtime.preview_research_memory_retrieval", "runtime.preview_research_novelty_check", diff --git a/agentcore/tui/src/keyboard.ts b/agentcore/tui/src/keyboard.ts index 53205fdf..92a96f9e 100644 --- a/agentcore/tui/src/keyboard.ts +++ b/agentcore/tui/src/keyboard.ts @@ -483,6 +483,19 @@ const runtimeCommands = new Set([ "opencode-wake-action-show", "opencode-wake-action-summary", "wake-action-summary", + "opencode-result-report-preview", + "result-report-preview", + "opencode-result-report-dry-run", + "result-report-dry-run", + "opencode-result-report", + "result-report", + "opencode-result-reports", + "result-reports", + "opencode-result-report-latest", + "result-report-latest", + "opencode-result-report-show", + "opencode-result-report-summary", + "result-report-summary", "research-memory-summary", "research-memory-search", "research-memory-preview", diff --git a/agentcore/tui/src/operator-actions.ts b/agentcore/tui/src/operator-actions.ts index 6c9b92bb..dd29ede3 100644 --- a/agentcore/tui/src/operator-actions.ts +++ b/agentcore/tui/src/operator-actions.ts @@ -129,6 +129,8 @@ const WRITE_COMMANDS = new Set([ "wake-batch-record", "opencode-wake-action-record", "wake-action-record", + "opencode-result-report", + "result-report", ]) const EXECUTION_COMMAND_FIELD = "__nxl_operator_execution_command" diff --git a/agentcore/tui/src/runtime-effects.ts b/agentcore/tui/src/runtime-effects.ts index d637c71e..0e17fd2a 100644 --- a/agentcore/tui/src/runtime-effects.ts +++ b/agentcore/tui/src/runtime-effects.ts @@ -107,6 +107,12 @@ import type { OpenCodeProgressResultSummary, OpenCodeProgressState, OpenCodeProgressSummaryState, + OpenCodeResultReportCommandSummary, + OpenCodeResultReportPreviewSummary, + OpenCodeResultReportRecordSummary, + OpenCodeResultReportResultSummary, + OpenCodeResultReportState, + OpenCodeResultReportSummaryState, OpenCodeForcedReportRequestSummary, OpenCodeCommanderQuestionCommandSummary, OpenCodeCommanderQuestionPreviewSummary, @@ -528,6 +534,12 @@ export type RuntimeUiEffect = | { type: "load-opencode-wake-action-execution"; actionExecutionId: string } | { type: "load-latest-opencode-wake-action-execution"; executionId?: string; sessionId?: string; launchId?: string } | { type: "load-opencode-wake-action-execution-summary"; limit?: number } + | { type: "preview-opencode-result-report"; sessionId?: string; launchId?: string; resultKind?: string; summary?: string; outcome?: string; changedFiles?: string[]; testsRun?: string[]; testResults?: string[]; artifacts?: string[]; metrics?: string[]; claims?: string[]; knownFailures?: string[]; followups?: string[]; confidence?: string; progressId?: string; watchdogId?: string; questionId?: string; guidanceId?: string; deliveryId?: string; wakeExecutionId?: string; wakeActionExecutionId?: string; reviewState?: string } + | { type: "record-opencode-result-report"; sessionId?: string; launchId?: string; resultKind?: string; summary?: string; outcome?: string; changedFiles?: string[]; testsRun?: string[]; testResults?: string[]; artifacts?: string[]; metrics?: string[]; claims?: string[]; knownFailures?: string[]; followups?: string[]; confidence?: string; progressId?: string; watchdogId?: string; questionId?: string; guidanceId?: string; deliveryId?: string; wakeExecutionId?: string; wakeActionExecutionId?: string; reviewState?: string; dryRun?: boolean } + | { type: "load-opencode-result-reports"; sessionId?: string; launchId?: string; resultKind?: string; resultDisposition?: string; reviewState?: string; limit?: number } + | { type: "load-opencode-result-report"; reportId: string } + | { type: "load-latest-opencode-result-report"; sessionId?: string; launchId?: string } + | { type: "load-opencode-result-report-summary"; limit?: number } | { type: "load-research-memory-summary" } | { type: "preview-research-memory-retrieval"; query?: string; labels?: string[]; limit?: number; sourceKind?: string; missionId?: string; sessionId?: string; includeFailures?: boolean; includeArtifacts?: boolean } | { type: "preview-research-novelty-check"; question?: string; method?: string; config?: string; labels?: string[]; limit?: number; missionId?: string; sessionId?: string; repetitionReason?: string; includeFailures?: boolean } @@ -1529,6 +1541,22 @@ export async function applyRuntimeUiEffect( return applyOpenCodeWakeActionExecutionLatest(state, await runtime.command("runtime.latest_opencode_wake_action_execution", { executionId: effect.executionId, sessionId: effect.sessionId, launchId: effect.launchId }), effect.executionId ?? effect.sessionId ?? effect.launchId ?? "latest") case "load-opencode-wake-action-execution-summary": return applyOpenCodeWakeActionExecutionSummary(state, await runtime.command("runtime.opencode_wake_action_execution_summary", { limit: effect.limit ?? HANDOFF_LIMIT })) + case "preview-opencode-result-report": + return applyOpenCodeResultReportPreview(state, await runtime.command("runtime.preview_opencode_result_report", opencodeResultReportPayload(effect))) + case "record-opencode-result-report": { + const next = applyOpenCodeResultReportResult(state, await runtime.command("runtime.record_opencode_result_report", { ...opencodeResultReportPayload(effect), dryRun: effect.dryRun === true, recordedBy: "operator" })) + if (next.opencodeResultReports?.commandError || effect.dryRun === true) return next + const result = next.opencodeResultReports?.latestResult + return applyOpenCodeResultReportRecords(next, await runtime.command("runtime.list_opencode_result_reports", { limit: HANDOFF_LIMIT, sessionId: result?.session_id ?? effect.sessionId, launchId: result?.launch_id ?? effect.launchId })) + } + case "load-opencode-result-reports": + return applyOpenCodeResultReportRecords(state, await runtime.command("runtime.list_opencode_result_reports", { limit: effect.limit ?? HANDOFF_LIMIT, sessionId: effect.sessionId, launchId: effect.launchId, resultKind: effect.resultKind, resultDisposition: effect.resultDisposition, reviewState: effect.reviewState })) + case "load-opencode-result-report": + return applyOpenCodeResultReportSelected(state, await runtime.command("runtime.get_opencode_result_report", { reportId: effect.reportId }), effect.reportId) + case "load-latest-opencode-result-report": + return applyOpenCodeResultReportLatest(state, await runtime.command("runtime.latest_opencode_result_report", { sessionId: effect.sessionId, launchId: effect.launchId }), effect.sessionId ?? effect.launchId ?? "latest") + case "load-opencode-result-report-summary": + return applyOpenCodeResultReportSummary(state, await runtime.command("runtime.opencode_result_report_summary", { limit: effect.limit ?? HANDOFF_LIMIT })) case "load-research-memory-summary": return applyResearchMemorySummary(state, await runtime.command("runtime.research_memory_summary")) case "preview-research-memory-retrieval": @@ -4216,6 +4244,89 @@ function applyOpenCodeWakeActionExecutionSummary(state: UiState, value: unknown) } } +function applyOpenCodeResultReportPreview(state: UiState, value: unknown): UiState { + const preview = readOpenCodeResultReportPreview(value) + const commandError = preview.status === "blocked" ? preview.blockers[0] ?? "OpenCode result report preview is blocked" : undefined + return { + ...state, + opencodeResultReports: { + ...opencodeResultReportState(state), + preview, + commandError, + }, + systemActions: [...state.systemActions, { title: "opencode result report preview", detail: `session=${preview.session_id || "missing"} kind=${preview.result_kind} review=${preview.review_state}`, status: preview.status }].slice(-12), + } +} + +function applyOpenCodeResultReportResult(state: UiState, value: unknown): UiState { + const result = readOpenCodeResultReportResult(value) + const commandError = result.status === "blocked" || result.status === "failed" ? result.error ?? "OpenCode result report is blocked" : undefined + return { + ...state, + opencodeResultReports: { + ...opencodeResultReportState(state), + latestResult: result, + latest: result.status === "recorded" ? result : opencodeResultReportState(state).latest, + commandError, + }, + systemActions: [...state.systemActions, { title: "opencode result report", detail: `report=${result.report_id} status=${result.status} kind=${result.result_kind}`, status: result.status }].slice(-12), + } +} + +function applyOpenCodeResultReportRecords(state: UiState, value: unknown): UiState { + const records = readOpenCodeResultReportRecords(value) + return { + ...state, + opencodeResultReports: { + ...opencodeResultReportState(state), + records, + commandError: undefined, + }, + systemActions: [...state.systemActions, { title: "opencode result reports", detail: `records=${records.length}`, status: "loaded" }].slice(-12), + } +} + +function applyOpenCodeResultReportSelected(state: UiState, value: unknown, reportId: string): UiState { + const selected = value ? readOpenCodeResultReportResult(value) : null + const commandError = selected ? undefined : `OpenCode result report not found: ${reportId}` + return { + ...state, + opencodeResultReports: { + ...opencodeResultReportState(state), + selected, + commandError, + }, + systemActions: [...state.systemActions, { title: "opencode result report show", detail: selected ? `report=${selected.report_id}` : `missing=${reportId}`, status: selected ? "loaded" : "blocked" }].slice(-12), + } +} + +function applyOpenCodeResultReportLatest(state: UiState, value: unknown, target: string): UiState { + const latest = value ? readOpenCodeResultReportResult(value) : null + const commandError = latest ? undefined : `OpenCode result report latest not found: ${target}` + return { + ...state, + opencodeResultReports: { + ...opencodeResultReportState(state), + latest, + commandError, + }, + systemActions: [...state.systemActions, { title: "opencode result report latest", detail: latest ? `report=${latest.report_id}` : `missing=${target}`, status: latest ? "loaded" : "blocked" }].slice(-12), + } +} + +function applyOpenCodeResultReportSummary(state: UiState, value: unknown): UiState { + const summary = readOpenCodeResultReportSummary(value) + return { + ...state, + opencodeResultReports: { + ...opencodeResultReportState(state), + summary, + commandError: undefined, + }, + systemActions: [...state.systemActions, { title: "opencode result report summary", detail: `reports=${summary.total_reports} review=${summary.needs_commander_review_count}`, status: "loaded" }].slice(-12), + } +} + function applyResearchMemorySummary(state: UiState, value: unknown): UiState { const summary = readResearchMemorySummary(value) return { @@ -5480,6 +5591,7 @@ function commandErrorFor(command: string, state: UiState): string | undefined { if (opencodeWakeSupervisorCommands.has(command)) return state.opencodeWakeSupervisor?.commandError if (opencodeWakeSupervisorExecutionCommands.has(command)) return state.opencodeWakeSupervisorExecutions?.commandError if (opencodeWakeActionExecutionCommands.has(command)) return state.opencodeWakeActions?.commandError + if (opencodeResultReportCommands.has(command)) return state.opencodeResultReports?.commandError if (researchMemoryCommands.has(command)) return state.researchMemory?.commandError if (commanderExecutorReviewCommands.has(command)) return state.commanderExecutorReview?.commandError if (executorReviewProposalDraftCommands.has(command)) return state.executorReviewProposalDrafts?.commandError @@ -5534,6 +5646,7 @@ function clearCommandErrorFor(command: string, state: UiState): UiState { if (opencodeWakeSupervisorCommands.has(command)) return { ...state, opencodeWakeSupervisor: { ...opencodeWakeSupervisorState(state), commandError: undefined } } if (opencodeWakeSupervisorExecutionCommands.has(command)) return { ...state, opencodeWakeSupervisorExecutions: { ...opencodeWakeSupervisorExecutionState(state), commandError: undefined } } if (opencodeWakeActionExecutionCommands.has(command)) return { ...state, opencodeWakeActions: { ...opencodeWakeActionExecutionState(state), commandError: undefined } } + if (opencodeResultReportCommands.has(command)) return { ...state, opencodeResultReports: { ...opencodeResultReportState(state), commandError: undefined } } if (researchMemoryCommands.has(command)) return { ...state, researchMemory: { ...researchMemoryState(state), commandError: undefined } } if (commanderExecutorReviewCommands.has(command)) return { ...state, commanderExecutorReview: { ...commanderExecutorReviewState(state), commandError: undefined } } if (executorReviewProposalDraftCommands.has(command)) return { ...state, executorReviewProposalDrafts: { ...executorReviewProposalDraftState(state), commandError: undefined } } @@ -5981,6 +6094,28 @@ function applyNamedRuntimeCommand(state: UiState, runtime: RuntimeClient, comman case "opencode-wake-action-summary": case "wake-action-summary": return applyRuntimeUiEffect(commandState, runtime, { type: "load-opencode-wake-action-execution-summary", limit: HANDOFF_LIMIT }) + case "opencode-result-report-preview": + case "result-report-preview": + return applyRuntimeUiEffect(commandState, runtime, opencodeResultReportEffect("preview-opencode-result-report", args, false)) + case "opencode-result-report-dry-run": + case "result-report-dry-run": { + const effect = opencodeResultReportEffect("record-opencode-result-report", args, true) as Extract + return applyRuntimeUiEffect(commandState, runtime, { ...effect, dryRun: true }) + } + case "opencode-result-report": + case "result-report": + return applyRuntimeUiEffect(commandState, runtime, opencodeResultReportEffect("record-opencode-result-report", args, true)) + case "opencode-result-reports": + case "result-reports": + return applyRuntimeUiEffect(commandState, runtime, opencodeResultReportListEffect(args)) + case "opencode-result-report-latest": + case "result-report-latest": + return applyRuntimeUiEffect(commandState, runtime, opencodeResultReportLatestEffect(args)) + case "opencode-result-report-show": + return applyRuntimeUiEffect(commandState, runtime, { type: "load-opencode-result-report", reportId: requiredArg(args, 0, "reportId") }) + case "opencode-result-report-summary": + case "result-report-summary": + return applyRuntimeUiEffect(commandState, runtime, { type: "load-opencode-result-report-summary", limit: HANDOFF_LIMIT }) case "research-memory-summary": return applyRuntimeUiEffect(commandState, runtime, { type: "load-research-memory-summary" }) case "research-memory-search": @@ -7385,6 +7520,22 @@ const opencodeWakeActionExecutionCommands = new Set([ "wake-action-summary", ]) +const opencodeResultReportCommands = new Set([ + "opencode-result-report-preview", + "result-report-preview", + "opencode-result-report-dry-run", + "result-report-dry-run", + "opencode-result-report", + "result-report", + "opencode-result-reports", + "result-reports", + "opencode-result-report-latest", + "result-report-latest", + "opencode-result-report-show", + "opencode-result-report-summary", + "result-report-summary", +]) + const researchMemoryCommands = new Set([ "research-memory-summary", "research-memory-search", @@ -11381,6 +11532,142 @@ function readOpenCodeWakeActionExecutionCommands(value: unknown): OpenCodeWakeAc })) } +function readOpenCodeResultReportPreview(value: unknown): OpenCodeResultReportPreviewSummary { + if (!isRecord(value) || typeof value.preview_id !== "string") throw new Error("runtime.preview_opencode_result_report returned invalid preview") + return { + preview_id: redactText(value.preview_id), + status: readString(value.status, "blocked"), + can_record: value.can_record === true, + session_id: redactText(readString(value.session_id, "")), + launch_id: typeof value.launch_id === "string" ? redactText(value.launch_id) : undefined, + result_kind: readString(value.result_kind, "unknown"), + result_disposition: readString(value.result_disposition, "reported_status_only"), + review_state: readString(value.review_state, "unknown"), + summary_preview: preview(readString(value.summary_preview, "")), + outcome_preview: typeof value.outcome_preview === "string" ? preview(readString(value.outcome_preview, "")) : undefined, + changed_files_preview: readStringList(value.changed_files_preview, 12).map(preview), + tests_run_preview: readStringList(value.tests_run_preview, 12).map(preview), + test_results_preview: readStringList(value.test_results_preview, 12).map(preview), + artifacts_preview: readStringList(value.artifacts_preview, 12).map(preview), + metrics_preview: readStringList(value.metrics_preview, 12).map(preview), + claims_preview: readStringList(value.claims_preview, 12).map(preview), + known_failures_preview: readStringList(value.known_failures_preview, 12).map(preview), + followups_preview: readStringList(value.followups_preview, 12).map(preview), + confidence: readPrimitiveConfidence(value.confidence), + linked_progress_id: typeof value.linked_progress_id === "string" ? redactText(value.linked_progress_id) : undefined, + linked_watchdog_id: typeof value.linked_watchdog_id === "string" ? redactText(value.linked_watchdog_id) : undefined, + linked_question_id: typeof value.linked_question_id === "string" ? redactText(value.linked_question_id) : undefined, + linked_guidance_id: typeof value.linked_guidance_id === "string" ? redactText(value.linked_guidance_id) : undefined, + linked_delivery_id: typeof value.linked_delivery_id === "string" ? redactText(value.linked_delivery_id) : undefined, + linked_wake_execution_id: typeof value.linked_wake_execution_id === "string" ? redactText(value.linked_wake_execution_id) : undefined, + linked_wake_action_execution_id: typeof value.linked_wake_action_execution_id === "string" ? redactText(value.linked_wake_action_execution_id) : undefined, + mission_mutated: value.mission_mutated === true, + research_db_written: value.research_db_written === true, + checkpoint_created: value.checkpoint_created === true, + commander_review_created: value.commander_review_created === true, + blockers: readStringList(value.blockers, 12).map(preview), + warnings: readStringList(value.warnings, 14).map(preview), + recommended_commands: readOpenCodeResultReportCommands(value.recommended_commands), + generated_at: readString(value.generated_at, ""), + redacted_summary_preview: preview(readString(value.redacted_summary_preview, "")), + report_hash: readString(value.report_hash, ""), + } +} + +function readOpenCodeResultReportResult(value: unknown): OpenCodeResultReportResultSummary { + if (!isRecord(value) || typeof value.report_id !== "string") throw new Error("runtime.record_opencode_result_report returned invalid result") + return { + report_id: redactText(value.report_id), + status: readString(value.status, "blocked"), + session_id: redactText(readString(value.session_id, "")), + launch_id: typeof value.launch_id === "string" ? redactText(value.launch_id) : undefined, + result_kind: readString(value.result_kind, "unknown"), + result_disposition: readString(value.result_disposition, "reported_status_only"), + review_state: readString(value.review_state, "unknown"), + summary_preview: preview(readString(value.summary_preview, "")), + outcome_preview: typeof value.outcome_preview === "string" ? preview(readString(value.outcome_preview, "")) : undefined, + changed_files_preview: readStringList(value.changed_files_preview, 12).map(preview), + tests_run_preview: readStringList(value.tests_run_preview, 12).map(preview), + test_results_preview: readStringList(value.test_results_preview, 12).map(preview), + artifacts_preview: readStringList(value.artifacts_preview, 12).map(preview), + metrics_preview: readStringList(value.metrics_preview, 12).map(preview), + claims_preview: readStringList(value.claims_preview, 12).map(preview), + known_failures_preview: readStringList(value.known_failures_preview, 12).map(preview), + followups_preview: readStringList(value.followups_preview, 12).map(preview), + confidence: readPrimitiveConfidence(value.confidence), + linked_progress_id: typeof value.linked_progress_id === "string" ? redactText(value.linked_progress_id) : undefined, + linked_watchdog_id: typeof value.linked_watchdog_id === "string" ? redactText(value.linked_watchdog_id) : undefined, + linked_question_id: typeof value.linked_question_id === "string" ? redactText(value.linked_question_id) : undefined, + linked_guidance_id: typeof value.linked_guidance_id === "string" ? redactText(value.linked_guidance_id) : undefined, + linked_delivery_id: typeof value.linked_delivery_id === "string" ? redactText(value.linked_delivery_id) : undefined, + linked_wake_execution_id: typeof value.linked_wake_execution_id === "string" ? redactText(value.linked_wake_execution_id) : undefined, + linked_wake_action_execution_id: typeof value.linked_wake_action_execution_id === "string" ? redactText(value.linked_wake_action_execution_id) : undefined, + mission_mutated: value.mission_mutated === true, + research_db_written: value.research_db_written === true, + checkpoint_created: value.checkpoint_created === true, + commander_review_created: value.commander_review_created === true, + recorded_at: readString(value.recorded_at, ""), + recorded_by: preview(readString(value.recorded_by, "")), + error: typeof value.error === "string" ? preview(readString(value.error, "")) : undefined, + report_hash: readString(value.report_hash, ""), + recommended_commands: readOpenCodeResultReportCommands(value.recommended_commands), + } +} + +function readOpenCodeResultReportRecords(value: unknown): OpenCodeResultReportRecordSummary[] { + if (!Array.isArray(value)) return [] + return value.filter(isRecord).slice(0, 20).map((record) => ({ + report_id: redactText(readString(record.report_id, "")), + session_id: redactText(readString(record.session_id, "")), + launch_id: typeof record.launch_id === "string" ? redactText(record.launch_id) : undefined, + result_kind: readString(record.result_kind, "unknown"), + result_disposition: readString(record.result_disposition, "reported_status_only"), + review_state: readString(record.review_state, "unknown"), + summary_preview: preview(readString(record.summary_preview, "")), + recorded_at: readString(record.recorded_at, ""), + recorded_by: preview(readString(record.recorded_by, "")), + confidence: readPrimitiveConfidence(record.confidence), + has_failures: record.has_failures === true, + has_artifacts: record.has_artifacts === true, + has_metrics: record.has_metrics === true, + report_hash: readString(record.report_hash, ""), + })) +} + +function readOpenCodeResultReportSummary(value: unknown): OpenCodeResultReportSummaryState { + if (!isRecord(value)) throw new Error("runtime.opencode_result_report_summary returned invalid summary") + return { + total_reports: readNumber(value.total_reports, 0), + session_count: readNumber(value.session_count, 0), + completion_count: readNumber(value.completion_count, 0), + partial_count: readNumber(value.partial_count, 0), + failure_count: readNumber(value.failure_count, 0), + inconclusive_count: readNumber(value.inconclusive_count, 0), + blocked_count: readNumber(value.blocked_count, 0), + needs_commander_review_count: readNumber(value.needs_commander_review_count, 0), + needs_human_review_count: readNumber(value.needs_human_review_count, 0), + latest_reports: readOpenCodeResultReportRecords(value.latest_reports), + generated_at: readString(value.generated_at, ""), + } +} + +function readOpenCodeResultReportCommands(value: unknown): OpenCodeResultReportCommandSummary[] { + if (!Array.isArray(value)) return [] + return value.filter(isRecord).slice(0, 12).map((command) => ({ + label: preview(readString(command.label, "")), + command: preview(readString(command.command, "")), + command_type: command.command_type === "write" ? "write" : "read", + requires_active_runtime: command.requires_active_runtime === true, + notes: typeof command.notes === "string" ? preview(readString(command.notes, "")) : undefined, + })) +} + +function readPrimitiveConfidence(value: unknown): number | string | undefined { + if (typeof value === "number" && Number.isFinite(value)) return value + if (typeof value === "string") return preview(readString(value, "")) + return undefined +} + function mergeExecutionRecords(existing: OpenCodeWakeSupervisorExecutionRecordSummary[], incoming: OpenCodeWakeSupervisorExecutionRecordSummary[]): OpenCodeWakeSupervisorExecutionRecordSummary[] { const seen = new Set() const merged: OpenCodeWakeSupervisorExecutionRecordSummary[] = [] @@ -15005,6 +15292,10 @@ function opencodeWakeActionExecutionState(state: UiState): OpenCodeWakeActionExe return state.opencodeWakeActions ?? { preview: null, latestResult: null, records: [], selected: null, latest: null, summary: null } } +function opencodeResultReportState(state: UiState): OpenCodeResultReportState { + return state.opencodeResultReports ?? { preview: null, latestResult: null, records: [], selected: null, latest: null, summary: null } +} + function progressPayload(effect: Extract): Record { return { sessionId: effect.sessionId, @@ -15140,6 +15431,33 @@ function opencodeWakeActionExecutionPayload(effect: Extract): Record { + return { + sessionId: effect.sessionId, + launchId: effect.launchId, + resultKind: effect.resultKind, + summary: effect.summary, + outcome: effect.outcome, + changedFiles: effect.changedFiles, + testsRun: effect.testsRun, + testResults: effect.testResults, + artifacts: effect.artifacts, + metrics: effect.metrics, + claims: effect.claims, + knownFailures: effect.knownFailures, + followups: effect.followups, + confidence: effect.confidence, + progressId: effect.progressId, + watchdogId: effect.watchdogId, + questionId: effect.questionId, + guidanceId: effect.guidanceId, + deliveryId: effect.deliveryId, + wakeExecutionId: effect.wakeExecutionId, + wakeActionExecutionId: effect.wakeActionExecutionId, + reviewState: effect.reviewState, + } +} + function researchMemoryState(state: UiState): ResearchMemoryState { return state.researchMemory ?? { summary: null, retrievalPreview: null, noveltyPreview: null } } @@ -16675,6 +16993,79 @@ function opencodeWakeActionExecutionLatestEffect(args: string[]): Extract { + const effect: Extract = { type } + const knownKeys = new Set(["session", "launch", "kind", "summary", "outcome", "changed_files", "tests_run", "test_results", "artifacts", "metrics", "claims", "known_failures", "followups", "confidence", "progress", "watchdog", "question", "guidance", "delivery", "wake_execution", "wake_action", "review_state", "recorded_by"]) + const freeTextKeys = new Set(["summary", "outcome"]) + for (let index = 0; index < args.length; index += 1) { + const { key, value, nextIndex } = readKeyValueWithFreeText(args, index, knownKeys, freeTextKeys, "OpenCode result report args must use session=, launch=, kind=, summary=, outcome=, changed_files=, tests_run=, test_results=, artifacts=, metrics=, claims=, known_failures=, followups=, confidence=, progress=, watchdog=, question=, guidance=, delivery=, wake_execution=, wake_action=, or review_state=") + index = nextIndex + if (key === "session") effect.sessionId = value + else if (key === "launch") effect.launchId = value + else if (key === "kind") effect.resultKind = value + else if (key === "summary") effect.summary = value + else if (key === "outcome") effect.outcome = value + else if (key === "changed_files") effect.changedFiles = commaList(value) + else if (key === "tests_run") effect.testsRun = commaList(value) + else if (key === "test_results") effect.testResults = commaList(value) + else if (key === "artifacts") effect.artifacts = commaList(value) + else if (key === "metrics") effect.metrics = commaList(value) + else if (key === "claims") effect.claims = commaList(value) + else if (key === "known_failures") effect.knownFailures = commaList(value) + else if (key === "followups") effect.followups = commaList(value) + else if (key === "confidence") effect.confidence = value + else if (key === "progress") effect.progressId = value + else if (key === "watchdog") effect.watchdogId = value + else if (key === "question") effect.questionId = value + else if (key === "guidance") effect.guidanceId = value + else if (key === "delivery") effect.deliveryId = value + else if (key === "wake_execution") effect.wakeExecutionId = value + else if (key === "wake_action") effect.wakeActionExecutionId = value + else if (key === "review_state") effect.reviewState = value + else if (key === "recorded_by") { + // Accepted for forward compatibility; runtime records operator for this branch. + } else throw new Error("OpenCode result report arg is unsupported") + } + if (!effect.sessionId && !effect.launchId && !effect.progressId && !effect.watchdogId && !effect.questionId && !effect.guidanceId && !effect.deliveryId && !effect.wakeExecutionId && !effect.wakeActionExecutionId) throw new Error("OpenCode result report requires session=, launch=, or linked evidence") + if (requireSummary && !effect.summary && !effect.progressId) throw new Error("OpenCode result report requires summary=") + return effect +} + +function opencodeResultReportListEffect(args: string[]): Extract { + const effect: Extract = { type: "load-opencode-result-reports", limit: HANDOFF_LIMIT } + for (const arg of args) { + const [key, ...rest] = arg.split("=") + const value = rest.join("=").trim() + if (!key || !value) throw new Error("OpenCode result reports args must use session=, launch=, kind=, disposition=, review_state=, or limit=") + if (key === "session") effect.sessionId = value + else if (key === "launch") effect.launchId = value + else if (key === "kind") effect.resultKind = value + else if (key === "disposition") effect.resultDisposition = value + else if (key === "review_state") effect.reviewState = value + else if (key === "limit") effect.limit = readPositiveInteger(value, "limit", HANDOFF_LIMIT) + else throw new Error("OpenCode result reports arg is unsupported") + } + return effect +} + +function opencodeResultReportLatestEffect(args: string[]): Extract { + const effect: Extract = { type: "load-latest-opencode-result-report" } + for (const arg of args) { + const [key, ...rest] = arg.split("=") + const value = rest.join("=").trim() + if (!key || !value) throw new Error("OpenCode result report latest args must use session= or launch=") + if (key === "session") effect.sessionId = value + else if (key === "launch") effect.launchId = value + else throw new Error("OpenCode result report latest arg is unsupported") + } + if (!effect.sessionId && !effect.launchId) throw new Error("OpenCode result report latest requires session= or launch=") + return effect +} + function researchMemoryRetrievalEffect(args: string[]): Extract { const effect: Extract = { type: "preview-research-memory-retrieval", limit: HANDOFF_LIMIT } const freeTextKeys = new Set(["query"]) diff --git a/agentcore/tui/src/runtime.ts b/agentcore/tui/src/runtime.ts index bbaececc..35e715b5 100644 --- a/agentcore/tui/src/runtime.ts +++ b/agentcore/tui/src/runtime.ts @@ -4,7 +4,7 @@ import { join } from "path" import type { RuntimeEvent } from "./events" import { redactText, redactUnknown } from "./redaction" import type { CommanderApplyPreviewSummary, CommanderApplyResultSummary, CommanderAuditEventSummary, CommanderAuthorityChainSummary, CommanderCyclePreviewSummary, CommanderCycleRecordSummary, CommanderCycleResultSummary, CommanderExecutorReviewPreviewSummary, CommanderExecutorReviewRecordSummary, CommanderExecutorReviewResultSummary, CommanderPlaybookDraftSummary, CommanderPlaybookSummary, CommanderProposalBundleSummary, CommanderProposalSummary, CommanderQueueItemSummary, CommanderQueueKind, CommanderQueueSummary, CommanderTargetContextSummary, CommanderTargetType, CommanderWorkbenchDraftSummary, CommanderWorkbenchReadinessSummary, CommanderWorkbenchStatusSummary, ContextBudgetAllocationSummary, ContextBudgetPreviewSummary, ContextBudgetProfileSummary, ContextBudgetSummaryState, ContinuationPlanPreviewSummary, ContinuationPlanRecordSummary, ContinuationPlanSummary, ContinuationStepResultSummary, ExecutorClaimSummary, ExecutorReviewProposalApplyReadinessPreviewSummary, ExecutorReviewProposalApplyReadinessRecordSummary, ExecutorReviewProposalApplyReadinessSummary, ExecutorReviewProposalCreatePreviewSummary, ExecutorReviewProposalCreateRecordSummary, ExecutorReviewProposalCreateResultSummary, ExecutorReviewProposalDraftCandidateSummary, ExecutorReviewProposalDraftPreviewSummary, ExecutorReviewProposalDraftSummary, ExecutorReviewProposalNarrowApplyPreviewSummary, ExecutorReviewProposalNarrowApplyRecordSummary, ExecutorReviewProposalNarrowApplyResultSummary, ExecutorReviewProposalReviewDecisionPreviewSummary, ExecutorReviewProposalReviewDecisionRecordSummary, ExecutorReviewProposalReviewDecisionResultSummary, ExecutorReviewProposalReviewRequestPreviewSummary, ExecutorReviewProposalReviewRequestRecordSummary, ExecutorReviewProposalReviewRequestResultSummary, ExternalApiAuditRecordSummary, ExternalApiConnectorSummary, ExternalApiResearchIngestionPreviewSummary, ExternalApiResearchIngestionRecordSummary, ExternalApiResearchIngestionResultSummary, ExternalApiRequestPreviewSummary, ExternalApiRequestResultSummary, MiniMaxLiveValidationPreviewSummary, MiniMaxLiveValidationRecordSummary, MiniMaxLiveValidationResultSummary, MiniMaxLiveValidationSurfaceResultSummary, MissionProgressSummary, MissionRecord, MissionResultSummary, ModelCapabilitySummary, OpenCodeHandoffFollowupCounts, OpenCodeHandoffFollowupQueueKind, OpenCodeHandoffFollowupSummary, OpenCodeHandoffPreviewSummary, OpenCodeHandoffReadinessPreviewSummary, OpenCodeHandoffReadinessSummary, OpenCodeHandoffRecordSummary, OpenCodeHandoffResultSummary, OpenCodeProcessSmokePreviewSummary, OpenCodeProcessSmokeRecordSummary, OpenCodeProcessSmokeResultSummary, OpenCodeResultReviewPacketSummary, OpenCodeResultReviewSummary, OpenCodeSessionPlanSummary, OpenCodeSessionPreviewSummary, OpenCodeSessionRecordSummary, OpenCodeSessionSummary, ProposalBundleReadinessSummary, ResearchSynthesisPreviewSummary, ResearchSynthesisRecordSummary, ResearchSynthesisResultSummary, ReviewRequestSummary, RuntimeCheckpointPreviewSummary, RuntimeCheckpointRecordSummary, RuntimeCheckpointScope, RuntimeCheckpointSummary, RuntimeRestorePreviewSummary, RuntimeResumeAnchorSummary, WakeAssessmentPreviewSummary, WakeAssessmentRecordSummary, WakeAssessmentSummary, WakeSchedulePreviewSummary, WakeScheduleRecordSummary, WakeScheduleSummary, WakeSchedulerAuditChainSummary, WakeSchedulerAuditCommandSummary, WakeSchedulerAuditIncidentSummary, WakeSchedulerAuditSummarySummary, WakeSchedulerAuditTimelineEntrySummary, WakeSchedulerBootstrapStatusSummary, WakeSchedulerEventRecordSummary, WakeSchedulerNavigationBoardSummary, WakeSchedulerNavigationCardSummary, WakeSchedulerNavigationCheckpointApprovalUsageSummaryState, WakeSchedulerNavigationCheckpointWriteGroupSummary, WakeSchedulerNavigationCheckpointWriteHistorySummary, WakeSchedulerNavigationCheckpointWritePairComparisonSummary, WakeSchedulerNavigationCheckpointWriteRunPreviewSummary, WakeSchedulerNavigationCheckpointWriteRunRecordSummary, WakeSchedulerNavigationCheckpointWriteRunResultSummary, WakeSchedulerNavigationCheckpointWriteStaleItemSummary, WakeSchedulerNavigationCommandPreviewSummary, WakeSchedulerNavigationStagePreviewSummary, WakeSchedulerNavigationStagedReadGroupSummary, WakeSchedulerNavigationStagedReadHistorySummary, WakeSchedulerNavigationStagedReadPairComparisonSummary, WakeSchedulerNavigationStagedReadStaleItemSummary, WakeSchedulerNavigationStagedRunPreviewSummary, WakeSchedulerNavigationStagedRunRecordSummary, WakeSchedulerNavigationStagedRunResultSummary, WakeSchedulerNavigationStagedCommandRecordSummary, WakeSchedulerNavigationStagedCommandSummary, WakeSchedulerNavigationStagedWriteCommandRecordSummary, WakeSchedulerNavigationStagedWriteCommandSummary, WakeSchedulerNavigationTargetKindSummary, WakeSchedulerNavigationTargetSummary, WakeSchedulerNavigationWriteApprovalRecordSummary, WakeSchedulerNavigationWriteApprovalSummary, WakeSchedulerNavigationWriteReadinessPreviewSummary, WakeSchedulerNavigationWriteBoardSummary, WakeSchedulerNavigationWritePreviewSummary, WakeSchedulerNavigationWriteRunGroupSummary, WakeSchedulerNavigationWriteRunHistorySummary, WakeSchedulerNavigationWriteRunPairComparisonSummary, WakeSchedulerNavigationWriteRunPreviewSummary, WakeSchedulerNavigationWriteRunRecordSummary, WakeSchedulerNavigationWriteRunResultSummary, WakeSchedulerNavigationWriteRunStaleItemSummary, WakeSchedulerNavigationWriteStagePreviewSummary, WakeSchedulerPreviewSummary, WakeSchedulerRecoveryPreviewSummary, WakeSchedulerRecoveryRecordSummary, WakeSchedulerRecoverySummary, WakeSchedulerRecoveryWorkflowPreviewSummary, WakeSchedulerRecoveryWorkflowRecordSummary, WakeSchedulerRecoveryWorkflowStepSummary, WakeSchedulerRecoveryWorkflowSummary, WakeSchedulerRecoveryWorkflowVerificationSummary, WakeSchedulerStateSummary, WakeScheduleTickPreviewSummary, WakeScheduleTickResultSummary } from "./state" -import type { CommandAuthorityRecordSummary, CommandAuthoritySummaryState, CommandAuthorityValidationProfileSummary, CommanderGuidanceDeliveryPreviewSummary, CommanderGuidanceDeliveryRecordSummary, CommanderGuidanceDeliveryResultSummary, CommanderGuidanceDeliverySummaryState, CommanderGuidancePreviewSummary, CommanderGuidanceRecordSummary, CommanderGuidanceResultSummary, CommanderGuidanceSummaryState, ContextPacketPreviewSummary, ContextPacketSectionSummary, ContextPacketSummaryState, OpenCodeCommanderQuestionPreviewSummary, OpenCodeCommanderQuestionRecordSummary, OpenCodeCommanderQuestionResultSummary, OpenCodeCommanderQuestionSummaryState, OpenCodeForcedReportRequestSummary, OpenCodeHumanControlPreviewSummary, OpenCodeHumanControlRecordSummary, OpenCodeHumanControlResultSummary, OpenCodeHumanControlSummaryState, OpenCodeLaunchPreviewSummary, OpenCodeLaunchReadinessCheckSummary, OpenCodeLaunchReadinessPreviewSummary, OpenCodeLaunchReadinessSummaryState, OpenCodeLaunchRecordSummary, OpenCodeLaunchResultSummary, OpenCodeProgressPreviewSummary, OpenCodeProgressRecordSummary, OpenCodeProgressResultSummary, OpenCodeProgressSummaryState, OpenCodeSessionInstructionPackFilePreviewSummary, OpenCodeSessionInstructionPackPreviewSummary, OpenCodeSessionInstructionPackRecordSummary, OpenCodeSessionInstructionPackResultSummary, OpenCodeWakeActionExecutionCommandSummary, OpenCodeWakeActionExecutionEvidenceRefSummary, OpenCodeWakeActionExecutionPreviewSummary, OpenCodeWakeActionExecutionRecordSummary, OpenCodeWakeActionExecutionResultSummary, OpenCodeWakeActionExecutionSummaryState, OpenCodeWakeSupervisorBatchPreviewSummary, OpenCodeWakeSupervisorBatchResultSummary, OpenCodeWakeSupervisorCheckSummary, OpenCodeWakeSupervisorContextSectionSummary, OpenCodeWakeSupervisorEvidenceRefSummary, OpenCodeWakeSupervisorExecutionCommandSummary, OpenCodeWakeSupervisorExecutionEvidenceRefSummary, OpenCodeWakeSupervisorExecutionPreviewSummary, OpenCodeWakeSupervisorExecutionRecordSummary, OpenCodeWakeSupervisorExecutionResultSummary, OpenCodeWakeSupervisorExecutionSummaryState, OpenCodeWakeSupervisorPreviewSummary, OpenCodeWakeSupervisorSessionCardSummary, OpenCodeWakeSupervisorSummaryState, OpenCodeWatchdogPreviewSummary, OpenCodeWatchdogRecordSummary, OpenCodeWatchdogResultSummary, OpenCodeWatchdogSummaryState, ResearchMemoryCandidateSummary, ResearchMemoryRetrievalPreviewSummary, ResearchMemorySummaryState, ResearchNoveltyPreviewSummary } from "./state" +import type { CommandAuthorityRecordSummary, CommandAuthoritySummaryState, CommandAuthorityValidationProfileSummary, CommanderGuidanceDeliveryPreviewSummary, CommanderGuidanceDeliveryRecordSummary, CommanderGuidanceDeliveryResultSummary, CommanderGuidanceDeliverySummaryState, CommanderGuidancePreviewSummary, CommanderGuidanceRecordSummary, CommanderGuidanceResultSummary, CommanderGuidanceSummaryState, ContextPacketPreviewSummary, ContextPacketSectionSummary, ContextPacketSummaryState, OpenCodeCommanderQuestionPreviewSummary, OpenCodeCommanderQuestionRecordSummary, OpenCodeCommanderQuestionResultSummary, OpenCodeCommanderQuestionSummaryState, OpenCodeForcedReportRequestSummary, OpenCodeHumanControlPreviewSummary, OpenCodeHumanControlRecordSummary, OpenCodeHumanControlResultSummary, OpenCodeHumanControlSummaryState, OpenCodeLaunchPreviewSummary, OpenCodeLaunchReadinessCheckSummary, OpenCodeLaunchReadinessPreviewSummary, OpenCodeLaunchReadinessSummaryState, OpenCodeLaunchRecordSummary, OpenCodeLaunchResultSummary, OpenCodeProgressPreviewSummary, OpenCodeProgressRecordSummary, OpenCodeProgressResultSummary, OpenCodeProgressSummaryState, OpenCodeResultReportCommandSummary, OpenCodeResultReportPreviewSummary, OpenCodeResultReportRecordSummary, OpenCodeResultReportResultSummary, OpenCodeResultReportSummaryState, OpenCodeSessionInstructionPackFilePreviewSummary, OpenCodeSessionInstructionPackPreviewSummary, OpenCodeSessionInstructionPackRecordSummary, OpenCodeSessionInstructionPackResultSummary, OpenCodeWakeActionExecutionCommandSummary, OpenCodeWakeActionExecutionEvidenceRefSummary, OpenCodeWakeActionExecutionPreviewSummary, OpenCodeWakeActionExecutionRecordSummary, OpenCodeWakeActionExecutionResultSummary, OpenCodeWakeActionExecutionSummaryState, OpenCodeWakeSupervisorBatchPreviewSummary, OpenCodeWakeSupervisorBatchResultSummary, OpenCodeWakeSupervisorCheckSummary, OpenCodeWakeSupervisorContextSectionSummary, OpenCodeWakeSupervisorEvidenceRefSummary, OpenCodeWakeSupervisorExecutionCommandSummary, OpenCodeWakeSupervisorExecutionEvidenceRefSummary, OpenCodeWakeSupervisorExecutionPreviewSummary, OpenCodeWakeSupervisorExecutionRecordSummary, OpenCodeWakeSupervisorExecutionResultSummary, OpenCodeWakeSupervisorExecutionSummaryState, OpenCodeWakeSupervisorPreviewSummary, OpenCodeWakeSupervisorSessionCardSummary, OpenCodeWakeSupervisorSummaryState, OpenCodeWatchdogPreviewSummary, OpenCodeWatchdogRecordSummary, OpenCodeWatchdogResultSummary, OpenCodeWatchdogSummaryState, ResearchMemoryCandidateSummary, ResearchMemoryRetrievalPreviewSummary, ResearchMemorySummaryState, ResearchNoveltyPreviewSummary } from "./state" export interface SubmitUserMessageResult { accepted: true @@ -64,6 +64,7 @@ export class FakeRuntimeClient implements RuntimeClient { private readonly opencodeWakeSupervisorExecutionRecords: OpenCodeWakeSupervisorExecutionResultSummary[] = [] private readonly opencodeWakeSupervisorBatchRecords: OpenCodeWakeSupervisorBatchResultSummary[] = [] private readonly opencodeWakeActionExecutionRecords: OpenCodeWakeActionExecutionResultSummary[] = [] + private readonly opencodeResultReports: OpenCodeResultReportResultSummary[] = [] private readonly commanderExecutorReviews: CommanderExecutorReviewResultSummary[] = [] private readonly executorReviewProposalCreates: ExecutorReviewProposalCreateResultSummary[] = [] private readonly executorReviewProposalReviewRequests: ExecutorReviewProposalReviewRequestResultSummary[] = [] @@ -578,6 +579,18 @@ export class FakeRuntimeClient implements RuntimeClient { return this.latestOpenCodeWakeActionExecution(payload) case "runtime.opencode_wake_action_execution_summary": return this.opencodeWakeActionExecutionSummary(payload) + case "runtime.preview_opencode_result_report": + return this.previewOpenCodeResultReport(payload) + case "runtime.record_opencode_result_report": + return this.recordOpenCodeResultReport(payload) + case "runtime.list_opencode_result_reports": + return this.listOpenCodeResultReports(payload) + case "runtime.get_opencode_result_report": + return this.getOpenCodeResultReport(String(payload.reportId ?? payload.report_id ?? "")) + case "runtime.latest_opencode_result_report": + return this.latestOpenCodeResultReport(payload) + case "runtime.opencode_result_report_summary": + return this.opencodeResultReportSummary(payload) case "runtime.research_memory_summary": return this.researchMemorySummary() case "runtime.preview_research_memory_retrieval": @@ -4040,6 +4053,201 @@ export class FakeRuntimeClient implements RuntimeClient { } } + private previewOpenCodeResultReport(payload: Record): OpenCodeResultReportPreviewSummary { + const progressId = optionalString(payload.progressId ?? payload.progress_id ?? payload.progress) + const progress = progressId ? this.opencodeProgressRecords.find((item) => item.progress_id === progressId) : undefined + const requestedSessionId = optionalString(payload.sessionId ?? payload.session_id ?? payload.session) + const requestedLaunchId = optionalString(payload.launchId ?? payload.launch_id ?? payload.launch) + const sessionId = requestedSessionId ?? progress?.session_id ?? "" + const launchId = requestedLaunchId ?? progress?.launch_id ?? this.opencodeLaunches.find((item) => item.session_id === sessionId && (item.status === "launched" || item.status === "launch_started"))?.launch_id + const session = this.opencodeSessions.find((item) => item.session_id === sessionId) + const launch = this.opencodeLaunches.find((item) => item.launch_id === launchId) + const resultKind = fakeResultReportKind(optionalString(payload.resultKind ?? payload.result_kind ?? payload.kind) ?? progress?.kind) + const rawSummary = String(payload.summary ?? progress?.report_summary_preview ?? "") + const outcome = optionalString(payload.outcome) + const rawChangedFiles = readRawCsvPayload(payload.changedFiles ?? payload.changed_files) + const rawTestsRun = readRawCsvPayload(payload.testsRun ?? payload.tests_run) + const rawTestResults = readRawCsvPayload(payload.testResults ?? payload.test_results) + const rawArtifacts = readRawCsvPayload(payload.artifacts) + const rawMetrics = readRawCsvPayload(payload.metrics) + const rawClaims = readRawCsvPayload(payload.claims) + const rawKnownFailures = readRawCsvPayload(payload.knownFailures ?? payload.known_failures) + const rawFollowups = readRawCsvPayload(payload.followups) + const summary = preview(redactText(rawSummary)) + const changedFiles = readCsvPayload(payload.changedFiles ?? payload.changed_files) + const testsRun = readCsvPayload(payload.testsRun ?? payload.tests_run) + const testResults = readCsvPayload(payload.testResults ?? payload.test_results) + const artifacts = readCsvPayload(payload.artifacts) + const metrics = readCsvPayload(payload.metrics) + const claims = readCsvPayload(payload.claims) + const knownFailures = readCsvPayload(payload.knownFailures ?? payload.known_failures) + const followups = readCsvPayload(payload.followups) + const reviewStateInput = optionalString(payload.reviewState ?? payload.review_state) + let reviewState = fakeResultReviewState(reviewStateInput, resultKind) + const blockers: string[] = [] + if (!sessionId) blockers.push("result report preview requires session=, launch=, or linked evidence") + if (sessionId && !session) blockers.push("linked planned OpenCode session does not resolve") + if (launchId && !launch) blockers.push("linked OpenCode launch does not resolve") + if (launch && launch.status !== "launched" && launch.status !== "launch_started") blockers.push("linked OpenCode launch is not launched") + if (!launchId) blockers.push("result report requires a linked launched OpenCode session") + if (progressId && !progress) blockers.push("linked progress_id does not resolve") + if (progress && sessionId && progress.session_id !== sessionId) blockers.push("linked progress belongs to a different session") + if (!summary) blockers.push("result report requires summary=") + if (resultKind === "completion_report" && !outcome && claims.length === 0) blockers.push("completion_report requires outcome or claims") + if (resultKind === "partial_report" && followups.length === 0 && knownFailures.length === 0) blockers.push("partial_report requires followups or known_failures") + if (resultKind === "failure_report" && !outcome && knownFailures.length === 0) blockers.push("failure_report requires outcome or known_failures") + if (resultKind === "inconclusive_report" && !outcome && followups.length === 0 && knownFailures.length === 0) blockers.push("inconclusive_report requires outcome, followups, or known_failures") + if (resultKind === "blocked_report" && followups.length === 0 && knownFailures.length === 0) blockers.push("blocked_report requires followups or known_failures") + if (String(payload.reviewState ?? payload.review_state ?? "").match(/accepted|rejected/i)) blockers.push("accepted/rejected review states belong to a future Commander result review gate") + if (resultKind !== "status_report" && reviewStateInput && reviewStateInput !== "needs_commander_review" && reviewStateInput !== "needs_human_review") { + blockers.push("terminal OpenCode result reports require Commander or human review") + reviewState = "needs_commander_review" + } + const rawValues = [rawSummary, outcome, ...rawChangedFiles, ...rawTestsRun, ...rawTestResults, ...rawArtifacts, ...rawMetrics, ...rawClaims, ...rawKnownFailures, ...rawFollowups].filter(Boolean).join("\n") + if (looksLikeRawResultPayload(rawValues)) blockers.push("raw logs, full diffs, file contents, provider output, full event logs, and research.db dumps are out of scope") + const reportHash = fakeNavigationStageHash(JSON.stringify({ + sessionId, + launchId, + resultKind, + summary, + outcome, + changedFiles, + testsRun, + testResults, + artifacts, + metrics, + claims, + knownFailures, + followups, + reviewState, + })) + if (this.opencodeResultReports.some((item) => item.session_id === sessionId && item.launch_id === launchId && item.report_hash === reportHash)) blockers.push("duplicate result report hash already exists for this session/launch") + return { + preview_id: `fake-result-report-preview-${reportHash.slice(0, 12)}`, + status: blockers.length ? "blocked" : "ready", + can_record: blockers.length === 0, + session_id: sessionId, + launch_id: launchId, + result_kind: resultKind, + result_disposition: fakeResultDisposition(resultKind), + review_state: reviewState, + summary_preview: summary, + outcome_preview: outcome ? preview(redactText(outcome)) : undefined, + changed_files_preview: changedFiles, + tests_run_preview: testsRun, + test_results_preview: testResults, + artifacts_preview: artifacts, + metrics_preview: metrics, + claims_preview: claims, + known_failures_preview: knownFailures, + followups_preview: followups, + confidence: readConfidence(payload.confidence), + linked_progress_id: progressId, + linked_watchdog_id: optionalString(payload.watchdogId ?? payload.watchdog_id ?? payload.watchdog), + linked_question_id: optionalString(payload.questionId ?? payload.question_id ?? payload.question), + linked_guidance_id: optionalString(payload.guidanceId ?? payload.guidance_id ?? payload.guidance), + linked_delivery_id: optionalString(payload.deliveryId ?? payload.delivery_id ?? payload.delivery), + linked_wake_execution_id: optionalString(payload.wakeExecutionId ?? payload.wake_execution_id ?? payload.wakeExecution ?? payload.wake_execution), + linked_wake_action_execution_id: optionalString(payload.wakeActionExecutionId ?? payload.wake_action_execution_id ?? payload.wakeAction ?? payload.wake_action), + mission_mutated: false, + research_db_written: false, + checkpoint_created: false, + commander_review_created: false, + blockers: blockers.map(redactText), + warnings: ["fake result reports are executor evidence only; no mission completion, Commander acceptance, research.db ingestion, provider call, OpenCode prompt, process control, or checkpoint occurs"], + recommended_commands: fakeResultReportCommands(sessionId), + generated_at: new Date(0).toISOString(), + redacted_summary_preview: blockers[0] ?? `fake result report ${resultKind} review=${reviewState}`, + report_hash: reportHash, + } + } + + private recordOpenCodeResultReport(payload: Record): OpenCodeResultReportResultSummary { + const previewResult = this.previewOpenCodeResultReport(payload) + const dryRun = payload.dryRun === true || payload.dry_run === true + const result: OpenCodeResultReportResultSummary = { + report_id: `fake_result_report_${previewResult.report_hash.slice(0, 12)}_${this.opencodeResultReports.length + 1}`, + status: previewResult.can_record ? dryRun ? "dry_run" : "recorded" : "blocked", + session_id: previewResult.session_id, + launch_id: previewResult.launch_id, + result_kind: previewResult.result_kind, + result_disposition: previewResult.result_disposition, + review_state: previewResult.review_state, + summary_preview: previewResult.summary_preview, + outcome_preview: previewResult.outcome_preview, + changed_files_preview: previewResult.changed_files_preview, + tests_run_preview: previewResult.tests_run_preview, + test_results_preview: previewResult.test_results_preview, + artifacts_preview: previewResult.artifacts_preview, + metrics_preview: previewResult.metrics_preview, + claims_preview: previewResult.claims_preview, + known_failures_preview: previewResult.known_failures_preview, + followups_preview: previewResult.followups_preview, + confidence: previewResult.confidence, + linked_progress_id: previewResult.linked_progress_id, + linked_watchdog_id: previewResult.linked_watchdog_id, + linked_question_id: previewResult.linked_question_id, + linked_guidance_id: previewResult.linked_guidance_id, + linked_delivery_id: previewResult.linked_delivery_id, + linked_wake_execution_id: previewResult.linked_wake_execution_id, + linked_wake_action_execution_id: previewResult.linked_wake_action_execution_id, + mission_mutated: false, + research_db_written: false, + checkpoint_created: false, + commander_review_created: false, + recorded_at: new Date(this.opencodeResultReports.length * 1000).toISOString(), + recorded_by: "operator", + error: previewResult.can_record ? undefined : previewResult.blockers[0] ?? "result report is blocked", + report_hash: previewResult.report_hash, + recommended_commands: previewResult.recommended_commands, + } + if (!dryRun && result.status === "recorded") this.opencodeResultReports.unshift(result) + return result + } + + private listOpenCodeResultReports(payload: Record): OpenCodeResultReportRecordSummary[] { + const sessionId = optionalString(payload.sessionId ?? payload.session_id ?? payload.session) + const launchId = optionalString(payload.launchId ?? payload.launch_id ?? payload.launch) + const resultKind = optionalString(payload.resultKind ?? payload.result_kind ?? payload.kind) + const disposition = optionalString(payload.resultDisposition ?? payload.result_disposition ?? payload.disposition) + const reviewState = optionalString(payload.reviewState ?? payload.review_state) + return this.opencodeResultReports + .filter((item) => !sessionId || item.session_id === sessionId) + .filter((item) => !launchId || item.launch_id === launchId) + .filter((item) => !resultKind || item.result_kind === resultKind) + .filter((item) => !disposition || item.result_disposition === disposition) + .filter((item) => !reviewState || item.review_state === reviewState) + .slice(0, readLimit(payload.limit, 20)) + .map(recordFromFakeResultReport) + } + + private getOpenCodeResultReport(id: string): OpenCodeResultReportResultSummary | null { + return this.opencodeResultReports.find((item) => item.report_id === id) ?? null + } + + private latestOpenCodeResultReport(payload: Record): OpenCodeResultReportResultSummary | null { + const sessionId = optionalString(payload.sessionId ?? payload.session_id ?? payload.session) + const launchId = optionalString(payload.launchId ?? payload.launch_id ?? payload.launch) + return this.opencodeResultReports.find((item) => (!sessionId || item.session_id === sessionId) && (!launchId || item.launch_id === launchId)) ?? null + } + + private opencodeResultReportSummary(payload: Record): OpenCodeResultReportSummaryState { + const records = this.opencodeResultReports.map(recordFromFakeResultReport) + return { + total_reports: records.length, + session_count: new Set(records.map((item) => item.session_id)).size, + completion_count: records.filter((item) => item.result_kind === "completion_report").length, + partial_count: records.filter((item) => item.result_kind === "partial_report").length, + failure_count: records.filter((item) => item.result_kind === "failure_report").length, + inconclusive_count: records.filter((item) => item.result_kind === "inconclusive_report").length, + blocked_count: records.filter((item) => item.result_kind === "blocked_report").length, + needs_commander_review_count: records.filter((item) => item.review_state === "needs_commander_review").length, + needs_human_review_count: records.filter((item) => item.review_state === "needs_human_review").length, + latest_reports: records.slice(0, readLimit(payload.limit, 10)), + generated_at: new Date(0).toISOString(), + } + } + private resolveHumanControlEvidence(input: { progressId?: string; watchdogId?: string; forcedReportId?: string; questionId?: string; guidanceId?: string; deliveryId?: string }): { sessionId?: string; launchId?: string; hasEvidence: boolean; blockers: string[] } { const blockers: string[] = [] const chains: Array<{ sessionId?: string; launchId?: string; label: string }> = [] @@ -10517,6 +10725,49 @@ function fakeCommandAuthorityRecords(): CommandAuthorityRecordSummary[] { targeted: ["tests/e2e_user/scenarios/test_opencode_wake_action_execution_tui.py"], aliases: ["/wake-action-summary"], }), + fakeCommandAuthorityRecord("/opencode-result-report-preview", "runtime.preview_opencode_result_report", "safe_read", "none", "opencode_handoff", { + reads: ["/opencode-launches", "/opencode-progress-latest", "/opencode-result-reports"], + targeted: ["tests/e2e_user/scenarios/test_opencode_result_report_tui.py"], + aliases: ["/result-report-preview"], + notes: ["Read-only OpenCode result-report preview. It validates bounded executor evidence only and never completes missions, writes research.db, creates checkpoints, calls providers, prompts OpenCode, controls processes, or accepts/rejects results."], + out: ["mission/session completion", "Commander acceptance/rejection", "research.db writes/ingestion", "provider/MiniMax calls", "MCP/online research", "OpenCode prompt send", "process pause/kill/stop/resume", "mission/proposal/review/apply mutation", "checkpoint creation"], + }), + fakeCommandAuthorityRecord("/opencode-result-report-dry-run", "runtime.record_opencode_result_report", "safe_read", "none", "opencode_handoff", { + reads: ["/opencode-result-report-preview"], + targeted: ["tests/e2e_user/scenarios/test_opencode_result_report_tui.py"], + aliases: ["/result-report-dry-run"], + notes: ["Dry-run OpenCode result-report metadata write validates eligibility and duplicate behavior without appending events or creating any mission/research/checkpoint/provider/OpenCode/process side effect."], + out: ["event append", "mission/session completion", "Commander acceptance/rejection", "research.db writes/ingestion", "provider/MiniMax calls", "MCP/online research", "OpenCode prompt send", "process control", "checkpoint creation"], + }), + fakeCommandAuthorityRecord("/opencode-result-report", "runtime.record_opencode_result_report", "medium_risk_write", "opencode_runtime", "opencode_handoff", { + mutates: true, + lock: true, + events: ["opencode_result_report_recorded"], + reads: ["/opencode-result-report-preview", "/opencode-result-reports", "/opencode-result-report-latest"], + targeted: ["tests/e2e_user/scenarios/test_opencode_result_report_tui.py"], + aliases: ["/result-report"], + notes: ["Appends bounded OpenCode result-report metadata only. Result reports are executor evidence, not mission completion, Commander acceptance/rejection, research.db ingestion, checkpoint creation, provider calls, OpenCode prompt delivery, or process control."], + out: ["mission/session completion", "Commander acceptance/rejection", "research.db writes/ingestion", "provider/MiniMax calls", "MCP/online research", "OpenCode prompt send", "process pause/kill/stop/resume", "mission/proposal/review/apply mutation", "checkpoint creation"], + }), + fakeCommandAuthorityRecord("/opencode-result-reports", "runtime.list_opencode_result_reports", "safe_read", "none", "opencode_handoff", { + reads: ["/opencode-result-report-show", "/opencode-result-report-latest"], + targeted: ["tests/e2e_user/scenarios/test_opencode_result_report_tui.py"], + aliases: ["/result-reports"], + }), + fakeCommandAuthorityRecord("/opencode-result-report-latest", "runtime.latest_opencode_result_report", "safe_read", "none", "opencode_handoff", { + reads: ["/opencode-result-reports"], + targeted: ["tests/e2e_user/scenarios/test_opencode_result_report_tui.py"], + aliases: ["/result-report-latest"], + }), + fakeCommandAuthorityRecord("/opencode-result-report-show", "runtime.get_opencode_result_report", "safe_read", "none", "opencode_handoff", { + reads: ["/opencode-result-reports"], + targeted: ["tests/e2e_user/scenarios/test_opencode_result_report_tui.py"], + }), + fakeCommandAuthorityRecord("/opencode-result-report-summary", "runtime.opencode_result_report_summary", "safe_read", "none", "opencode_handoff", { + reads: ["/opencode-result-reports"], + targeted: ["tests/e2e_user/scenarios/test_opencode_result_report_tui.py"], + aliases: ["/result-report-summary"], + }), fakeCommandAuthorityRecord("/research-memory-summary", "runtime.research_memory_summary", "safe_read", "none", "research", { targeted: ["tests/e2e_user/scenarios/test_research_memory_novelty_tui.py"], notes: ["Read-only research memory summary; no provider calls, no MCP/online research, no research.db writes, no mission/proposal creation, no OpenCode launch."], @@ -11204,6 +11455,86 @@ function recordFromFakeWakeActionExecution(item: OpenCodeWakeActionExecutionResu } } +function fakeResultReportKind(value: string | undefined): string { + switch (value) { + case "completion_report": + case "partial_report": + case "failure_report": + case "inconclusive_report": + case "blocked_report": + case "status_report": + return value + default: + return "status_report" + } +} + +function fakeResultDisposition(kind: string): string { + switch (kind) { + case "completion_report": + return "reported_done" + case "partial_report": + return "reported_partial" + case "failure_report": + return "reported_failed" + case "inconclusive_report": + return "reported_inconclusive" + case "blocked_report": + return "reported_blocked" + default: + return "reported_status_only" + } +} + +function fakeResultReviewState(input: string | undefined, kind: string): string { + if (input === "needs_commander_review" || input === "needs_human_review") return input + if (kind === "status_report" && (input === "not_ready_for_review" || input === "review_not_required" || input === "unknown")) return input + return kind === "status_report" ? "review_not_required" : "needs_commander_review" +} + +function readConfidence(value: unknown): number | string | undefined { + if (typeof value === "number" && Number.isFinite(value)) return Math.max(0, Math.min(1, value)) + if (typeof value === "string") { + const cleaned = value.trim().toLowerCase() + if (cleaned === "low" || cleaned === "medium" || cleaned === "high" || cleaned === "unknown") return cleaned + const parsed = Number(cleaned) + if (Number.isFinite(parsed)) return Math.max(0, Math.min(1, parsed)) + } + return undefined +} + +function looksLikeRawResultPayload(value: string): boolean { + if (value.length > 4_000) return true + return /\b(stdout|stderr|traceback|stack trace|provider output|file contents|raw opencode output|full event log|full research\.db|research\.db|events\.jsonl)\b/i.test(value) || /diff --git|@@\s+-\d+|\+\+\+ b\/|--- a\//.test(value) +} + +function fakeResultReportCommands(sessionId: string): OpenCodeResultReportCommandSummary[] { + return [ + { label: "List result reports", command: sessionId ? `/opencode-result-reports session=${sessionId}` : "/opencode-result-reports", command_type: "read", notes: "read bounded fake result-report metadata" }, + { label: "Latest result report", command: sessionId ? `/opencode-result-report-latest session=${sessionId}` : "/opencode-result-report-latest", command_type: "read", notes: "read latest fake result-report metadata" }, + { label: "Record result report", command: sessionId ? `/opencode-result-report session=${sessionId} kind=completion_report summary= outcome=` : "/opencode-result-report session= kind=completion_report summary= outcome=", command_type: "write", requires_active_runtime: true, notes: "manual explicit command required; metadata only, no mission completion or research ingestion" }, + ] +} + +function recordFromFakeResultReport(item: OpenCodeResultReportResultSummary): OpenCodeResultReportRecordSummary { + return { + report_id: item.report_id, + session_id: item.session_id, + launch_id: item.launch_id, + result_kind: item.result_kind, + result_disposition: item.result_disposition, + review_state: item.review_state, + summary_preview: item.summary_preview, + recorded_at: item.recorded_at, + recorded_by: item.recorded_by, + confidence: item.confidence, + has_failures: item.known_failures_preview.length > 0, + has_artifacts: item.artifacts_preview.length > 0, + has_metrics: item.metrics_preview.length > 0, + report_hash: item.report_hash, + } +} + function fakeSupervisorDecision( latestProgress: OpenCodeProgressResultSummary | null, latestWatchdog: OpenCodeWatchdogResultSummary | null, diff --git a/agentcore/tui/src/snapshot.ts b/agentcore/tui/src/snapshot.ts index cd4c74b2..03197cc9 100644 --- a/agentcore/tui/src/snapshot.ts +++ b/agentcore/tui/src/snapshot.ts @@ -98,6 +98,7 @@ export function layoutSnapshot(state: UiState): string { out.push(...opencodeWakeSupervisorLines(state)) out.push(...opencodeWakeSupervisorExecutionLines(state)) out.push(...opencodeWakeActionExecutionLines(state)) + out.push(...opencodeResultReportLines(state)) out.push(...researchMemoryLines(state)) out.push(...commanderExecutorReviewLines(state)) out.push(...executorReviewProposalDraftLines(state)) @@ -1470,6 +1471,56 @@ function opencodeWakeActionExecutionLines(state: UiState): string[] { return out } +function opencodeResultReportLines(state: UiState): string[] { + const reports = state.opencodeResultReports + const out = ["OpenCode result reports"] + if (!reports) { + out.push(" preview=none") + out.push(" latest_result=none") + out.push(" records=0") + out.push(" note=result reports are executor evidence only; mission not completed; research.db not written; Commander review is future work") + return out + } + if (reports.preview) { + const item = reports.preview + out.push(` preview=${item.preview_id} status=${item.status} can_record=${item.can_record} kind=${item.result_kind} disposition=${item.result_disposition} review_state=${item.review_state}`) + out.push(` session=${item.session_id} launch=${item.launch_id ?? "none"} summary=${preview(redactText(item.summary_preview))}`) + if (item.outcome_preview) out.push(` outcome=${preview(redactText(item.outcome_preview))}`) + out.push(` previews changed_files=${item.changed_files_preview.join(",") || "none"} tests=${item.tests_run_preview.join(",") || "none"} artifacts=${item.artifacts_preview.join(",") || "none"} metrics=${item.metrics_preview.join(",") || "none"} claims=${item.claims_preview.join(",") || "none"} failures=${item.known_failures_preview.join(",") || "none"} followups=${item.followups_preview.join(",") || "none"}`) + out.push(` flags mission_mutated=${item.mission_mutated} research_db_written=${item.research_db_written} checkpoint_created=${item.checkpoint_created} commander_review_created=${item.commander_review_created}`) + if (item.linked_progress_id || item.linked_watchdog_id || item.linked_question_id || item.linked_guidance_id || item.linked_delivery_id || item.linked_wake_execution_id || item.linked_wake_action_execution_id) out.push(` linked progress=${item.linked_progress_id ?? "none"} watchdog=${item.linked_watchdog_id ?? "none"} question=${item.linked_question_id ?? "none"} guidance=${item.linked_guidance_id ?? "none"} delivery=${item.linked_delivery_id ?? "none"} wake_execution=${item.linked_wake_execution_id ?? "none"} wake_action=${item.linked_wake_action_execution_id ?? "none"}`) + if (item.blockers.length > 0) out.push(` blockers=${item.blockers.map(redactText).join("; ")}`) + if (item.warnings.length > 0) out.push(` warnings=${item.warnings.map(redactText).join("; ")}`) + } else { + out.push(" preview=none") + } + if (reports.latestResult) { + const result = reports.latestResult + out.push(` latest_result=${result.report_id} status=${result.status} kind=${result.result_kind} disposition=${result.result_disposition} review_state=${result.review_state}`) + out.push(` latest_session=${result.session_id} launch=${result.launch_id ?? "none"} summary=${preview(redactText(result.summary_preview))}`) + if (result.outcome_preview) out.push(` latest_outcome=${preview(redactText(result.outcome_preview))}`) + out.push(` latest_previews changed_files=${result.changed_files_preview.join(",") || "none"} tests=${result.tests_run_preview.join(",") || "none"} test_results=${result.test_results_preview.join(",") || "none"} artifacts=${result.artifacts_preview.join(",") || "none"} metrics=${result.metrics_preview.join(",") || "none"} claims=${result.claims_preview.join(",") || "none"} failures=${result.known_failures_preview.join(",") || "none"} followups=${result.followups_preview.join(",") || "none"}`) + out.push(` latest_flags mission_mutated=${result.mission_mutated} research_db_written=${result.research_db_written} checkpoint_created=${result.checkpoint_created} commander_review_created=${result.commander_review_created}`) + if (result.error) out.push(` latest_error=${redactText(result.error)}`) + } else { + out.push(" latest_result=none") + } + if (reports.selected) out.push(` selected=${reports.selected.report_id} status=${reports.selected.status} kind=${reports.selected.result_kind} review_state=${reports.selected.review_state}`) + if (reports.latest) out.push(` latest=${reports.latest.report_id} status=${reports.latest.status} kind=${reports.latest.result_kind} review_state=${reports.latest.review_state}`) + if (reports.summary) { + const summary = reports.summary + out.push(` summary total=${summary.total_reports} sessions=${summary.session_count} completion=${summary.completion_count} partial=${summary.partial_count} failure=${summary.failure_count} inconclusive=${summary.inconclusive_count} blocked=${summary.blocked_count} needs_commander_review=${summary.needs_commander_review_count} needs_human_review=${summary.needs_human_review_count}`) + } else { + out.push(" summary=none") + } + out.push(" records") + if (reports.records.length === 0) out.push(" - empty") + else out.push(...reports.records.slice(0, 12).map((record) => ` - ${record.report_id} session=${record.session_id} launch=${record.launch_id ?? "none"} kind=${record.result_kind} disposition=${record.result_disposition} review_state=${record.review_state} failures=${record.has_failures} artifacts=${record.has_artifacts} metrics=${record.has_metrics}: ${preview(redactText(record.summary_preview))}`)) + if (reports.commandError) out.push(` command_error=${redactText(reports.commandError)}`) + out.push(" note=result report is executor evidence only; mission not completed; research.db not written; checkpoint not created; Commander review is future work") + return out +} + function researchMemoryLines(state: UiState): string[] { const memory = state.researchMemory const out = ["Research memory and novelty"] diff --git a/agentcore/tui/src/state.ts b/agentcore/tui/src/state.ts index da28c0cc..0b3cbec1 100644 --- a/agentcore/tui/src/state.ts +++ b/agentcore/tui/src/state.ts @@ -2663,6 +2663,131 @@ export type OpenCodeWakeActionExecutionState = { commandError?: string } +export type OpenCodeResultReportCommandSummary = { + label: string + command: string + command_type: "read" | "write" | string + requires_active_runtime?: boolean + notes?: string +} + +export type OpenCodeResultReportPreviewSummary = { + preview_id: string + status: "ready" | "blocked" | string + can_record: boolean + session_id: string + launch_id?: string + result_kind: string + result_disposition: string + review_state: string + summary_preview: string + outcome_preview?: string + changed_files_preview: string[] + tests_run_preview: string[] + test_results_preview: string[] + artifacts_preview: string[] + metrics_preview: string[] + claims_preview: string[] + known_failures_preview: string[] + followups_preview: string[] + confidence?: number | string + linked_progress_id?: string + linked_watchdog_id?: string + linked_question_id?: string + linked_guidance_id?: string + linked_delivery_id?: string + linked_wake_execution_id?: string + linked_wake_action_execution_id?: string + mission_mutated: false | boolean + research_db_written: false | boolean + checkpoint_created: false | boolean + commander_review_created: false | boolean + blockers: string[] + warnings: string[] + recommended_commands: OpenCodeResultReportCommandSummary[] + generated_at: string + redacted_summary_preview: string + report_hash: string +} + +export type OpenCodeResultReportResultSummary = { + report_id: string + status: "recorded" | "blocked" | "dry_run" | "failed" | string + session_id: string + launch_id?: string + result_kind: string + result_disposition: string + review_state: string + summary_preview: string + outcome_preview?: string + changed_files_preview: string[] + tests_run_preview: string[] + test_results_preview: string[] + artifacts_preview: string[] + metrics_preview: string[] + claims_preview: string[] + known_failures_preview: string[] + followups_preview: string[] + confidence?: number | string + linked_progress_id?: string + linked_watchdog_id?: string + linked_question_id?: string + linked_guidance_id?: string + linked_delivery_id?: string + linked_wake_execution_id?: string + linked_wake_action_execution_id?: string + mission_mutated: false | boolean + research_db_written: false | boolean + checkpoint_created: false | boolean + commander_review_created: false | boolean + recorded_at: string + recorded_by: string + error?: string + report_hash: string + recommended_commands: OpenCodeResultReportCommandSummary[] +} + +export type OpenCodeResultReportRecordSummary = { + report_id: string + session_id: string + launch_id?: string + result_kind: string + result_disposition: string + review_state: string + summary_preview: string + recorded_at: string + recorded_by: string + confidence?: number | string + has_failures: boolean + has_artifacts: boolean + has_metrics: boolean + report_hash: string +} + +export type OpenCodeResultReportSummaryState = { + total_reports: number + session_count: number + completion_count: number + partial_count: number + failure_count: number + inconclusive_count: number + blocked_count: number + needs_commander_review_count: number + needs_human_review_count: number + latest_reports: OpenCodeResultReportRecordSummary[] + generated_at: string +} + +export type OpenCodeResultReportState = { + preview?: OpenCodeResultReportPreviewSummary | null + latestResult?: OpenCodeResultReportResultSummary | null + records: OpenCodeResultReportRecordSummary[] + selected?: OpenCodeResultReportResultSummary | null + latest?: OpenCodeResultReportResultSummary | null + summary?: OpenCodeResultReportSummaryState | null + commandError?: string +} + export type ResearchMemoryCommandSummary = { label: string command: string @@ -5051,6 +5176,7 @@ export type UiState = { opencodeWakeSupervisor?: OpenCodeWakeSupervisorState opencodeWakeSupervisorExecutions?: OpenCodeWakeSupervisorExecutionsState opencodeWakeActions?: OpenCodeWakeActionExecutionState + opencodeResultReports?: OpenCodeResultReportState researchMemory?: ResearchMemoryState commanderExecutorReview?: CommanderExecutorReviewState executorReviewProposalDrafts?: ExecutorReviewProposalDraftState diff --git a/agentcore/tui/test/keyboard.test.ts b/agentcore/tui/test/keyboard.test.ts index 862b1e05..68ed2667 100644 --- a/agentcore/tui/test/keyboard.test.ts +++ b/agentcore/tui/test/keyboard.test.ts @@ -546,6 +546,42 @@ describe("TUI keyboard command model", () => { } }) + test("OpenCode result report slash commands are exact whitelist entries", () => { + let result = applyKeyCommandWithEffects({ + ...initialState("/tmp/demo"), + screen: "main", + focus: "message-box", + messageDraft: "/opencode-result-report-preview session=session-1 summary=done", + }, { type: "submit" }) + expect(result.effects).toEqual([{ type: "send-command", command: "opencode-result-report-preview", args: ["session=session-1", "summary=done"] }]) + + result = applyKeyCommandWithEffects({ + ...initialState("/tmp/demo"), + screen: "main", + focus: "message-box", + messageDraft: "/result-report session=session-1 kind=completion_report summary=done outcome=tests-passed", + }, { type: "submit" }) + expect(result.effects).toEqual([{ type: "send-command", command: "result-report", args: ["session=session-1", "kind=completion_report", "summary=done", "outcome=tests-passed"] }]) + + result = applyKeyCommandWithEffects({ + ...initialState("/tmp/demo"), + screen: "main", + focus: "message-box", + messageDraft: "/opencode-result-report-summary", + }, { type: "submit" }) + expect(result.effects).toEqual([{ type: "send-command", command: "opencode-result-report-summary" }]) + + for (const message of ["/tmp/result-report", "/path/opencode-result-report", ".opencode-result-report session=session-1", ":opencode-result-report session=session-1"]) { + result = applyKeyCommandWithEffects({ + ...initialState("/tmp/demo"), + screen: "main", + focus: "message-box", + messageDraft: message, + }, { type: "submit" }) + expect(result.effects).toEqual([{ type: "send-user-message", message }]) + } + }) + test("review slash commands route through whitelisted runtime command effects with args", () => { const state: UiState = { ...initialState("/tmp/demo"), diff --git a/agentcore/tui/test/runtime-effects.test.ts b/agentcore/tui/test/runtime-effects.test.ts index fc74f314..b9fd6508 100644 --- a/agentcore/tui/test/runtime-effects.test.ts +++ b/agentcore/tui/test/runtime-effects.test.ts @@ -6075,6 +6075,102 @@ describe("runtime UI effects", () => { expect(JSON.stringify(state)).not.toContain("abc123") }) + test("OpenCode result report slash commands record bounded executor evidence only", async () => { + const runtime = new FakeRuntimeClient("/tmp/demo", "demo") + let state = initialState("/tmp/demo") + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-session-plan", args: ["objective=result", "report", "test", "token=abc123"] }) + const sessionId = state.opencodeSessions?.latestPlan?.session_id + expect(sessionId).toBeTruthy() + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-session-instruction-pack-write", args: [`session=${sessionId}`] }) + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-launch", args: [`session=${sessionId}`] }) + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-progress", args: [`session=${sessionId}`, "summary=done", "token=abc123"] }) + const progressId = state.opencodeProgress?.latestResult?.progress_id + expect(progressId).toBeTruthy() + + const commandCountBeforePreview = runtime.sentCommands.length + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-report-preview", args: [`session=${sessionId}`, "summary=implemented candidate fix token=abc123"] }) + expect(state.opencodeResultReports?.preview).toMatchObject({ + status: "ready", + can_record: true, + session_id: sessionId, + result_kind: "status_report", + result_disposition: "reported_status_only", + review_state: "review_not_required", + mission_mutated: false, + research_db_written: false, + checkpoint_created: false, + commander_review_created: false, + }) + expect(runtime.sentCommands).toHaveLength(commandCountBeforePreview) + + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-report-dry-run", args: [`session=${sessionId}`, "kind=completion_report", "summary=implemented candidate fix token=abc123", "outcome=tests passed"] }) + expect(state.opencodeResultReports?.latestResult).toMatchObject({ status: "dry_run", result_kind: "completion_report", review_state: "needs_commander_review" }) + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-report-dry-run", args: [`progress=${progressId}`, "kind=completion_report", "outcome=tests passed"] }) + expect(state.opencodeResultReports?.latestResult).toMatchObject({ status: "dry_run", result_kind: "completion_report", review_state: "needs_commander_review" }) + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-report-dry-run", args: [`session=${sessionId}`, "kind=completion_report", "review_state=review_not_required", "summary=implemented candidate fix", "outcome=tests passed"] }) + expect(state.opencodeResultReports?.latestResult).toMatchObject({ status: "blocked", result_kind: "completion_report", review_state: "needs_commander_review" }) + expect(state.opencodeResultReports?.commandError).toContain("terminal OpenCode result reports require Commander or human review") + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-report-dry-run", args: [`session=${sessionId}`, "kind=completion_report", `summary=${"x".repeat(180)} diff --git a/file b/file`, "outcome=tests passed"] }) + expect(state.opencodeResultReports?.latestResult).toMatchObject({ status: "blocked", result_kind: "completion_report" }) + expect(state.opencodeResultReports?.commandError).toContain("raw logs") + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-report-dry-run", args: [`session=${sessionId}`, "kind=completion_report", "summary=provider output says done", "outcome=file contents omitted"] }) + expect(state.opencodeResultReports?.latestResult).toMatchObject({ status: "blocked", result_kind: "completion_report" }) + expect(state.opencodeResultReports?.commandError).toContain("raw logs") + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-reports", args: [`session=${sessionId}`] }) + expect(state.opencodeResultReports?.records).toHaveLength(0) + + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-report", args: [`session=${sessionId}`, "kind=completion_report", "summary=implemented candidate fix token=abc123", "outcome=tests passed", "changed_files=fileA.ts", "tests_run=bun-test", "claims=fix-works", "followups=commander-review"] }) + expect(state.opencodeResultReports?.latestResult).toMatchObject({ + status: "recorded", + result_kind: "completion_report", + result_disposition: "reported_done", + review_state: "needs_commander_review", + mission_mutated: false, + research_db_written: false, + checkpoint_created: false, + commander_review_created: false, + }) + const reportId = state.opencodeResultReports?.latestResult?.report_id + expect(reportId).toBeTruthy() + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-report", args: [`session=${sessionId}`, "kind=completion_report", "summary=implemented candidate fix token=abc123", "outcome=tests passed", "changed_files=fileA.ts", "tests_run=bun-test", "claims=fix-works", "followups=commander-review"] }) + expect(state.opencodeResultReports?.latestResult).toMatchObject({ status: "blocked" }) + expect(state.opencodeResultReports?.commandError).toContain("duplicate") + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-report", args: [`session=${sessionId}`, "kind=failure_report", "summary=failed alternative", "known_failures=test still fails"] }) + expect(state.opencodeResultReports?.latestResult).toMatchObject({ status: "recorded", result_kind: "failure_report" }) + + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-reports", args: [`session=${sessionId}`] }) + expect(state.opencodeResultReports?.records).toEqual(expect.arrayContaining([expect.objectContaining({ report_id: reportId, result_kind: "completion_report" })])) + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-report-latest", args: [`session=${sessionId}`] }) + expect(state.opencodeResultReports?.latest).toMatchObject({ result_kind: "failure_report" }) + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-report-show", args: [reportId!] }) + expect(state.opencodeResultReports?.selected).toMatchObject({ report_id: reportId }) + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "opencode-result-report-summary", args: [] }) + expect(state.opencodeResultReports?.summary).toMatchObject({ total_reports: 2, completion_count: 1, failure_count: 1, needs_commander_review_count: 2 }) + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "stage-command", args: ["/opencode-result-report", `session=${sessionId}`, "kind=completion_report", "summary=done", "outcome=ok"] }) + expect(state.operatorActions?.staged?.command_type).toBe("write") + state = await applyRuntimeUiEffect(state, runtime, { type: "send-command", command: "authority-show", args: ["/opencode-result-report"] }) + expect(state.commandAuthority?.selected).toMatchObject({ + slash_command: "/opencode-result-report", + risk: "medium_risk_write", + calls_provider: false, + creates_external_process: false, + mutates_events: true, + }) + expect(state.commandAuthority?.selected?.notes.join(" ")).toContain("result-report metadata") + + const snapshot = layoutSnapshot(state) + expect(snapshot).toContain("OpenCode result reports") + expect(snapshot).toContain("review_state=needs_commander_review") + expect(snapshot).toContain("mission_mutated=false") + expect(snapshot).toContain("research_db_written=false") + expect(snapshot).toContain("checkpoint_created=false") + expect(snapshot).toContain("commander_review_created=false") + expect(snapshot).toContain("mission not completed") + expect(snapshot).toContain("selected=/opencode-result-report risk=medium_risk_write") + expect(snapshot).not.toContain("abc123") + expect(JSON.stringify(state)).not.toContain("abc123") + }) + test("research memory and novelty slash commands render read-only previews", async () => { const runtime = new FakeRuntimeClient("/tmp/demo", "demo") let state = initialState("/tmp/demo") diff --git a/docs/OPENCODE_NATIVE_CONTEXT_COMPATIBILITY.md b/docs/OPENCODE_NATIVE_CONTEXT_COMPATIBILITY.md index 0969d7e5..69df350c 100644 --- a/docs/OPENCODE_NATIVE_CONTEXT_COMPATIBILITY.md +++ b/docs/OPENCODE_NATIVE_CONTEXT_COMPATIBILITY.md @@ -214,7 +214,7 @@ Session tactical memory should map to generated bounded files such as: - `.nxl/opencode/sessions//MANIFEST.json` - `.nxl/opencode/sessions//opencode-session-config.json` -Branch 9B3 writes these files as bounded, per-session future-launch artifacts with `launch_ready=false`. It does not launch OpenCode, mutate `AGENTS.md`, mutate global OpenCode config, call providers, call MCPs, or query `research.db`. Branch 9C adds a read-only launch-readiness preview that verifies the planned session, instruction-pack event metadata, on-disk generated file hashes, `MANIFEST.json`, `opencode-session-config.json`, context packet/budget status, advisory novelty metadata, and static native launch assumptions without spawning OpenCode. Branch 9D adds the first explicit launch gate: it rebuilds readiness, requires a ready planned session and matching instruction pack, supports preview/dry-run/list/get without starting OpenCode, and records bounded launch metadata for one explicit launch attempt. Fake launch remains process-free for tests; real external launch requires explicit opt-in and still does not supervise progress, enforce timeout, inject guidance, call providers/MCPs, or mutate missions. Branch 9E adds typed, bounded progress and heartbeat records anchored to 9D launch records. These reports capture heartbeat/progress/blocker/question metadata for future supervision without polling OpenCode, reading raw logs, enforcing timeouts, answering questions, injecting Commander guidance, writing `research.db`, or mutating missions. Branch 9G converts explicit operator input plus eligible 9E progress questions/blockers and 9F watchdog/forced-report evidence into durable `OpenCodeCommanderQuestion` records. It records the question side only and does not answer, call Commander providers, send prompts to OpenCode, inject guidance, control processes, run wake supervision, write `research.db`, or mutate missions. Branch 9H records bounded `CommanderGuidance` answers for pending questions and projects the linked question as answered. Delivery remains `not_delivered`; it does not call providers, send prompts to OpenCode, inject guidance into a process, control processes, run wake supervision, write `research.db`, or mutate missions. Branch 9I adds an explicit delivery gate for one `CommanderGuidance` record. Because the current adapter surface has no safe tested running-session prompt injection path, real/runtime delivery uses `operator_handoff` only: it records `pending_delivery` metadata and an operator handoff preview, but does not claim OpenCode received the guidance. `adapter_send` remains blocked until a later branch adds explicit opt-in and a safe adapter path. Branch 9J adds durable human-control records for pause/resume/stop/correction/override/report/note intent. These records project human intent for future Commander/wake supervision, but always record `process_control_performed=false`, `open_code_prompt_sent=false`, and `mission_mutated=false`. Branch 9K adds a read-only wake supervisor preview across launch/progress/watchdog/question/guidance/delivery/human-control evidence. Branch 9L records explicit wake-supervisor execution ticks and capped batches from the 9K preview while persisting `action_execution_status=not_executed`; it does not execute recommended commands, send OpenCode prompts, call providers/MCPs, control processes, write `research.db`, mutate missions, create checkpoints, or ingest results. Branch 9M adds a separate recommended-action execution gate for typed metadata-only actions. It may call existing 9F watchdog/forced-report, 9G question, or 9I operator-handoff services through explicit allowlisted APIs only when the requested action matches the 9L recorded recommendation; it blocks mismatched action overrides and never executes arbitrary recommended command text, calls providers/MCPs, sends OpenCode prompts, controls processes, writes `research.db`, mutates missions, creates checkpoints, or ingests results. +Branch 9B3 writes these files as bounded, per-session future-launch artifacts with `launch_ready=false`. It does not launch OpenCode, mutate `AGENTS.md`, mutate global OpenCode config, call providers, call MCPs, or query `research.db`. Branch 9C adds a read-only launch-readiness preview that verifies the planned session, instruction-pack event metadata, on-disk generated file hashes, `MANIFEST.json`, `opencode-session-config.json`, context packet/budget status, advisory novelty metadata, and static native launch assumptions without spawning OpenCode. Branch 9D adds the first explicit launch gate: it rebuilds readiness, requires a ready planned session and matching instruction pack, supports preview/dry-run/list/get without starting OpenCode, and records bounded launch metadata for one explicit launch attempt. Fake launch remains process-free for tests; real external launch requires explicit opt-in and still does not supervise progress, enforce timeout, inject guidance, call providers/MCPs, or mutate missions. Branch 9E adds typed, bounded progress and heartbeat records anchored to 9D launch records. These reports capture heartbeat/progress/blocker/question metadata for future supervision without polling OpenCode, reading raw logs, enforcing timeouts, answering questions, injecting Commander guidance, writing `research.db`, or mutating missions. Branch 9G converts explicit operator input plus eligible 9E progress questions/blockers and 9F watchdog/forced-report evidence into durable `OpenCodeCommanderQuestion` records. It records the question side only and does not answer, call Commander providers, send prompts to OpenCode, inject guidance, control processes, run wake supervision, write `research.db`, or mutate missions. Branch 9H records bounded `CommanderGuidance` answers for pending questions and projects the linked question as answered. Delivery remains `not_delivered`; it does not call providers, send prompts to OpenCode, inject guidance into a process, control processes, run wake supervision, write `research.db`, or mutate missions. Branch 9I adds an explicit delivery gate for one `CommanderGuidance` record. Because the current adapter surface has no safe tested running-session prompt injection path, real/runtime delivery uses `operator_handoff` only: it records `pending_delivery` metadata and an operator handoff preview, but does not claim OpenCode received the guidance. `adapter_send` remains blocked until a later branch adds explicit opt-in and a safe adapter path. Branch 9J adds durable human-control records for pause/resume/stop/correction/override/report/note intent. These records project human intent for future Commander/wake supervision, but always record `process_control_performed=false`, `open_code_prompt_sent=false`, and `mission_mutated=false`. Branch 9K adds a read-only wake supervisor preview across launch/progress/watchdog/question/guidance/delivery/human-control evidence. Branch 9L records explicit wake-supervisor execution ticks and capped batches from the 9K preview while persisting `action_execution_status=not_executed`; it does not execute recommended commands, send OpenCode prompts, call providers/MCPs, control processes, write `research.db`, mutate missions, create checkpoints, or ingest results. Branch 9M adds a separate recommended-action execution gate for typed metadata-only actions. It may call existing 9F watchdog/forced-report, 9G question, or 9I operator-handoff services through explicit allowlisted APIs only when the requested action matches the 9L recorded recommendation; it blocks mismatched action overrides and never executes arbitrary recommended command text, calls providers/MCPs, sends OpenCode prompts, controls processes, writes `research.db`, mutates missions, creates checkpoints, or ingests results. Branch 9N adds bounded OpenCode result-report records for completion, partial, failure, inconclusive, blocked, and status reports. Result reports are executor evidence only: they do not mark missions or sessions complete, accept or reject results, write or ingest `research.db`, create checkpoints, call providers/MCPs, send OpenCode prompts, control processes, mutate missions/proposals/reviews/apply records, or run Commander review. Durable memory remains: @@ -223,6 +223,7 @@ Durable memory remains: - `research.db` - planned and launched OpenCode session records - progress reports +- result reports - `CommanderGuidance` - `OpenCodeCommanderQuestion` - `HumanIntervention` @@ -276,7 +277,7 @@ Proposed upper-layer protocol: - Runtime can mark a session paused or blocked until guidance is available. - Guidance is injected only as bounded follow-up input on resume/continue. -Branch 9B0 does not implement this protocol. Branch 9G implements the durable question/request side only. It creates pending Commander question records from explicit input, 9E progress evidence, and 9F watchdog/forced-report evidence; it does not implement Commander answers, guidance injection, provider calls, OpenCode prompt sends, process pause/kill/stop, wake execution, or mission/proposal/review/apply mutation. Branch 9H implements the durable answer/guidance record side only. It creates `CommanderGuidance` metadata, appends an answered-question projection event, and leaves delivery status as `not_delivered`. Branch 9I implements the explicit delivery gate as operator handoff metadata only in the real runtime path: it can preview, dry-run, and record `opencode_commander_guidance_delivery_requested`, which projects guidance to `pending_delivery`; it still does not send a prompt to OpenCode, call providers/MCPs, control processes, run wake supervision, write `research.db`, mutate missions, or mark guidance as `delivered`. Branch 9J adds explicit human-control metadata for launched sessions. It can record pause/resume/stop/correction/override/report/note intent for later supervision, but it does not perform OS/process control, send OpenCode prompts, call providers/MCPs, execute wake supervision, write `research.db`, mutate missions, or claim OpenCode received the instruction. Branch 9L records wake-supervisor execution metadata from 9K previews only; it can persist single-session and batch tick records, but `action_execution_status` remains `not_executed` and no recommended action is performed. Branch 9M consumes one 9L execution record through an explicit gate and executes only typed metadata actions when they match the 9L recommendation: `record_watchdog`, `request_forced_report`, `create_commander_question`, and optional operator-handoff delivery. It blocks mismatched overrides, answer generation, real guidance delivery, result review, unsupported actions, and all arbitrary command execution. +Branch 9B0 does not implement this protocol. Branch 9G implements the durable question/request side only. It creates pending Commander question records from explicit input, 9E progress evidence, and 9F watchdog/forced-report evidence; it does not implement Commander answers, guidance injection, provider calls, OpenCode prompt sends, process pause/kill/stop, wake execution, or mission/proposal/review/apply mutation. Branch 9H implements the durable answer/guidance record side only. It creates `CommanderGuidance` metadata, appends an answered-question projection event, and leaves delivery status as `not_delivered`. Branch 9I implements the explicit delivery gate as operator handoff metadata only in the real runtime path: it can preview, dry-run, and record `opencode_commander_guidance_delivery_requested`, which projects guidance to `pending_delivery`; it still does not send a prompt to OpenCode, call providers/MCPs, control processes, run wake supervision, write `research.db`, mutate missions, or mark guidance as `delivered`. Branch 9J adds explicit human-control metadata for launched sessions. It can record pause/resume/stop/correction/override/report/note intent for later supervision, but it does not perform OS/process control, send OpenCode prompts, call providers/MCPs, execute wake supervision, write `research.db`, mutate missions, or claim OpenCode received the instruction. Branch 9L records wake-supervisor execution metadata from 9K previews only; it can persist single-session and batch tick records, but `action_execution_status` remains `not_executed` and no recommended action is performed. Branch 9M consumes one 9L execution record through an explicit gate and executes only typed metadata actions when they match the 9L recommendation: `record_watchdog`, `request_forced_report`, `create_commander_question`, and optional operator-handoff delivery. It blocks mismatched overrides, answer generation, real guidance delivery, result review, unsupported actions, and all arbitrary command execution. Branch 9N records bounded executor result reports for later Commander review; it does not accept results, complete missions, ingest research, create checkpoints, or mutate authority records beyond the result-report event. ## Provider And Model Support Implications @@ -329,9 +330,10 @@ Branch 9B0 does not implement this protocol. Branch 9G implements the durable qu - 9K: wake supervisor preview (read-only aggregate evidence/context surface across 9A/9D/9E/9F/9G/9H/9I/9J records; no scheduled wake tick, provider call, OpenCode prompt send, process control, `research.db` write, or mission mutation) - 9L: scheduled wake supervision execution metadata (single/batch records with `action_execution_status=not_executed`; no recommended command execution) - 9M: wake recommended-action execution gate (typed allowlisted metadata actions only; no arbitrary command execution, provider call, OpenCode prompt send, process control, `research.db` write, mission mutation, result ingestion, or checkpoint creation) -- 9M: OpenCode result report model or gated action execution -- 9N: Commander research promotion gate -- 9O: end-to-end supervised training demo +- 9N: OpenCode result report model (executor evidence only; no Commander acceptance, mission completion, research ingestion, or checkpoint creation) +- 9O: Commander result review gate +- 9P: Commander research promotion gate +- 9Q: end-to-end supervised training demo ## Branch 9B0 Explicit Out Of Scope diff --git a/tests/e2e_user/scenarios/test_opencode_result_report_tui.py b/tests/e2e_user/scenarios/test_opencode_result_report_tui.py new file mode 100644 index 00000000..93f76998 --- /dev/null +++ b/tests/e2e_user/scenarios/test_opencode_result_report_tui.py @@ -0,0 +1,193 @@ +from __future__ import annotations + +import json +import re + +import pytest + + +@pytest.mark.phase_m4 +def test_user_records_opencode_result_report_metadata_only(sandbox) -> None: + install = sandbox.install_from_current_repo() + assert install.exit_code == 0, install.stdout + install.stderr + + sandbox.env["NXL_TUI_HEADLESS"] = "1" + sandbox.runner.env["NXL_TUI_HEADLESS"] = "1" + sandbox.env["NXL_RUNTIME_CLIENT"] = "real" + sandbox.runner.env["NXL_RUNTIME_CLIENT"] = "real" + sandbox.env["NXL_OPENCODE_ADAPTER"] = "fake" + sandbox.runner.env["NXL_OPENCODE_ADAPTER"] = "fake" + sandbox.env["NXL_SECRET_RESULT_REPORT_TOKEN"] = "result-report-secret-abc123" + sandbox.runner.env["NXL_SECRET_RESULT_REPORT_TOKEN"] = "result-report-secret-abc123" + + project = sandbox.make_empty_project_dir("opencode_result_report_project") + spec_dir = project / ".nxl" / "spec" + spec_dir.mkdir(parents=True) + (spec_dir / "current.json").write_text( + json.dumps( + { + "spec_id": "spec_opencode_result_report_e2e", + "version": 1, + "status": "approved", + "objective": "Exercise OpenCode result report records through real runtime TUI", + "project_mode": "build", + "domain": "test", + "success_metrics": ["result report surface renders"], + "evaluation_protocol": "run headless TUI", + "approved_by": "e2e", + "approved_at": "2026-07-01T00:00:00Z", + }, + indent=2, + ), + encoding="utf-8", + ) + + plan_keys = [ + {"type": "submit"}, + {"type": "insert", "text": "/opencode-session-plan objective=result report test token=abc123"}, + {"type": "submit"}, + {"type": "insert", "text": "/opencode-sessions"}, + {"type": "submit"}, + ] + sandbox.env["NXL_TUI_KEYS"] = json.dumps(plan_keys) + sandbox.runner.env["NXL_TUI_KEYS"] = json.dumps(plan_keys) + plan_result = sandbox.run_cli([], cwd=project) + assert plan_result.exit_code == 0, plan_result.stdout + plan_result.stderr + session_match = re.search(r"latest=(opencode_session_[A-Za-z0-9._-]+)", plan_result.stdout) + assert session_match, plan_result.stdout + session_id = session_match.group(1) + + launch_keys = [ + {"type": "submit"}, + {"type": "insert", "text": f"/opencode-session-instruction-pack-write session={session_id}"}, + {"type": "submit"}, + {"type": "insert", "text": f"/opencode-launch-readiness session={session_id}"}, + {"type": "submit"}, + {"type": "insert", "text": f"/opencode-launch session={session_id}"}, + {"type": "submit"}, + ] + sandbox.env["NXL_TUI_KEYS"] = json.dumps(launch_keys) + sandbox.runner.env["NXL_TUI_KEYS"] = json.dumps(launch_keys) + launch_result = sandbox.run_cli([], cwd=project) + assert launch_result.exit_code == 0, launch_result.stdout + launch_result.stderr + assert "status=launched" in launch_result.stdout + + progress_keys = [ + {"type": "submit"}, + {"type": "insert", "text": f"/opencode-progress session={session_id} summary=implemented first change files=fileA.ts tests=bun-test"}, + {"type": "submit"}, + {"type": "insert", "text": f"/opencode-progress session={session_id} summary=done with candidate fix token=abc123"}, + {"type": "submit"}, + ] + sandbox.env["NXL_TUI_KEYS"] = json.dumps(progress_keys) + sandbox.runner.env["NXL_TUI_KEYS"] = json.dumps(progress_keys) + progress_result = sandbox.run_cli([], cwd=project) + assert progress_result.exit_code == 0, progress_result.stdout + progress_result.stderr + + events_path = project / ".nxl" / "events.jsonl" + events_before_report = [ + json.loads(line) + for line in events_path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + event_kinds_before = [event["kind"] for event in events_before_report] + + report_keys = [ + {"type": "submit"}, + {"type": "insert", "text": f"/opencode-result-report-preview session={session_id} summary=implemented candidate fix token=abc123"}, + {"type": "submit"}, + {"type": "insert", "text": f"/opencode-result-report-dry-run session={session_id} kind=completion_report summary=implemented candidate fix token=abc123 outcome=tests passed"}, + {"type": "submit"}, + {"type": "insert", "text": f"/opencode-result-report session={session_id} kind=completion_report summary=implemented candidate fix token=abc123 outcome=tests passed changed_files=fileA.ts tests_run=bun-test claims=fix-works followups=commander-review"}, + {"type": "submit"}, + {"type": "insert", "text": f"/opencode-result-reports session={session_id}"}, + {"type": "submit"}, + {"type": "insert", "text": f"/opencode-result-report-latest session={session_id}"}, + {"type": "submit"}, + {"type": "insert", "text": "/opencode-result-report-summary"}, + {"type": "submit"}, + {"type": "insert", "text": "/authority-show /opencode-result-report"}, + {"type": "submit"}, + ] + sandbox.env["NXL_TUI_KEYS"] = json.dumps(report_keys) + sandbox.runner.env["NXL_TUI_KEYS"] = json.dumps(report_keys) + report_result = sandbox.run_cli([], cwd=project) + assert report_result.exit_code == 0, report_result.stdout + report_result.stderr + + assert "OpenCode result reports" in report_result.stdout + assert "latest_result=" in report_result.stdout + assert "kind=completion_report" in report_result.stdout + assert "review_state=needs_commander_review" in report_result.stdout + assert "mission_mutated=false" in report_result.stdout + assert "research_db_written=false" in report_result.stdout + assert "checkpoint_created=false" in report_result.stdout + assert "commander_review_created=false" in report_result.stdout + assert "mission not completed" in report_result.stdout + assert "research.db not written" in report_result.stdout + assert "selected=/opencode-result-report risk=medium_risk_write" in report_result.stdout + assert "result-report-secret" not in report_result.stdout + assert "result-report-secret-abc123" not in report_result.stdout + assert "token=abc123" not in report_result.stdout + + events = [ + json.loads(line) + for line in events_path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + event_kinds = [event["kind"] for event in events] + assert event_kinds.count("opencode_result_report_recorded") == event_kinds_before.count("opencode_result_report_recorded") + 1 + assert event_kinds.count("opencode_session_launch_started") == 1 + assert event_kinds.count("opencode_session_launch_succeeded") == 1 + + new_events = events[len(events_before_report) :] + new_event_kinds = [event["kind"] for event in new_events] + metadata_new_event_kinds = [ + kind + for kind in new_event_kinds + if kind not in {"runtime_started", "runtime_ready", "runtime_shutdown"} + ] + assert metadata_new_event_kinds == ["opencode_result_report_recorded"] + report_events = [event for event in new_events if event["kind"] == "opencode_result_report_recorded"] + assert len(report_events) == 1 + serialized_report_events = json.dumps(report_events) + assert '"mission_mutated": false' in serialized_report_events + assert '"research_db_written": false' in serialized_report_events + assert '"checkpoint_created": false' in serialized_report_events + assert '"commander_review_created": false' in serialized_report_events + + forbidden_new_events = { + "commander_result_review_created", + "commander_result_review_accepted", + "research_result_ingested", + "research_db_written", + "runtime_checkpoint_created", + "opencode_prompt_sent", + "opencode_commander_guidance_delivered", + "opencode_session_process_paused", + "opencode_session_process_resumed", + "opencode_session_process_killed", + "opencode_session_process_stopped", + "opencode_session_launch_started", + "opencode_session_launch_succeeded", + "runtime_wake_scheduler_started", + "runtime_wake_schedule_tick_completed", + "mission_created", + "mission_claimed", + "mission_progress_recorded", + "mission_result_submitted", + "mission_completed", + "mission_failed", + "review_request_created", + "commander_proposal_created", + "commander_proposal_applied", + "external_api_request_executed", + "external_api_research_ingestion_created", + } + assert forbidden_new_events.isdisjoint(new_event_kinds) + serialized_new_events = json.dumps(new_events) + assert "result-report-secret" not in serialized_new_events + assert "token=abc123" not in serialized_new_events + assert "stdout" not in serialized_new_events + assert "stderr" not in serialized_new_events + assert "diff --git" not in serialized_new_events + assert "file contents" not in serialized_new_events