// Host-neutral foreground Assurance coordination. // // Deterministic QA runs to completion inside the caller's Tool execution. A // successful QA pass creates one short-lived Host Review reservation; the // Parent invokes a foreground reviewer and submit_review consumes its // structured verdict. No lifecycle work survives a Tool call except the // evidence reservation. import { readGitIntegrationPreference, type GitIntegrationMode, type GitIntegrationPreference } from "../git_integration_preference"; import { createHash, randomUUID } from "node:crypto"; import type { FindingEvidence, ReviewAdvisoryFindingV1, TaskFinding } from "../kernel/types"; import { reviewIncompleteHistory } from "../kernel/reducer"; import { anchorForEvidence } from "../kernel/refutation"; import { findingsDigest, type VerificationDescriptor, VerificationAbortedError, } from "./verification"; import { createInvocationRegistry, type InvocationState, type InvocationToken } from "./invocations"; import type { ReviewBundle, ReviewManifestV5 } from "./review_evidence"; import { createVerdictAuthority, type VerdictAuthority } from "./verdict_authority"; import type { ConfirmationReferenceSource } from "./host_port"; import { buildRoleDelegationPacket, STATIC_REVIEW_RULES, currentReviewContractHash } from "../role_prompt_bridge"; import { projectAssurance, type AssuranceProjectionResult } from "../kernel/assurance_projection"; import { taskStatusIdentity } from "../workspace_scope"; import { pluginSourceSkewWarning, type PluginSourceSkewWarning } from "../plugin_source_skew"; import type { TaskIntentIdentityToken } from "../kernel/intent_token_registry"; import type { AssuranceHostPort, HostReviewReservation, ReviewEvidence } from "./host_port"; import type { GithubTrackerResult as GithubTrackerProjectionResult } from "../github_issue_tracker"; import { isLaneWorkspaceForTask } from "../unattended/lane_workspace"; import { findExistingActiveBatch, findSettledBatchRecord, type BatchRecordLookup } from "../unattended/batch_preflight"; import { readAnyBatchRunState } from "../unattended/batch_state"; export type AssuranceRole = "qa" | "review"; export interface AssuranceCorrelation { record_revision: string; intent_content_hash: string; diff_hash: string; } export interface HostContext { cwd: string; } export interface TaskTombstone { task_id: string; lifecycle_status: string; terminal_lifecycle: string; terminal_event_id: string; } export interface TaskRecordRead { revision?: string; record?: { contract?: string; findings: Array<{ kind: string; status: string; id?: string; acceptance_id?: string | null }>; /** Kernel history; Review disclosure reads `revise_intent` reasons from it. */ history?: Array<{ type: string; reason: string }>; } | null; } export interface TaskIntentRead { /** * The kernel's own intent identity token. Declared as `string` before this * port was ever type checked, which is a shape no host reader has produced. */ token?: TaskIntentIdentityToken; } export interface GithubTerminalProjectionInput { task_id: string; phase: "done" | "stopped"; terminal_event_id: string; } export function deriveGithubTerminalProjectionInput( taskId: string, projection: AssuranceProjectionResult, tombstone: TaskTombstone | null, ): GithubTerminalProjectionInput | null { if ( projection.error || projection.claim !== null || (projection.projection.lifecycle !== "done" && projection.projection.lifecycle !== "stopped") || tombstone?.task_id !== taskId || tombstone.lifecycle_status !== "terminal" || tombstone.terminal_lifecycle !== projection.projection.lifecycle ) return null; return { task_id: taskId, phase: projection.projection.lifecycle, terminal_event_id: tombstone.terminal_event_id, }; } /** * Post-settlement GitHub tracker projection, shared so a task settled under * either Host produces the same opted-in terminal projection. * * Transport, never authority: the projection runs only for a fresh claimless * done/stopped projection plus its exact terminal tombstone (`derive` returns * null otherwise), a tracker failure is reported as a retryable failure rather * than evidence or a blocker, and a failure never repeats the settling Kernel * mutation. * * A completed settlement inside a batch Lane writes nothing to the tracker: the * Lane's delivery is not on the batch branch yet, so the coordinator closes the * Child only after it integrates that child's commit. */ export async function projectTerminalTrackerState(input: { root: string; task_id: string; projection: AssuranceProjectionResult; tombstone: TaskTombstone | null; markTerminal: (root: string, projection: GithubTerminalProjectionInput) => Promise; isLaneWorkspace?: (root: string, taskId: string) => boolean; }): Promise { if (input.projection.error) return undefined; const terminal = deriveGithubTerminalProjectionInput(input.task_id, input.projection, input.tombstone); if (!terminal) return undefined; // Only a completed delivery waits for integration; an explicit stop inside a // Lane is never integrated and still projects `not planned` here. if (terminal.phase === "done" && (input.isLaneWorkspace ?? isLaneWorkspaceForTask)(input.root, input.task_id)) return undefined; try { return await input.markTerminal(input.root, terminal); } catch { return TRACKER_PROJECTION_FAILURE; } } /** The one shape both Hosts report when post-settlement observation fails. */ export const TRACKER_PROJECTION_FAILURE: GithubTrackerProjectionResult = { contract: "immune_brain/github_issue_tracker_result/v1", operation: "mark-terminal", status: "retryable_failure", association_found: false, message: "tracker observation failed after authoritative settlement", }; export interface AssuranceVerdict { contract: "assurance_kernel/assurance_verdict/v2"; role: AssuranceRole; task_id: string; snapshot_digest: string; decision: "pass" | "rework"; approval?: { kind: "qa" | "review"; authority_role: "qa" | "reviewer"; summary: string; }; findings?: Array<{ id: string; kind: "blocking" | "advisory"; acceptance_id: string | null; summary: string; /** * Derived by `parseAssuranceVerdict` for every Review-role finding, the * advisory notes of a pass verdict included. QA findings never carry it. */ anchor?: string; evidence?: FindingEvidence; /** Digest this plugin's verdict parser derives for one finding. */ findings_digest: string; }>; } /** * The single Review-verdict → Kernel-finding mapping both Host adapters use. * Keeping it here is what makes the two Hosts produce the same * `findingsDigestV2` for the same verdict: the reviewer's anchor and evidence * are forwarded unchanged into the capability binding and the Kernel action. */ export function reviewReworkFindings(verdict: AssuranceVerdict): TaskFinding[] { if (verdict.decision !== "rework" || !verdict.findings?.length) throw new Error("review rework findings require a rework verdict"); // Advisory notes belong to a pass attestation; one that reached a rework // verdict is a contract violation, never Executor work to filter away. if (verdict.findings.some((finding) => finding.kind !== "blocking")) throw new Error("review rework findings must all be blocking"); return verdict.findings.map((finding) => ({ id: finding.id, kind: "blocking" as const, status: "open" as const, acceptance_id: finding.acceptance_id, source: "review" as const, review_round: null, summary: finding.summary, anchor: finding.anchor ?? null, evidence: finding.evidence ?? null, counterevidence: null, })); } /** * The advisory counterpart of `reviewReworkFindings`: a pass verdict may carry * non-blocking Review notes, and both Hosts record them on the review * attestation itself so settlement can never separate the two. */ export function reviewAdvisoryRecords(verdict: AssuranceVerdict): ReviewAdvisoryFindingV1[] { if (verdict.decision !== "pass") throw new Error("advisory findings require a pass verdict"); return (verdict.findings ?? []) .filter((finding) => finding.kind === "advisory") .map((finding) => ({ id: finding.id, acceptance_id: finding.acceptance_id, summary: finding.summary, anchor: finding.anchor ?? null, evidence: finding.evidence ?? null, })); } export interface ReviewIncompleteOutcome { contract: "assurance_kernel/assurance_verdict/v2"; role: "review"; task_id: string; snapshot_digest: string; decision: "incomplete"; inspected_paths: string[]; reason: string; } export type ChildReviewResult = AssuranceVerdict | ReviewIncompleteOutcome; type ReviewIncompleteResult = { state: "review_incomplete"; reason: string; recovery_action: string }; const REVIEW_INCOMPLETE_RECOVERY = "Inspect the incomplete Review coverage, request_authorization may offer one snapshot-bound retry under a changed Review contract (legacy unknown contract requires explicit disclosure); otherwise reduce the change or revise the TaskIntent. Never redispatch without Kernel authorization."; export interface ReviewRevisionIdentity { contract: "assurance_kernel/review_revision_identity/v1"; base_head: string; review_commit: string; review_tree: string; manifest_digest: string; } export interface SnapshotDescriptor { contract: "assurance_kernel/assurance_snapshot/v2"; task_id: string; /** The exact run this snapshot was captured from; null when none is committed. */ run_id: string | null; role: AssuranceRole; record_revision: string; workspace_revision: string; intent_revision: number; intent_content_hash: string; diff_hash: string; lifecycle: string; artifact_state: string; risk: "routine" | "material" | "critical"; fresh_acceptance_ids: string[]; missing_acceptance_ids: string[]; stale_attestation_ids: string[]; acceptance: Array<{ id: string; assertion: string; verification: string }>; dirty_files: string[]; review_bundle_digest: string | null; /** Present only when Review authority binds an immutable Git revision (v4). */ review_revision?: ReviewRevisionIdentity; /** * Review round 3 and later: the paths the latest rework changed. `skipped` * means the prior revision was absent or unresolvable and the admission rule * does not apply to this round. */ review_delta?: | { round: number; prior_review_commit: string; delta_paths: string[] } | { round: number; skipped: "prior_review_revision_absent" | "prior_review_revision_unresolvable" }; root: string; } export type QaPreparationReason = "executable_or_cwd_unavailable" | "process_launch_failed" | "process_cleanup_failed" | "protected_input_or_output_drift" | "execution_metadata_limit_exceeded" | "timeout" | "output_limit" | "nonzero_exit" | "execution_failed" | "command_identity_changed" | "delivery_cleanup_failed" | "delivery_unavailable"; export interface QaCheckDiagnostic { acceptance_id: string; descriptor_ref: string; descriptor_digest: string; stage: "resolution" | "prepare" | "check" | "integrity"; outcome: "running" | "passed" | QaPreparationReason; elapsed_ms: number; exit_code: number | null; stdout_bytes: number | null; stderr_bytes: number | null; } const QA_STAGES = ["resolution", "prepare", "check", "integrity"]; const QA_OUTCOMES = ["running", "passed", "executable_or_cwd_unavailable", "process_launch_failed", "process_cleanup_failed", "protected_input_or_output_drift", "execution_metadata_limit_exceeded", "timeout", "output_limit", "nonzero_exit", "execution_failed", "command_identity_changed", "delivery_cleanup_failed", "delivery_unavailable"]; /** The Tool boundary never spreads arbitrary executor fields. */ export function qaDiagnosticMetadata(d: QaCheckDiagnostic): QaCheckDiagnostic { if (typeof d.acceptance_id !== "string" || !d.acceptance_id || !/^acceptance\/\d{1,6}\/verification\/(command|environment\/prepare)$/.test(d.descriptor_ref) || !/^sha256:[a-f0-9]{64}$/.test(d.descriptor_digest) || !QA_STAGES.includes(d.stage) || !QA_OUTCOMES.includes(d.outcome) || !Number.isFinite(d.elapsed_ms) || d.elapsed_ms < 0 || [d.stdout_bytes, d.stderr_bytes].some(n => n !== null && (!Number.isSafeInteger(n) || n < 0)) || (d.exit_code !== null && !Number.isSafeInteger(d.exit_code))) throw new Error("invalid QA diagnostic metadata"); const safe = { acceptance_id: d.acceptance_id, descriptor_ref: d.descriptor_ref, descriptor_digest: d.descriptor_digest, stage: d.stage, outcome: d.outcome, elapsed_ms: d.elapsed_ms, exit_code: d.exit_code, stdout_bytes: d.stdout_bytes, stderr_bytes: d.stderr_bytes }; if (Buffer.byteLength(JSON.stringify(safe)) > 16_384) throw new QaPreparationError("prepare", [], "execution_metadata_limit_exceeded"); return safe; } /** * What a failed `prepare` stage ran under, plus the end of its stderr; for an * in-place run that found the working directory different from the frozen * tree, the differing in-scope paths. It travels only in the direct Assurance * Tool result: it is not part of `QaCheckDiagnostic` and is never persisted * (no TaskRecord, finding, audit evidence, batch state or tracker projection). */ export type QaDeliveryConditions = | { delivery: "working_directory" } | { delivery: "isolated_materialization"; worktree_node_modules: "absent" }; export interface QaPrepareObservation { conditions: QaDeliveryConditions; declared_timeout_ms: number; stderr_tail: string; drift_paths?: string[]; } export const QA_DRIFT_PATH_LIMIT = 64; export const QA_PREPARE_STDERR_TAIL_BYTES = 2048; export const QA_PREPARE_REPRODUCTION_NOTE = "a reproduction in the live worktree is not equivalent: this run used an isolated delivery with no worktree node_modules"; export const QA_DRIFT_NOTE = "in-scope tracked content of the working directory differs from the frozen snapshot; see prepare_observation.drift_paths"; const QA_PREPARE_REASONS: QaPreparationReason[] = ["timeout", "nonzero_exit", "output_limit"]; // Control characters other than newline and tab: C0, DEL and C1. const QA_TAIL_CONTROL = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g; /** The last bytes of stderr, whole UTF-8 characters only, without terminal control sequences. */ export function qaPrepareStderrTail(stderr: string): string { const bytes = Buffer.from(stderr, "utf8"); let start = Math.max(0, bytes.length - QA_PREPARE_STDERR_TAIL_BYTES); // Do not begin inside a multi-byte character. while (start < bytes.length && (bytes[start]! & 0xc0) === 0x80) start++; return bytes.subarray(start).toString("utf8").replace(QA_TAIL_CONTROL, ""); } export function qaPrepareObservation(declaredTimeoutMs: number, stderr: string, delivery: QaDeliveryConditions["delivery"] = "working_directory", driftPaths?: string[]): QaPrepareObservation { return { conditions: delivery === "isolated_materialization" ? { delivery, worktree_node_modules: "absent" } : { delivery: "working_directory" }, declared_timeout_ms: declaredTimeoutMs, stderr_tail: qaPrepareStderrTail(stderr), ...(driftPaths ? { drift_paths: driftPaths.slice(0, QA_DRIFT_PATH_LIMIT).map(path => path.replace(QA_TAIL_CONTROL, "").slice(0, 512)) } : {}), }; } /** The Tool boundary never spreads arbitrary executor fields. */ export function qaPrepareObservationMetadata(o: QaPrepareObservation): QaPrepareObservation { const delivery = o.conditions?.delivery; if (!Number.isSafeInteger(o.declared_timeout_ms) || o.declared_timeout_ms < 0 || typeof o.stderr_tail !== "string" || (delivery !== "working_directory" && delivery !== "isolated_materialization") || (o.drift_paths !== undefined && (!Array.isArray(o.drift_paths) || o.drift_paths.some(path => typeof path !== "string")))) throw new Error("invalid QA prepare observation"); return qaPrepareObservation(o.declared_timeout_ms, o.stderr_tail, delivery, o.drift_paths); } export interface QaVerificationProgressInput { index: number; total: number; acceptance_id: string; phase: "running" | "passed" | "failed"; elapsed_ms: number; diagnostic?: QaCheckDiagnostic; } export class QaPreparationError extends Error { /** Present for a `prepare` stage ending in timeout, nonzero exit or output limit, and for in-place drift. */ public readonly prepare_observation?: QaPrepareObservation; constructor(public readonly stage: QaCheckDiagnostic["stage"], public readonly acceptance_ids: string[], public readonly reason: QaPreparationReason, public readonly diagnostics: QaCheckDiagnostic[] = [], prepareObservation?: QaPrepareObservation) { if (!QA_STAGES.includes(stage) || !QA_OUTCOMES.includes(reason)) throw new Error("invalid QA preparation classification"); const safe = diagnostics.map(qaDiagnosticMetadata); if (Buffer.byteLength(JSON.stringify(safe)) > 16_384) throw new QaPreparationError("prepare", [], "execution_metadata_limit_exceeded"); super(`QA ${stage} failed (${reason}); affected checks=${acceptance_ids.length}`); this.diagnostics = safe; if (prepareObservation && ((stage === "prepare" && QA_PREPARE_REASONS.includes(reason)) || (reason === "protected_input_or_output_drift" && prepareObservation.drift_paths))) this.prepare_observation = qaPrepareObservationMetadata(prepareObservation); this.name = "QaPreparationError"; } } export type QaVerificationProgress = QaVerificationProgressInput; export interface AssuranceRecovery { category: "repair" | "environment" | "authorization"; task_id: string; run_id: string | null; record_revision: string; next_obligation: AssuranceProjectionResult["projection"]["next_obligation"]; acceptance_ids: string[]; finding_ids: string[]; next_action: string; } export interface StatusProjectionWarning { code: "unstaged_scope_paths" | "out_of_scope_paths" | "invalid_git_integration" | "implementation_pending" | PluginSourceSkewWarning["code"]; paths: string[]; message: string; } export type StatusProjectionResult = AssuranceProjectionResult & { /** Parent-slug batch evidence, independent of the TaskRecord projection; never execution authority. */ batch?: NonNullable; /** The resolved `Git integration` preference (ADR 0024); set by the status read only. */ git_integration?: { mode: GitIntegrationMode; source: GitIntegrationPreference["source"] }; warnings?: StatusProjectionWarning[]; }; /** * The read-only status projection both Hosts return from their status Tool. * Unstaged or untracked in-scope paths, out-of-scope staged or dirty paths, and * a loaded plugin build that differs * from this workspace's own plugin source, and an invalid `Git integration` * value, come back as warnings beside the * projection instead of replacing it. Every path that freezes a snapshot or * binds authority keeps projecting through the refusing delivery identity. */ export async function projectTaskStatus(root: string, taskId: string, loadedFrom?: string): Promise { let unstaged: string[] = []; let outOfScope: string[] = []; let changed: string[] = []; const result = await projectAssurance(root, taskId, (dir, record) => { const identity = taskStatusIdentity(dir, record); changed = identity.changed_paths; unstaged = identity.unstaged_scope_paths; outOfScope = identity.out_of_scope_paths; return identity; }); const warnings: StatusProjectionWarning[] = []; if (!result.error && result.projection.lifecycle === "active" && result.projection.artifact_state === "active" && result.projection.next_obligation === "submit_assurance" && changed.length === 0 && unstaged.length === 0 && outOfScope.length === 0) warnings.push({ code: "implementation_pending", paths: [], message: "fresh enrollment: submit_assurance means implement first; implement the enrolled acceptance, run focused checks, stage task-owned changes, then call advance_assurance", }); if (!result.error && unstaged.length > 0) warnings.push({ code: "unstaged_scope_paths", paths: unstaged, message: `task scope contains unstaged or untracked changes: ${unstaged.join(", ")}; advance_assurance refuses until they are staged or removed`, }); if (!result.error && outOfScope.length > 0) warnings.push({ code: "out_of_scope_paths", paths: outOfScope, message: `paths outside the task scope are staged or changed: ${outOfScope.join(", ")}; they are not part of this task's identity, and a frozen Review or QA snapshot refuses them until they are unstaged or reverted`, }); // Reported beside errors too: a stale loaded build is the likely cause of one. const skew = pluginSourceSkewWarning(root, loadedFrom); if (skew) warnings.push(skew); const preference = readGitIntegrationPreference(root); if (preference.diagnostic) warnings.push({ code: "invalid_git_integration", paths: preference.source ? [preference.source] : [], message: preference.diagnostic }); const git_integration = { mode: preference.mode, source: preference.source }; let batch = findExistingActiveBatch(root, taskId); if (batch === null) { const settled = findSettledBatchRecord(root, taskId); if (settled) { const record = readAnyBatchRunState(root, settled.batch_id); if (record) batch = { corrupt: false, record }; } } const status = { ...result, git_integration, ...(batch ? { batch } : {}) }; return warnings.length === 0 ? status : { ...status, warnings }; } /** Observation only; fresh Kernel facts remain the sole owner of the obligation. */ export function deriveAssuranceRecovery(taskId: string, fresh: AssuranceProjectionResult, result: { state?: string; environment_failure?: boolean; diagnostics?: QaCheckDiagnostic[]; reason?: string }, findings: NonNullable["findings"] = []): AssuranceRecovery | null { if (fresh.error || fresh.claim?.task_id !== taskId || fresh.projection.lifecycle !== "active") return null; const p = fresh.projection; const findingIds = [...p.blocking_finding_ids, ...p.unresolved_user_decision_ids, ...p.replan_required_ids]; const scope = result.reason?.includes("task delivery contains paths outside the authorization envelope"); const migration = result.reason?.includes("verification_contract_migration_required"); const unstaged = result.reason?.includes("task delivery has unstaged or untracked changes"); const reviewPreparation = result.state === "review_preparation_failed" && p.next_obligation === "run_review"; const authority = scope || migration || p.next_obligation === "resolve_user_decision" || p.next_obligation === "revise_intent" || p.unresolved_user_decision_ids.length > 0 || p.replan_required_ids.length > 0 || p.authorization.state !== "none"; const repair = unstaged || p.next_obligation === "resolve_findings" || p.blocking_finding_ids.length > 0; if (!authority && !repair && !result.environment_failure && !reviewPreparation) return null; const affected = findings.filter(f => f.status === "open" && f.id && findingIds.includes(f.id) && f.acceptance_id).map(f => f.acceptance_id!); const category = authority ? "authorization" : repair ? "repair" : "environment"; return { category, task_id: taskId, run_id: p.run_id ?? null, record_revision: p.record_revision, next_obligation: p.next_obligation, finding_ids: findingIds, acceptance_ids: [...new Set(affected.length ? affected : result.diagnostics?.length ? result.diagnostics.map(d => d.acceptance_id) : reviewPreparation ? p.fresh_acceptance_ids : p.missing_acceptance_ids)], next_action: scope ? "Reconcile the listed paths with the TaskIntent scope: unstage unrelated changes or submit the required native Intent revision." : migration ? "route Planner for the verification descriptor v2 Intent revision and native gate" : category === "authorization" ? p.authorization.blocked ? "inspect authority state" : p.next_obligation === "revise_intent" ? "route Planner for the required Intent revision and native gate" : "request_authorization" : unstaged ? "Stage only the scoped implementation changes, then call advance_assurance." : category === "repair" ? "Route foreground Executor to repair and verify the identified findings; dispose each by resolve_finding or evidence-bound refute_finding, then run fresh advance_assurance." : reviewPreparation ? "Repair the Review evidence environment, then call advance_assurance to resume run_review; retain fresh QA and do not re-confirm the batch." : "Repair the identified verification environment, then call advance_assurance; a local check is non-attesting.", }; } export interface ForegroundToolUpdate { content: Array<{ type: "text"; text: string }>; details: Record; } /** Wall-clock milliseconds spent per advance stage; the stage names are the progress stages. */ export interface AssuranceTimings { total_ms: number; stage_ms: Record } type AssuranceRecoveryFields = { timings?: AssuranceTimings; diagnostics?: QaCheckDiagnostic[]; /** Direct Tool result only; never persisted (see `QaPrepareObservation`). */ prepare_observation?: QaPrepareObservation; environment_failure?: boolean; recovery?: AssuranceRecovery; recovery_error?: "fresh_kernel_projection_unavailable"; }; export type AssuranceAdvanceResult = AssuranceRecoveryFields & ( | ReviewIncompleteResult | { state: "review_ready"; operation: "review"; operation_id: string; snapshot_digest: string; review_bundle_digest: string; agent_params: unknown } | { state: "rework"; operation: "qa"; operation_id: string; summary: string } | { state: "cancelled"; operation: "qa" | "review"; operation_id: string; reason: string } | { state: "failed"; operation: "qa" | "review"; operation_id: string; reason: string } /** * Read-only Review evidence preparation failed after QA authority settled. * The Kernel obligation stays `run_review`, nothing was lost, and the same * deterministic revision can be rebuilt on the next advance. */ | { state: "review_preparation_failed"; operation: "review"; operation_id: string; reason: string } | { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string } | { state: "blocked"; reason: string; code?: "verdict_invalid"; recovery_action?: string } | { state: "completed" } | { state: "stopped" }); /** * D7: a settled Task is delivered by its scoped content, not by a commit. A * Host's completion report names that content identity and any auto settle commit. */ export interface AssuranceDelivery { content_hash: string; note: string; } export function deliveryOf(diffHash: string, settleCommit?: string): AssuranceDelivery { return { content_hash: diffHash, note: settleCommit ? `Scoped content hash ${diffHash} was committed in settle commit ${settleCommit} (Git integration: auto).` : `Changes are in the working tree with scoped content hash ${diffHash}; commit however you prefer.`, }; } export type AssuranceSubmitReviewResult = AssuranceRecoveryFields & ( | ReviewIncompleteResult | { state: "rework"; operation: "review"; operation_id: string; summary: string } | { state: "review_preparation_failed"; operation: "review"; operation_id: string; reason: string } | { state: "completed" } | { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string } | { state: "blocked"; reason: string; code?: "verdict_invalid"; recovery_action?: string }); /** * ADR 0017: optional metadata a host adapter supplies alongside the verdict * input. The coordinator threads it opaquely; it never resolves verdict sources. */ export interface ReviewSubmissionOptions { /** sha256 over the reviewer's own result bytes, bound into the review attestation. */ reviewer_verdict_sha256?: string; /** Synchronous host check immediately before the reservation is consumed. */ validateReceipt?: () => { reason: string; release: boolean; recovery_action: string } | null; } export type ActiveAssuranceState = | { state: "running"; operation: "qa"; operation_id: string; deadline_seconds: number } | { state: "review_ready"; operation: "review"; operation_id: string } | { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string }; export interface AssuranceCoordinatorPorts { host: AssuranceHostPort; confirmationReference: ConfirmationReferenceSource; onReworkApplied?: (ctx: HostContext, taskId: string, findingsCount: number) => void; projectTask(root: string, taskId: string): Promise; readTaskRecord(root: string, taskId: string): Promise; readTaskIntent(root: string, taskId: string): Promise; runQa( snapshot: SnapshotDescriptor, descriptors: Map, options: { signal?: AbortSignal; onProgress?: (progress: QaVerificationProgress) => void }, ): Promise; writeReviewEvidence(input: { snapshot: SnapshotDescriptor; evidence: ReviewBundle | ReviewManifestV5 }): ReviewEvidence; applyOrdinaryOperation(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; snapshot?: AssuranceCorrelation & { workspace_revision: string }; turn_budget?: number; review_contract_hash?: string; inspected_paths?: string[]; reason?: string } }): Promise; advanceBeforeProjection?: () => Promise; qaBeforeProjection?: () => Promise; qaBeforeAuthorityCommit?: () => Promise; qaOnAuthorityCommit?: () => void; qaAfterAuthorityCommit?: () => Promise; qaJobTimeoutMs?: number; /** Attach per-stage `timings` to advance results. Defaults to IMM_ASSURANCE_TIMINGS=1. */ reportTimings?: boolean; /** Overrides QA_MAX_IDENTICAL_FAILURES; tests only. */ qaMaxIdenticalFailures?: number; } export const QA_MIN_JOB_TIMEOUT_SECONDS = 15 * 60; export const QA_MAX_JOB_TIMEOUT_SECONDS = 60 * 60; export const QA_JOB_OVERHEAD_SECONDS = 2 * 60; export const QA_JOB_TIMEOUT_SECONDS = QA_MIN_JOB_TIMEOUT_SECONDS; /** * A QA run that fails before attestation writes no authority, so the Kernel * would happily rerun the same descriptors forever. After this many identical * failures on the same snapshot (record revision, intent hash, diff hash) the * coordinator refuses to rerun until the snapshot changes. */ export const QA_MAX_IDENTICAL_FAILURES = 2; export const REVIEW_PREPARATION_TIMEOUT_MS = 30_000; export const REVIEW_DISPATCH_TIMEOUT_MS = 120_000; export const REVIEW_VERDICT_VALIDATION_TIMEOUT_MS = 30_000; export const ASSURANCE_STALL_MS = 30_000; type ReviewWorkload = "quick" | "standard" | "heavy"; export interface ReviewTimingProfile { softDeadlineSeconds: number; stopThresholdSeconds: number } export const REVIEW_TIMING_PROFILES: Readonly> = { quick: { softDeadlineSeconds: 5 * 60, stopThresholdSeconds: 15 * 60 }, standard: { softDeadlineSeconds: 10 * 60, stopThresholdSeconds: 30 * 60 }, heavy: { softDeadlineSeconds: 20 * 60, stopThresholdSeconds: 60 * 60 }, }; function declaredQaJobTimeoutMs(descriptors: Iterable): number { let declaredMs = 0; const prepared = new Set(); for (const descriptor of descriptors) { declaredMs += descriptor.command.timeout_ms; const key = JSON.stringify(descriptor.environment); if (!prepared.has(key)) declaredMs += descriptor.environment.prepare?.timeout_ms ?? 0; prepared.add(key); } return Math.max(QA_MIN_JOB_TIMEOUT_SECONDS * 1000, declaredMs + QA_JOB_OVERHEAD_SECONDS * 1000); } export function deriveQaJobTimeoutMs(descriptors: Iterable): number { const derivedMs = declaredQaJobTimeoutMs(descriptors); if (derivedMs > QA_MAX_JOB_TIMEOUT_SECONDS * 1000) throw new Error(`declared QA budget ${Math.ceil(derivedMs / 60_000)} minutes exceeds the maximum of 60 minutes`); return derivedMs; } const QUICK_REVIEW_MAX_ACCEPTANCE = 3; const QUICK_REVIEW_MAX_FILES = 5; const QUICK_REVIEW_MAX_BYTES = 64 * 1024; const HEAVY_REVIEW_MIN_ACCEPTANCE = 9; const HEAVY_REVIEW_MIN_BYTES = 512 * 1024 + 1; const HEAVY_REVIEW_MIN_FILES = 12; export function classifyReviewWorkload(snapshot: Pick, evidence: ReviewBundle | ReviewManifestV5): ReviewWorkload { const bytes = Buffer.byteLength(JSON.stringify(evidence)); const paths = reviewChangedPathCount(evidence); if (snapshot.risk === "critical" || snapshot.acceptance.length >= HEAVY_REVIEW_MIN_ACCEPTANCE || paths >= HEAVY_REVIEW_MIN_FILES || bytes >= HEAVY_REVIEW_MIN_BYTES) return "heavy"; if (snapshot.risk === "routine" && snapshot.acceptance.length <= QUICK_REVIEW_MAX_ACCEPTANCE && paths <= QUICK_REVIEW_MAX_FILES && bytes <= QUICK_REVIEW_MAX_BYTES) return "quick"; return "standard"; } export function reviewChangedPathCount(evidence: ReviewBundle | ReviewManifestV5): number { return "changed_paths" in evidence ? Object.keys(evidence.changed_paths).length : Object.keys(evidence.dirty_files).length; } const HEAVY_REVIEW_BASE_TURNS = 12; const HEAVY_REVIEW_MIN_TURNS = 24; const HEAVY_REVIEW_MAX_TURNS = 48; /** A heavy Review gets one extra turn per changed path, so a wide change is read in full. */ export function reviewTurnBudget(workload: ReviewWorkload, changedPaths = 0, previousBudget: number | null = null): number { if (previousBudget !== null) return Math.min(96, previousBudget * 2); if (workload === "quick") return 12; if (workload === "standard") return 16; return Math.min(HEAVY_REVIEW_MAX_TURNS, Math.max(HEAVY_REVIEW_MIN_TURNS, HEAVY_REVIEW_BASE_TURNS + changedPaths)); } export function snapshotDigest(snapshot: SnapshotDescriptor): string { return `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`; } /** * The per-dispatch Review facts: which evidence to read, which revision it pins, * what the acceptances assert, and the exact verdict shapes. * * This is the whole prompt for a Host whose reviewer definition already carries * the role contract and `STATIC_REVIEW_RULES` — the Claude Host. A Host whose * dispatched agent carries no instructions (Pi) gets `buildReviewPrompt`, which * prepends both. */ function reviewDeltaLines(delta: SnapshotDescriptor["review_delta"]): string[] { if (!delta) return []; if ("skipped" in delta) return [`Review round ${delta.round}: no rework delta is available (${delta.skipped}); the delta admission rule does not apply to this round.`]; return [ `Review round ${delta.round}: review only the latest rework delta and its direct interactions. Prior reviewed commit ${delta.prior_review_commit}; delta_paths: ${delta.delta_paths.length ? delta.delta_paths.join(", ") : "(none)"}. A blocking finding must have a caller_chain path in delta_paths or violated.kind "security_boundary"; otherwise it is advisory on a pass verdict.`, `Changed paths outside delta_paths were inspected by earlier rounds at ${delta.prior_review_commit}; list them in approval.inspected_paths by carry-forward without re-reading their full diff. An unread or truncated read is not a defect and is never a blocking finding.`, ]; } export function buildReviewSnapshotPrompt( snapshot: SnapshotDescriptor, evidencePath?: string, turnBudget?: number, ): string { if (snapshot.role !== "review") throw new Error("native review prompt requires review role"); const acceptance = snapshot.acceptance.map((item) => `- ${item.id}: ${item.assertion}`).join("\n"); const digest = snapshotDigest(snapshot); const revision = snapshot.review_revision; const evidenceContract = revision ? [ `Review evidence contract: assurance_kernel/review_manifest/v5. The manifest is metadata only; source is read from immutable Git objects.`, `Review one immutable Git revision, not live workspace bytes. Read the metadata manifest at ${evidencePath ?? ""} first; it carries no source. Verify that git rev-parse ${revision.base_head} and git rev-parse ${revision.review_commit} both resolve, that ${revision.review_commit}^{} is a commit whose only parent is ${revision.base_head}, and that its tree is ${revision.review_tree}.`, `Analyze the change with git diff ${revision.base_head} ${revision.review_commit} (and git show ${revision.review_commit}: for full files). Every path in changed_paths is the task's work: added, modified, or deleted since Enrollment. Deleted paths have a null oid. Never treat a path that is absent from ${revision.base_head} as pre-existing, and never review files outside the revision.`, `Unchanged files are not part of the mutation authority. Read one only when it is directly required by an acceptance assertion, a changed caller, or the same state machine, and cite the path plus the reason in your finding. Repository-wide exploration is out of scope.`, `The user-selected worktree may contain staged or committed work that is not in this revision, and revision objects may not be checked out anywhere. Do not read working-tree files as evidence; git object reads against the shared object database are the only source of truth.`, ] : [ `Verify immutable bundle provenance before analyzing findings. Review the immutable evidence JSON at ${evidencePath ?? ""}. Read that file first; verify that git rev-parse HEAD in the isolated reviewer worktree equals bundle.head. For every tracked dirty_files entry, verify git rev-parse HEAD: equals base_oid, then compare that immutable HEAD blob with current_content. A null base_oid denotes an untracked current file; a null current_content denotes a deletion. Do not inspect or depend on live task bytes outside the immutable bundle.`, `Limit repository inspection to the acceptance assertions and dirty_files contents in the immutable bundle. Do not explore unrelated repository paths.`, `The user-selected worktree may contain staged task changes that are absent from the isolated reviewer worktree. Review authority is bound only to the bundle dirty_files current_content bytes and committed HEAD provenance. Analyze code exclusively from those bundle bytes; repository file reads are permitted only for the provenance git commands above. A symbol present in current_content but absent from HEAD is the task change, not an absence.`, ]; return [ ...evidenceContract, ...(turnBudget === undefined ? [] : [`Review turn budget: ${turnBudget}. Reserve the final turn for the structured result; return incomplete if coverage cannot finish within this budget.`]), `Snapshot digest: ${digest}`, `TaskRecord revision: ${snapshot.record_revision}`, revision ? `Intent revision ${snapshot.intent_revision} (hash ${snapshot.intent_content_hash}), diff ${snapshot.diff_hash}, review revision ${revision.review_commit} (base ${revision.base_head}, tree ${revision.review_tree}, manifest ${revision.manifest_digest}), state ${snapshot.lifecycle}:${snapshot.artifact_state}.` : `Intent revision ${snapshot.intent_revision} (hash ${snapshot.intent_content_hash}), diff ${snapshot.diff_hash}, review bundle ${snapshot.review_bundle_digest}, state ${snapshot.lifecycle}:${snapshot.artifact_state}.`, ...reviewDeltaLines(snapshot.review_delta), "Acceptance assertions:", acceptance, `PASS shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"pass","approval":{"kind":"review","authority_role":"reviewer","summary":"","inspected_paths":[""]}}`, `A pass verdict may carry non-blocking notes as findings, but every one of them must set kind "advisory"; a blocking finding is a rework verdict and must omit approval: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"pass","approval":{"kind":"review","authority_role":"reviewer","summary":"","inspected_paths":[""]},"findings":[{"id":"review-1","kind":"advisory","acceptance_id":"","summary":"","evidence":{"trigger":"","caller_chain":[""],"violated":{"kind":"acceptance|security_boundary","ref":""}}}]}`, `REWORK shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"rework","findings":[{"id":"review-1","kind":"blocking","acceptance_id":"","summary":"","evidence":{"trigger":"","caller_chain":[""],"violated":{"kind":"acceptance|security_boundary","ref":""}}}]}`, `INCOMPLETE shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"incomplete","inspected_paths":[""],"reason":""}. If coverage cannot finish within the turn budget, return incomplete, never a coverage finding. Omit approval and findings entirely; inspected_paths must be a unique subset of changed paths.`, `REWORK verdicts must omit the approval field entirely; do not emit "approval": null. A rework carries only blocking findings, at least one; a note that blocks nothing is an advisory finding on a pass verdict, never on rework.`, ].join("\n"); } /** * The complete Review prompt: the internal role contract, the static review * rules, and the per-dispatch facts. Dispatched by a Host whose agent carries no * instructions of its own. */ export function buildReviewPrompt( snapshot: SnapshotDescriptor, evidencePath?: string, turnBudget?: number, ): string { if (snapshot.role !== "review") throw new Error("native review prompt requires review role"); const digest = snapshotDigest(snapshot); const rolePacket = buildRoleDelegationPacket({ role: "code-review", context: { task_id: snapshot.task_id, review_gate: "imm-code-review", changed_files_signature: snapshot.diff_hash, snapshot_digest: digest, }, }); return [ rolePacket.prompt, ...STATIC_REVIEW_RULES, buildReviewSnapshotPrompt(snapshot, evidencePath, turnBudget), ].join("\n"); } function parseVerdictEvidence(value: unknown, index: number): FindingEvidence { if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`finding ${index} evidence is required`); const evidence = value as Record; const unknown = Object.keys(evidence).find( (key) => !["trigger", "caller_chain", "violated"].includes(key), ); if (unknown) throw new Error(`finding ${index} evidence has unknown field: ${unknown}`); if (typeof evidence.trigger !== "string" || !evidence.trigger.trim()) throw new Error(`finding ${index} evidence.trigger must be a non-empty string`); const chain = evidence.caller_chain; if ( !Array.isArray(chain) || chain.length === 0 || chain.some((entry) => typeof entry !== "string" || !entry.trim()) ) throw new Error( `finding ${index} evidence.caller_chain must be a non-empty list of paths or symbols`, ); const violated = evidence.violated; if (!violated || typeof violated !== "object" || Array.isArray(violated)) throw new Error(`finding ${index} evidence.violated is required`); const violatedRecord = violated as Record; const unknownViolated = Object.keys(violatedRecord).find( (key) => !["kind", "ref"].includes(key), ); if (unknownViolated) throw new Error(`finding ${index} evidence.violated has unknown field: ${unknownViolated}`); if (violatedRecord.kind !== "acceptance" && violatedRecord.kind !== "security_boundary") throw new Error( `finding ${index} evidence.violated.kind must be acceptance or security_boundary`, ); if (typeof violatedRecord.ref !== "string" || !violatedRecord.ref.trim()) throw new Error(`finding ${index} evidence.violated.ref must be a non-empty string`); return { trigger: evidence.trigger, caller_chain: chain as string[], violated: { kind: violatedRecord.kind, ref: violatedRecord.ref, }, }; } function readChildVerdict(input: unknown): Record { let raw: Record; if (typeof input === "string") { // Agent transport prefixes a status line to the strict JSON verdict. const first = input.indexOf("{"); const last = input.lastIndexOf("}"); const cleaned = first >= 0 && last >= first ? input.slice(first, last + 1) : ""; if (!cleaned) throw new Error("reviewer returned no strict JSON verdict"); try { raw = JSON.parse(cleaned) as Record; } catch { throw new Error("reviewer verdict is not valid JSON"); } } else if (typeof input === "object" && input !== null && !Array.isArray(input)) { raw = input as Record; } else { throw new Error("reviewer verdict must be a JSON object"); } return raw; } export function parseChildVerdict(input: unknown, snapshot: SnapshotDescriptor): ChildReviewResult { const raw = readChildVerdict(input); if (raw.decision !== "incomplete") return parseAssuranceVerdict(raw, snapshot); if (snapshot.role !== "review" || raw.role !== "review") throw new Error("incomplete is only valid for Review"); if (raw.contract !== "assurance_kernel/assurance_verdict/v2" || raw.task_id !== snapshot.task_id || raw.snapshot_digest !== snapshotDigest(snapshot)) throw new Error("incomplete snapshot identity mismatch"); const unknown = Object.keys(raw).find(key => !["contract", "role", "task_id", "snapshot_digest", "decision", "inspected_paths", "reason"].includes(key)); if (unknown) throw new Error(`incomplete has unknown field: ${unknown}`); if (typeof raw.reason !== "string" || !raw.reason.trim() || /[\r\n\u2028\u2029]/.test(raw.reason)) throw new Error("incomplete reason must be a non-empty single line"); const paths = raw.inspected_paths; if (!Array.isArray(paths) || paths.some(path => typeof path !== "string" || !snapshot.dirty_files.includes(path)) || new Set(paths).size !== paths.length) throw new Error("incomplete inspected_paths must be a unique subset of changed paths"); return { contract: "assurance_kernel/assurance_verdict/v2", role: "review", task_id: snapshot.task_id, snapshot_digest: snapshotDigest(snapshot), decision: "incomplete", inspected_paths: paths, reason: raw.reason }; } export function parseAssuranceVerdict(input: unknown, snapshot: SnapshotDescriptor): AssuranceVerdict { const raw = readChildVerdict(input); const allowed = ["contract", "role", "task_id", "snapshot_digest", "decision", "approval", "findings"]; const unknown = Object.keys(raw).find((key) => !allowed.includes(key)); if (unknown) throw new Error(`child verdict has unknown field: ${unknown}`); if (raw.contract !== "assurance_kernel/assurance_verdict/v2") throw new Error("assurance verdict contract is invalid"); if (raw.role !== snapshot.role) throw new Error("child verdict role mismatch"); if (raw.task_id !== snapshot.task_id) throw new Error("child verdict task mismatch"); if (raw.snapshot_digest !== snapshotDigest(snapshot)) throw new Error("child verdict snapshot digest mismatch"); if (raw.decision !== "pass" && raw.decision !== "rework") throw new Error("child verdict decision must be pass or rework"); if (raw.decision === "pass") { const approval = raw.approval as Record | undefined; const expectedKind = snapshot.role === "qa" ? "qa" : "review"; const expectedRole = snapshot.role === "qa" ? "qa" : "reviewer"; if (!approval || approval.kind !== expectedKind || approval.authority_role !== expectedRole || typeof approval.summary !== "string" || !approval.summary.trim()) throw new Error("pass verdict approval is invalid"); // A Review pass must claim the paths it inspected: BR-DEC-3 converts silent // partial coverage into an explicit false statement. The list is checked // here against the reviewed change set and removed before settlement, so // the Kernel approval and TaskRecord never see it. QA verdicts treat the // field as unknown, exactly as before. if (snapshot.role === "review") { const required = snapshot.dirty_files; const listed = approval.inspected_paths; if (listed === undefined) throw new Error(`review pass verdict approval.inspected_paths is required; it must list every reviewed changed path${required.length ? `: ${required.join(", ")}` : " (none for this change set)"}`); if (!Array.isArray(listed) || listed.some((entry) => typeof entry !== "string" || !entry.trim())) throw new Error("review pass verdict approval.inspected_paths must be an array of repository-relative path strings"); const seen = new Set(); for (const entry of listed as string[]) { if (seen.has(entry)) throw new Error(`review pass verdict approval.inspected_paths lists a duplicate path: ${entry}`); seen.add(entry); } const foreign = (listed as string[]).filter((entry) => !required.includes(entry)); if (foreign.length) throw new Error(`review pass verdict approval.inspected_paths lists a path outside the reviewed change set: ${foreign.join(", ")}`); const missing = required.filter((entry) => !seen.has(entry)); if (missing.length) throw new Error(`review pass verdict approval.inspected_paths omits reviewed changed paths: ${missing.join(", ")}`); } const allowedApproval = snapshot.role === "review" ? ["kind", "authority_role", "summary", "inspected_paths"] : ["kind", "authority_role", "summary"]; const unknownApproval = Object.keys(approval).find((key) => !allowedApproval.includes(key)); if (unknownApproval) throw new Error(`pass verdict approval has unknown field: ${unknownApproval}`); const passFindings = raw.findings === undefined ? [] : parseVerdictFindings(raw.findings as unknown[], snapshot); if (passFindings.some((finding) => finding.kind !== "advisory")) throw new Error("pass verdict findings must all be advisory"); return { contract: "assurance_kernel/assurance_verdict/v2", role: snapshot.role, task_id: snapshot.task_id, snapshot_digest: snapshotDigest(snapshot), decision: "pass", approval: { kind: expectedKind, authority_role: expectedRole, summary: approval.summary }, ...(passFindings.length > 0 ? { findings: passFindings } : {}), }; } if (!Array.isArray(raw.findings) || raw.findings.length === 0) throw new Error("rework verdict findings are invalid"); if (raw.approval !== undefined && raw.approval !== null) throw new Error("rework verdict must omit approval"); const findings = parseVerdictFindings(raw.findings, snapshot); // The verdict bytes stay the Reviewer's: a Review rework that mixes in // advisory notes, or blocks on nothing, is rejected for resubmission and // never demoted, filtered or synthesized here. if (snapshot.role === "review") { const outOfDelta = outOfDeltaBlockingFindings(findings, snapshot); if (outOfDelta.length > 0) throw new Error(`review round ${snapshot.review_delta!.round} admits a blocking finding only when a caller_chain path is in the rework delta or it is a security_boundary violation; finding(s) ${outOfDelta.map((finding) => finding.id).join(", ")} are outside the delta (${(snapshot.review_delta as { delta_paths: string[] }).delta_paths.join(", ") || "no changed paths"}); resubmit each as an advisory finding on a pass verdict, or justify it with violated.kind "security_boundary"`); const advisory = findings.filter((finding) => finding.kind === "advisory"); if (advisory.length > 0 || !findings.some((finding) => finding.kind === "blocking")) throw new Error(`review rework verdict must contain only blocking findings and at least one: ${advisory.length > 0 ? `advisory finding(s) ${advisory.map((finding) => finding.id).join(", ")} are not allowed on rework; ` : "it has no blocking finding; "}resubmit decision "pass" with approval.inspected_paths listing every reviewed changed path when nothing blocks, carrying any non-blocking notes as advisory findings`); } return { contract: "assurance_kernel/assurance_verdict/v2", role: snapshot.role, task_id: snapshot.task_id, snapshot_digest: snapshotDigest(snapshot), decision: "rework", findings }; } /** * Review round 3 and later admits a blocking finding only when it touches the * latest rework delta or crosses a security boundary. The rule is skipped, never * blocking, when no delta was computed for the round. */ function outOfDeltaBlockingFindings( findings: ReturnType, snapshot: SnapshotDescriptor, ): Array<{ id: string }> { const delta = snapshot.review_delta; if (!delta || !("delta_paths" in delta)) return []; const changed = new Set(delta.delta_paths); return findings.filter((finding) => { if (finding.kind !== "blocking") return false; const evidence = (finding as { evidence?: { violated: { kind: string }; caller_chain: string[] } }).evidence; if (!evidence) return false; if (evidence.violated.kind === "security_boundary") return false; return !evidence.caller_chain.some((entry) => changed.has(callerChainPath(entry))); }); } /** The path component of a caller_chain entry: drops a `:line`, `#symbol` or `(` suffix. */ function callerChainPath(entry: string): string { return entry.trim().replace(/^\.\//, "").split(/[:#(\s]/, 1)[0] ?? ""; } /** * Parse one verdict's findings. Review findings carry evidence and derive their * anchor here so a capability minted for one anchor set cannot be spent on * another; QA findings stay identifier-only. */ function parseVerdictFindings(rawFindings: unknown[], snapshot: SnapshotDescriptor) { return rawFindings.map((item, index) => { const finding = item as Record; const allowedFindingKeys = snapshot.role === "review" ? ["id", "kind", "acceptance_id", "summary", "evidence"] : ["id", "kind", "acceptance_id", "summary"]; const unknownFinding = Object.keys(finding).find( (key) => !allowedFindingKeys.includes(key), ); if (unknownFinding) throw new Error(`finding ${index} has unknown field: ${unknownFinding}`); if (typeof finding.id !== "string" || !finding.id.trim() || (finding.kind !== "blocking" && finding.kind !== "advisory") || (finding.acceptance_id !== null && typeof finding.acceptance_id !== "string") || typeof finding.summary !== "string" || !finding.summary.trim()) throw new Error(`finding ${index} is invalid`); const id = `review-${snapshotDigest(snapshot).slice(7, 19)}-${index + 1}-${finding.id.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 48)}`; const normalized = { id, kind: finding.kind as "blocking" | "advisory", acceptance_id: finding.acceptance_id as string | null, summary: finding.summary as string }; if (snapshot.role !== "review") return { ...normalized, findings_digest: findingsDigest([normalized]) }; // Review findings must carry evidence; the anchor is derived here so a // capability minted for one anchor set cannot be spent on another and // the reviewer can never assert the anchor directly. const evidence = parseVerdictEvidence(finding.evidence, index); // The digest that decides refutation inheritance is derived here so the // reviewer can never assert the anchor directly; the Kernel recomputes it // from the same evidence before admitting the finding. const anchor = anchorForEvidence(evidence); return { ...normalized, anchor, evidence, findings_digest: findingsDigest([normalized]), }; }); } /** * The five snapshot fields one Review reservation binds. A reservation is only * reusable while all five still match the live projection; each call site adds * its own conditions on top of this comparison. */ export interface ReservationSnapshot { record_revision: string; workspace_revision: string; intent_revision: number; intent_content_hash: string; diff_hash: string; } /** * Field names where the reserved snapshot differs from the live projection; * an empty result is an exact match on all five. BR-DEC-06 keeps the five * fields distinct, so this compares each one instead of a merged digest. */ export function compareReservationSnapshot( snapshot: ReservationSnapshot, current: ReservationSnapshot, ): string[] { const fields: Array = [ "record_revision", "workspace_revision", "intent_revision", "intent_content_hash", "diff_hash", ]; return fields.filter((field) => snapshot[field] !== current[field]); } /** * `advance()`'s complete Review-ready predicate: claim identity, an active task * still awaiting Review, and an exactly matching reservation snapshot. The * release-on-mismatch cleanup that follows stays with the caller, because it is * a side effect rather than part of this pure decision. */ export function reservationStillValid( reservation: { snapshot: ReservationSnapshot } | undefined, projection: AssuranceProjectionResult, taskId: string, ): boolean { if (projection.error) return false; if (projection.claim?.task_id !== taskId) return false; const current = projection.projection; if (current.lifecycle !== "active" || current.next_obligation !== "run_review") return false; if (!reservation) return false; return compareReservationSnapshot(reservation.snapshot, current).length === 0; } interface ReviewReservation { taskId: string; operationId: string; correlation: AssuranceCorrelation; snapshot: SnapshotDescriptor; hostReservation: HostReviewReservation; turnBudget: number; reviewContractHash: string; verdictCorrectionRequired: boolean; evidence: { path: string; remove(): void }; } export const invocationRegistry = createInvocationRegistry(); export class AssuranceCoordinator { private readonly activeOperations = new Map(); private readonly operationControllers = new Map(); private readonly reviewReservations = new Map(); private readonly rejectedReviewOperations = new Map(); private readonly qaFailureStreaks = new Map(); private readonly qaAttemptKeys = new Map(); private readonly unknownOperations = new Map(); private readonly sessionInvocations = new Set(); private sessionActive = true; private sessionGeneration = 0; readonly authority: VerdictAuthority; constructor(private readonly ports: AssuranceCoordinatorPorts, authorityOverrides: Partial = {}) { this.authority = { ...createVerdictAuthority({ confirmationReference: ports.confirmationReference, commitInvocation: token => this.commitInvocation(token), onReworkApplied: ports.onReworkApplied, }), ...authorityOverrides }; } onSessionStart(): void { this.sessionActive = true; this.sessionGeneration += 1; } async onSessionShutdown(): Promise { this.sessionActive = false; this.sessionGeneration += 1; for (const { controller } of this.operationControllers.values()) { if (!controller.signal.aborted) controller.abort(new Error("session shutdown")); } this.operationControllers.clear(); this.activeOperations.clear(); this.rejectedReviewOperations.clear(); for (const reservation of this.reviewReservations.values()) { try { this.ports.host.releaseReview(reservation.hostReservation); } catch { /* cleanup cannot create authority */ } this.removeEvidence(reservation); } this.reviewReservations.clear(); this.qaFailureStreaks.clear(); this.qaAttemptKeys.clear(); for (const invocation of [...this.sessionInvocations]) this.closeSessionInvocation(invocation); } active(taskId: string): ActiveAssuranceState | null { const operationId = this.activeOperations.get(taskId); if (operationId) return { state: "running", operation: "qa", operation_id: operationId, deadline_seconds: QA_JOB_TIMEOUT_SECONDS }; const unknown = this.unknownOperations.get(taskId); if (unknown) return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason }; const reservation = this.reviewReservations.get(taskId); if (reservation) return { state: "review_ready", operation: "review", operation_id: reservation.operationId }; return null; } openInvocation(taskId: string): InvocationToken { const invocation = invocationRegistry.open(taskId); this.sessionInvocations.add(invocation); return invocation; } closeInvocation(invocation: InvocationToken): void { this.closeSessionInvocation(invocation); } private closeSessionInvocation(invocation: InvocationToken): void { try { invocationRegistry.cancel(invocation); } catch { /* already terminal */ } this.sessionInvocations.delete(invocation); } commitInvocation(invocation: InvocationToken): void { invocationRegistry.commit(invocation); } invocationState(invocation: InvocationToken): InvocationState { return invocationRegistry.stateOf(invocation); } isInvocationOpen(taskId: string): boolean { return invocationRegistry.isOpen(taskId); } sessionActiveValue(): boolean { return this.sessionActive; } sessionGenerationValue(): number { return this.sessionGeneration; } async advance(taskId: string, ctx: HostContext, signal?: AbortSignal, onUpdate?: (update: ForegroundToolUpdate) => void): Promise { const checks = new Map(); const started = performance.now(); const stageMs: Record = {}; let stage = "preparing"; let stageStarted = started; const closeStage = (now: number) => { stageMs[stage] = (stageMs[stage] ?? 0) + Math.round(now - stageStarted); }; const result = await this.advanceOnce(taskId, ctx, signal, update => { const next = update.details.stage; if (typeof next === "string" && next !== stage) { const now = performance.now(); closeStage(now); stage = next; stageStarted = now; } const diagnostic = update.details.diagnostic as QaCheckDiagnostic | undefined; if (diagnostic) { checks.set(diagnostic.descriptor_ref, diagnostic); if (Buffer.byteLength(JSON.stringify([...checks.values()])) > 16_384) throw new QaPreparationError("prepare", [diagnostic.acceptance_id], "execution_metadata_limit_exceeded", [{ ...diagnostic, stage: "prepare", outcome: "execution_metadata_limit_exceeded" }]); } onUpdate?.(update); }); this.recordQaAttempt(taskId, result); const diagnostics = result.diagnostics ?? [...checks.values()]; const enriched = diagnostics.length ? { ...result, diagnostics } : result; if (!(this.ports.reportTimings ?? process.env.IMM_ASSURANCE_TIMINGS === "1")) return this.withRecovery(taskId, ctx, enriched); const finished = performance.now(); closeStage(finished); return this.withRecovery(taskId, ctx, { ...enriched, timings: { total_ms: Math.round(finished - started), stage_ms: stageMs } }); } private recordQaAttempt(taskId: string, result: AssuranceAdvanceResult): void { const key = this.qaAttemptKeys.get(taskId); this.qaAttemptKeys.delete(taskId); if (!key || result.state === "cancelled") return; if (result.state !== "failed" || result.operation !== "qa") { this.qaFailureStreaks.delete(taskId); return; } const streak = this.qaFailureStreaks.get(taskId); this.qaFailureStreaks.set(taskId, { key, count: streak?.key === key ? streak.count + 1 : 1 }); } private async withRecovery(taskId: string, ctx: HostContext, enriched: T): Promise { if (enriched.recovery_error || !["failed", "blocked", "rework", "review_preparation_failed"].includes(enriched.state)) return enriched; try { const fresh = await this.ports.projectTask(ctx.cwd, taskId); if (fresh.error || fresh.claim?.task_id !== taskId || fresh.projection.lifecycle !== "active") return { ...enriched, recovery_error: "fresh_kernel_projection_unavailable" }; const record = await this.ports.readTaskRecord(ctx.cwd, taskId); if (!record.record || record.revision !== fresh.projection.record_revision) return { ...enriched, recovery_error: "fresh_kernel_projection_unavailable" }; const recovery = deriveAssuranceRecovery(taskId, fresh, enriched, record.record?.findings); return recovery ? { ...enriched, recovery } : enriched; } catch { return { ...enriched, recovery_error: "fresh_kernel_projection_unavailable" }; } } private async advanceOnce(taskId: string, ctx: HostContext, signal?: AbortSignal, onUpdate?: (update: ForegroundToolUpdate) => void): Promise { if (this.isInvocationOpen(taskId)) return { state: "blocked", reason: "an authority invocation is already open" }; const active = this.active(taskId); if (active?.state === "review_ready") { const reservation = this.reviewReservations.get(taskId); let projection: AssuranceProjectionResult; try { projection = await this.ports.projectTask(ctx.cwd, taskId); } catch (error) { return { state: "blocked", reason: `cannot validate Review reservation: ${boundedAssuranceError(error)}`, recovery_error: "fresh_kernel_projection_unavailable" }; } const matches = reservationStillValid(reservation, projection, taskId); if (matches) return this.reviewReadyResult(taskId); if (reservation) this.releaseReviewReservation(taskId, reservation); this.rejectedReviewOperations.delete(taskId); if (projection.error || (projection.projection.lifecycle !== "done" && projection.projection.lifecycle !== "stopped" && (!projection.claim || projection.claim.task_id !== taskId))) return { state: "blocked", reason: projection.error ?? "no active backend claim for this task", recovery_error: "fresh_kernel_projection_unavailable" }; } const refreshed = this.active(taskId); if (refreshed?.state === "running") return { state: "blocked", reason: `assurance operation ${refreshed.operation_id} is already running` }; const operationId = randomUUID(); const operationGeneration = this.sessionGeneration; const operationController = new AbortController(); const relayExternalAbort = () => operationController.abort( signal?.reason instanceof Error ? signal.reason : new Error("assurance operation cancelled"), ); signal?.addEventListener("abort", relayExternalAbort, { once: true }); if (signal?.aborted) relayExternalAbort(); this.activeOperations.set(taskId, operationId); this.operationControllers.set(taskId, { operationId, controller: operationController }); this.rejectedReviewOperations.delete(taskId); let authorityCommitted = false; let authorityBoundaryStarted = false; // Record revision observed immediately before an in-flight ordinary // mutation, so a rejected precondition can be proven to have written // nothing instead of being reported as an unknown settlement. let boundaryBaseline: string | null = null; let reviewPreparationStarted = false; let initialProjectionAvailable = false; // Set only by the QA job-budget deadline, which is not a host cancellation. let qaJobBudgetExceeded = false; let phase = "preparing"; const operationLive = () => this.sessionActive && this.sessionGeneration === operationGeneration && this.activeOperations.get(taskId) === operationId && !operationController.signal.aborted; const ensureOperationLive = () => { if (!operationLive()) throw new VerificationAbortedError(); }; const progress = (stage: string, summary: string, details: Record = {}) => { phase = stage; onUpdate?.({ content: [{ type: "text", text: summary }], details: { state: "running", operation: "qa", operation_id: operationId, stage, ...details } }); }; const aborted = () => operationController.signal.aborted; // A declared job budget past the ceiling is refused here, before any // preparation command runs or any snapshot is captured. const qaBudgetFailure = () => ({ state: "failed" as const, operation: "qa" as const, operation_id: operationId, reason: `${phase}: deterministic QA exceeded its declared job budget before settlement`, }); try { ensureOperationLive(); progress(phase, `Preparing deterministic QA for ${taskId}`); await this.ports.advanceBeforeProjection?.(); ensureOperationLive(); let projection: AssuranceProjectionResult; try { projection = await this.ports.projectTask(ctx.cwd, taskId); } catch (error) { // Retry only an explicit transient read failure, before any write. if (!(error instanceof Error) || !("code" in error) || !["EINTR", "EAGAIN"].includes(String(error.code))) throw error; ensureOperationLive(); progress("retrying_projection", "Retrying the initial authority read once; no writes replayed", { retry_attempt: 1 }); ensureOperationLive(); projection = await this.ports.projectTask(ctx.cwd, taskId); } ensureOperationLive(); if (projection.error) return { state: "blocked", reason: projection.error, recovery_error: "fresh_kernel_projection_unavailable" }; if (projection.projection.lifecycle === "done" || projection.projection.lifecycle === "stopped") { this.unknownOperations.delete(taskId); return { state: projection.projection.lifecycle === "done" ? "completed" : "stopped" }; } if (!projection.claim) return { state: "blocked", reason: "no active backend claim", recovery_error: "fresh_kernel_projection_unavailable" }; if (projection.claim.task_id !== taskId) return { state: "blocked", reason: `backend claim belongs to ${projection.claim.task_id}, not ${taskId}`, recovery_error: "fresh_kernel_projection_unavailable" }; initialProjectionAvailable = true; // A closed Kernel projection, not the lost host reply, decides what // remains. Committed QA/Review is never replayed from local memory. const parked = await this.ports.readTaskRecord(ctx.cwd, taskId); if (this.unknownOperations.has(taskId)) { if (!parked.record || parked.revision !== projection.projection.record_revision) throw new Error("cannot reconcile unknown settlement"); reviewIncompleteHistory(parked.record.history, projection.projection); const pending = this.reviewReservations.get(taskId); if (pending) this.releaseReviewReservation(taskId, pending); this.unknownOperations.delete(taskId); } if (projection.projection.authorization.state === "authorize_review_retry") return { state: "blocked", reason: "review_retry_requires_authorization", recovery_action: "request_authorization" }; const incomplete = reviewIncompleteHistory(parked.record?.history, projection.projection); if (incomplete.retry_authorized && incomplete.authorized_contract_hash !== currentReviewContractHash()) return { state: "blocked", reason: "authorized Review contract changed; restore the confirmed contract before dispatch" }; if (incomplete.count >= incomplete.round_limit) return { state: "review_incomplete", reason: "Review coverage remained incomplete after two rounds", recovery_action: REVIEW_INCOMPLETE_RECOVERY }; ensureOperationLive(); if (parked.record?.findings.some((finding) => finding.kind === "replan_required" && finding.status === "open")) return { state: "blocked", reason: "review requires a durable replan; the task is paused for a user decision" }; if (projection.projection.artifact_state === "active") { if (projection.projection.next_obligation !== "submit_assurance") return { state: "blocked", reason: `Kernel requires ${projection.projection.next_obligation}` }; if (aborted()) return this.cancelled("qa", operationId, "host cancellation before artifact freeze"); progress("freezing_artifacts", "Freezing planning artifacts for deterministic assurance"); boundaryBaseline = projection.projection.record_revision; const freeze = this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "freeze_artifacts", actor_id: "executor" } }); authorityBoundaryStarted = true; await freeze; ensureOperationLive(); projection = await this.ports.projectTask(ctx.cwd, taskId); ensureOperationLive(); if (projection.error || projection.projection.lifecycle !== "active" || projection.projection.artifact_state !== "frozen") return this.unknownAfterCommit(taskId, "qa", operationId, projection.error ?? "artifact freeze did not settle"); // The freeze outcome is proven by the re-projection above, so later // read-only preparation failures are never a committed-authority mystery. authorityBoundaryStarted = false; boundaryBaseline = null; } if (projection.projection.next_obligation === "complete") { progress("completing", "Completing the routine task after deterministic QA"); const completionBaseline = projection.projection.record_revision; try { await this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "complete", actor_id: "kernel-assurance" } }); return { state: "completed" }; } catch (error) { if (await this.mutationProvablyRejected(ctx, taskId, completionBaseline)) return { state: "failed", operation: "qa", operation_id: operationId, reason: `${phase}: ${boundedAssuranceError(error)}` }; return this.unknownAfterCommit(taskId, "qa", operationId, boundedAssuranceError(error)); } } if (projection.projection.next_obligation !== "run_qa" && projection.projection.next_obligation !== "run_review") return { state: "blocked", reason: `Kernel requires ${projection.projection.next_obligation}` }; const qaAlreadySettled = projection.projection.next_obligation === "run_review"; if (qaAlreadySettled) reviewPreparationStarted = true; ensureOperationLive(); // Prove the immutable Review revision before any descriptor runs. A task // that cannot publish its revision must stop with zero QA attestation // rather than approving bytes nobody can review. if (!qaAlreadySettled && projection.projection.risk !== "routine") { progress("preparing_review_revision", "Proving the immutable Review revision before QA"); try { if (parked.record?.contract === "assurance_kernel/task_record/v4") { if (!this.authority.ensureReviewRevision) throw new Error("v4 Review revision preparation is unavailable"); await this.authority.ensureReviewRevision(ctx.cwd, taskId, projection); } } catch (error) { return { state: "blocked", reason: `review revision preparation failed: ${boundedAssuranceError(error)}` }; } ensureOperationLive(); } let qaVerdict: AssuranceVerdict | undefined; if (qaAlreadySettled) { progress("resuming_review", "Resuming Review preparation from the Kernel assurance projection"); } else { progress("capturing_snapshot", "Capturing the immutable QA snapshot"); await this.ports.qaBeforeProjection?.(); ensureOperationLive(); const assurance = await this.authority.buildAssurance(ctx.cwd, taskId, "qa", projection); ensureOperationLive(); // Preparation counts against the same ceiling as the checks, so the // aggregate clock starts before the first prepared group runs. A host // may only tighten the derived budget, never raise it past the ceiling. const qaKey = `${assurance.snapshot.record_revision}|${assurance.snapshot.intent_content_hash}|${assurance.snapshot.diff_hash}`; const streak = this.qaFailureStreaks.get(taskId); const maxFailures = this.ports.qaMaxIdenticalFailures ?? QA_MAX_IDENTICAL_FAILURES; if (streak?.key === qaKey && streak.count >= maxFailures) return { state: "blocked", reason: `deterministic QA already failed ${streak.count} times on this exact snapshot; change the workspace diff or intent, or restart the session after fixing the environment, before retrying` }; this.qaAttemptKeys.set(taskId, qaKey); const declaredQaJobMs = deriveQaJobTimeoutMs(assurance.descriptors.values()); const qaJobTimeoutMs = Math.min(declaredQaJobMs, this.ports.qaJobTimeoutMs ?? declaredQaJobMs); const qaJobDeadline = setTimeout(() => { qaJobBudgetExceeded = true; operationController.abort(new Error(`deterministic QA exceeded its declared ${Math.ceil(qaJobTimeoutMs / 60_000)} minute job budget`)); }, qaJobTimeoutMs); try { phase = "verifying"; qaVerdict = await this.ports.runQa(assurance.snapshot, assurance.descriptors, { signal: operationController.signal, onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, { current: item.index, total: item.total, acceptance_id: item.acceptance_id, acceptance_phase: item.phase, elapsed_ms: item.elapsed_ms, ...(item.diagnostic ? { diagnostic: qaDiagnosticMetadata(item.diagnostic) } : {}), }), }); } finally { clearTimeout(qaJobDeadline); } ensureOperationLive(); const invocation = this.openInvocation(taskId); try { progress("settling_qa", "Settling deterministic QA through the Kernel revision boundary"); authorityBoundaryStarted = true; await this.authority.applyVerdict(ctx, { taskId, snapshot: assurance.snapshot, verdict: qaVerdict, invocation, actorId: "deterministic-qa", hooks: { beforeCommit: async () => { ensureOperationLive(); await this.ports.qaBeforeAuthorityCommit?.(); ensureOperationLive(); }, onCommit: () => { authorityCommitted = true; this.ports.qaOnAuthorityCommit?.(); }, afterCommit: async () => { await this.ports.qaAfterAuthorityCommit?.(); }, }, }); if (!(authorityCommitted && aborted())) { ensureOperationLive(); authorityBoundaryStarted = false; } } catch (error) { if (qaJobBudgetExceeded) return qaBudgetFailure(); if (!authorityCommitted && (aborted() || error instanceof VerificationAbortedError)) return this.cancelled("qa", operationId, "host cancellation before QA authority commit"); return authorityCommitted ? this.unknownAfterCommit(taskId, "qa", operationId, boundedAssuranceError(error)) : { state: "failed", operation: "qa", operation_id: operationId, reason: boundedAssuranceError(error) }; } finally { if (this.invocationState(invocation) === "open") this.closeInvocation(invocation); } } if (qaVerdict?.decision === "rework") return { state: "rework", operation: "qa", operation_id: operationId, summary: qaVerdict.findings?.map((finding) => finding.summary).join("; ") ?? "deterministic QA requested rework" }; if (!(authorityCommitted && aborted())) ensureOperationLive(); progress("preparing_review", "Preparing the reserved foreground Review bundle"); let fresh: AssuranceProjectionResult; try { fresh = await this.ports.projectTask(ctx.cwd, taskId); } catch (error) { return this.reviewPreparationFailed(taskId, operationId, boundedAssuranceError(error)); } if (fresh.error || !fresh.claim) { if (qaAlreadySettled) return this.reviewPreparationFailed(taskId, operationId, fresh.error ?? "claim disappeared after QA settlement"); return this.unknownAfterCommit(taskId, "qa", operationId, fresh.error ?? "claim disappeared after QA settlement"); } if (fresh.projection.next_obligation === "complete") { progress("completing", "Deterministic QA passed; completing the routine task"); try { await this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "complete", actor_id: "kernel-assurance" } }); return { state: "completed" }; } catch (error) { return this.unknownAfterCommit(taskId, "qa", operationId, boundedAssuranceError(error)); } } if (fresh.projection.next_obligation !== "run_review") { if (authorityCommitted && aborted()) return this.unknownAfterCommit(taskId, "qa", operationId, "QA settlement projection did not require Review after cancellation"); return { state: "blocked", reason: `Kernel requires ${fresh.projection.next_obligation} after QA` }; } reviewPreparationStarted = true; authorityCommitted = false; authorityBoundaryStarted = false; if (aborted()) return this.reviewPreparationFailed(taskId, operationId, "host cancellation after QA authority settlement"); progress("preparing_review", "Preparing the reserved foreground Review evidence"); let review: Awaited>; let evidence: ReviewEvidence; try { review = await this.authority.buildAssurance(ctx.cwd, taskId, "review", fresh); const payload = review.reviewManifest ?? review.reviewBundle; if (!payload) throw new Error("review evidence is missing after QA settlement"); evidence = this.ports.writeReviewEvidence({ snapshot: review.snapshot, evidence: payload }); } catch (error) { // QA authority is already settled and durable; only the read-only // evidence transport failed. Keep run_review and allow a retry. return this.reviewPreparationFailed(taskId, operationId, boundedAssuranceError(error)); } try { ensureOperationLive(); } catch (error) { try { evidence.remove(); } catch { /* cleanup cannot create authority */ } throw error; } let hostReservation: HostReviewReservation; let turnBudget: number; try { const stored = await this.ports.readTaskRecord(ctx.cwd, taskId); const rounds = reviewIncompleteHistory(stored.record?.history, review.snapshot); if (rounds.retry_authorized && rounds.authorized_contract_hash !== currentReviewContractHash()) throw new Error("authorized Review contract changed before dispatch"); if (rounds.count >= rounds.round_limit) { evidence.remove(); return { state: "review_incomplete", reason: "Review coverage remained incomplete after two rounds", recovery_action: REVIEW_INCOMPLETE_RECOVERY }; } ensureOperationLive(); const reviewEvidence = review.reviewManifest ?? review.reviewBundle!; const workload = classifyReviewWorkload(review.snapshot, reviewEvidence); turnBudget = reviewTurnBudget(workload, reviewChangedPathCount(reviewEvidence), rounds.previous_budget); hostReservation = this.ports.host.prepareReview({ taskId, operationId, prompt: buildReviewPrompt(review.snapshot, evidence.path, turnBudget), snapshotPrompt: buildReviewSnapshotPrompt(review.snapshot, evidence.path, turnBudget), evidencePath: evidence.path, writePacket: evidence.writePacket, maxTurns: turnBudget, }); ensureOperationLive(); } catch (error) { try { evidence.remove(); } catch { /* cleanup cannot create authority */ } return this.reviewPreparationFailed(taskId, operationId, boundedAssuranceError(error)); } const reservation: ReviewReservation = { taskId, operationId, correlation: { record_revision: review.snapshot.record_revision, intent_content_hash: review.snapshot.intent_content_hash, diff_hash: review.snapshot.diff_hash }, snapshot: review.snapshot, hostReservation, turnBudget, reviewContractHash: currentReviewContractHash(), verdictCorrectionRequired: false, evidence, }; this.reviewReservations.set(taskId, reservation); progress("review_ready", "QA passed; invoke the reserved foreground Agent, then call submit_review", { snapshot_digest: snapshotDigest(review.snapshot), review_bundle_digest: review.snapshot.review_bundle_digest ?? "", agent_params: hostReservation.dispatch }); return { state: "review_ready", operation: "review", operation_id: operationId, snapshot_digest: snapshotDigest(review.snapshot), review_bundle_digest: review.snapshot.review_bundle_digest ?? "", agent_params: hostReservation.dispatch }; } catch (error) { // Recovery decoration must not bypass the initial read retry policy. if (!initialProjectionAvailable && !aborted() && !(error instanceof VerificationAbortedError)) return { state: "failed", operation: "qa", operation_id: operationId, reason: `${phase}: ${boundedAssuranceError(error)}`, recovery_error: "fresh_kernel_projection_unavailable" }; if (error instanceof QaPreparationError && !authorityCommitted && !authorityBoundaryStarted && !aborted()) return { state: "failed", operation: "qa", operation_id: operationId, reason: error.prepare_observation?.drift_paths ? `${error.message}; ${QA_DRIFT_NOTE}` : error.prepare_observation?.conditions.delivery === "isolated_materialization" ? `${error.message}; ${QA_PREPARE_REPRODUCTION_NOTE}` : error.message, environment_failure: true, diagnostics: error.diagnostics.map(qaDiagnosticMetadata), ...(error.prepare_observation ? { prepare_observation: qaPrepareObservationMetadata(error.prepare_observation) } : {}) }; if (phase === "verifying" && !authorityCommitted && !authorityBoundaryStarted && !aborted() && !(error instanceof VerificationAbortedError)) return { state: "failed", operation: "qa", operation_id: operationId, reason: "QA execution failed before attestation (execution_failed)", environment_failure: true }; if (reviewPreparationStarted) { const reason = aborted() || error instanceof VerificationAbortedError ? `${phase}: host cancellation` : `${phase}: ${boundedAssuranceError(error)}`; return this.reviewPreparationFailed(taskId, operationId, reason); } if (authorityCommitted || authorityBoundaryStarted) { // An in-flight mutation is genuinely unknown only when the authority // state cannot be proven. A Kernel precondition rejection — a missing // scope-bound Spec, for example — writes nothing, and reporting it as // `settlement_unknown` sends the Loop into a reconciling retry that // can never change the outcome. const cancelling = aborted() || error instanceof VerificationAbortedError; if (!authorityCommitted && boundaryBaseline !== null && !cancelling && await this.mutationProvablyRejected(ctx, taskId, boundaryBaseline)) return { state: "failed", operation: "qa", operation_id: operationId, reason: `${phase}: ${boundedAssuranceError(error)}` }; return this.unknownAfterCommit(taskId, "qa", operationId, `${phase}: ${boundedAssuranceError(error)}`); } if (qaJobBudgetExceeded) return qaBudgetFailure(); if (aborted() || error instanceof VerificationAbortedError) return this.cancelled("qa", operationId, `${phase}: host cancellation`); return { state: "failed", operation: "qa", operation_id: operationId, reason: `${phase}: ${boundedAssuranceError(error)}` }; } finally { if (this.activeOperations.get(taskId) === operationId) this.activeOperations.delete(taskId); const controller = this.operationControllers.get(taskId); if (controller?.operationId === operationId) this.operationControllers.delete(taskId); signal?.removeEventListener("abort", relayExternalAbort); } } async submitReview(taskId: string, ctx: HostContext, verdictInput: unknown, options?: ReviewSubmissionOptions): Promise { return this.withRecovery(taskId, ctx, await this.submitReviewOnce(taskId, ctx, verdictInput, options)); } private async submitReviewOnce(taskId: string, ctx: HostContext, verdictInput: unknown, options?: ReviewSubmissionOptions): Promise { const unknown = this.unknownOperations.get(taskId); if (unknown) return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason }; const rejected = this.rejectedReviewOperations.get(taskId); if (rejected) return { state: "blocked", reason: rejected.reason }; const reservation = this.reviewReservations.get(taskId); if (!reservation) return { state: "blocked", reason: "no active Review operation" }; let fresh: AssuranceProjectionResult; try { fresh = await this.ports.projectTask(ctx.cwd, taskId); } catch (error) { const reason = boundedAssuranceError(error); this.releaseReviewReservation(taskId, reservation, reason); return { state: "blocked", reason, recovery_error: "fresh_kernel_projection_unavailable" }; } if (fresh.error || !fresh.claim || fresh.claim.task_id !== taskId || compareReservationSnapshot(reservation.snapshot, fresh.projection).length > 0 || fresh.projection.lifecycle !== reservation.snapshot.lifecycle || fresh.projection.artifact_state !== reservation.snapshot.artifact_state) { const reason = fresh.error ?? "assurance snapshot changed before Review submission"; this.releaseReviewReservation(taskId, reservation, reason); return { state: "blocked", reason, ...(fresh.error || !fresh.claim || fresh.claim.task_id !== taskId ? { recovery_error: "fresh_kernel_projection_unavailable" as const } : {}) }; } if (reservation.snapshot.review_revision) { try { if (!this.authority.ensureReviewRevision) throw new Error("v4 Review revision verification is unavailable"); const revision = await this.authority.ensureReviewRevision(ctx.cwd, taskId, fresh); if (!revision || revision.base_head !== reservation.snapshot.review_revision.base_head || revision.review_commit !== reservation.snapshot.review_revision.review_commit || revision.review_tree !== reservation.snapshot.review_revision.review_tree || revision.manifest_digest !== reservation.snapshot.review_revision.manifest_digest) throw new Error("Review revision changed before submission"); } catch (error) { return this.reviewPreparationFailed(taskId, reservation.operationId, boundedAssuranceError(error)); } } let verdict: ChildReviewResult; try { verdict = parseChildVerdict(verdictInput, reservation.snapshot); } catch (error) { reservation.verdictCorrectionRequired = true; return { state: "blocked", code: "verdict_invalid", reason: boundedAssuranceError(error) }; } const receiptError = options?.validateReceipt?.(); if (receiptError) { if (receiptError.release) this.releaseReviewReservation(taskId, reservation, receiptError.reason); return { state: "blocked", reason: receiptError.reason, recovery_action: receiptError.recovery_action }; } if (reservation.reviewContractHash !== currentReviewContractHash()) { this.releaseReviewReservation(taskId, reservation); return { state: "blocked", reason: "Review contract changed during dispatch", recovery_action: "advance_assurance" }; } if (verdict.decision === "incomplete") { try { await this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "record_review_incomplete", actor_id: "parent-mediated-review", snapshot: { ...reservation.correlation, workspace_revision: reservation.snapshot.workspace_revision }, turn_budget: reservation.turnBudget, review_contract_hash: reservation.reviewContractHash, inspected_paths: verdict.inspected_paths, reason: verdict.reason, } }); const stored = await this.ports.readTaskRecord(ctx.cwd, taskId); const rounds = reviewIncompleteHistory(stored.record?.history, reservation.snapshot); if (rounds.count === 0 || rounds.previous_budget !== reservation.turnBudget) throw new Error("incomplete mutation is not observable"); this.releaseReviewReservation(taskId, reservation); return rounds.count >= rounds.round_limit ? { state: "review_incomplete", reason: verdict.reason, recovery_action: REVIEW_INCOMPLETE_RECOVERY } : { state: "blocked", reason: "review_incomplete_retry", recovery_action: "advance_assurance" }; } catch (error) { if (await this.mutationProvablyRejected(ctx, taskId, reservation.snapshot.record_revision)) return { state: "blocked", reason: boundedAssuranceError(error) }; return this.unknownAfterCommit(taskId, "review", reservation.operationId, boundedAssuranceError(error)); } } const invocation = this.openInvocation(taskId); this.releaseReviewReservation(taskId, reservation); try { await this.authority.applyVerdict(ctx, { taskId, snapshot: reservation.snapshot, verdict, invocation, actorId: "parent-mediated-review", ...(options?.reviewer_verdict_sha256 ? { reviewer_verdict_sha256: options.reviewer_verdict_sha256 } : {}), }); } catch (error) { const reason = boundedAssuranceError(error); return this.invocationState(invocation) === "committed" ? this.unknownAfterCommit(taskId, "review", reservation.operationId, reason) : { state: "blocked", reason }; } finally { this.closeInvocation(invocation); } if (verdict.decision === "rework") { return { state: "rework", operation: "review", operation_id: reservation.operationId, summary: verdict.findings?.map((finding) => finding.summary).join("; ") ?? "independent Review requested rework", }; } let settled: AssuranceProjectionResult; try { settled = await this.ports.projectTask(ctx.cwd, taskId); } catch (error) { return this.unknownAfterCommit(taskId, "review", reservation.operationId, boundedAssuranceError(error)); } if (settled.error || !settled.claim) return this.unknownAfterCommit(taskId, "review", reservation.operationId, settled.error ?? "claim disappeared after Review settlement"); if (settled.projection.next_obligation === "complete") { try { await this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "complete", actor_id: "kernel-assurance" } }); return { state: "completed" }; } catch (error) { return this.unknownAfterCommit(taskId, "review", reservation.operationId, boundedAssuranceError(error)); } } return { state: "blocked", reason: `Kernel requires ${settled.projection.next_obligation} after Review` }; } isReviewVerdictValid(taskId: string, verdictInput: unknown): boolean { const reservation = this.reviewReservations.get(taskId); if (!reservation) return false; try { parseChildVerdict(verdictInput, reservation.snapshot); return true; } catch { return false; } } abandonReview(taskId: string, reason: string): AssuranceSubmitReviewResult { const reservation = this.reviewReservations.get(taskId); if (!reservation) return { state: "blocked", reason }; this.releaseReviewReservation(taskId, reservation, reason); return { state: "blocked", reason }; } private reviewReadyResult(taskId: string): AssuranceAdvanceResult { const reservation = this.reviewReservations.get(taskId); if (!reservation) return { state: "blocked", reason: "Review reservation disappeared" }; if (reservation.verdictCorrectionRequired) return { state: "blocked", code: "verdict_invalid", reason: "Review verdict correction is required before advancing" }; return { state: "review_ready", operation: "review", operation_id: reservation.operationId, snapshot_digest: snapshotDigest(reservation.snapshot), review_bundle_digest: reservation.snapshot.review_bundle_digest ?? "", agent_params: reservation.hostReservation.dispatch }; } releaseStoppedReview(taskId: string): void { const reservation = this.reviewReservations.get(taskId); if (reservation) this.releaseReviewReservation(taskId, reservation); this.rejectedReviewOperations.delete(taskId); } private releaseReviewReservation(taskId: string, reservation: ReviewReservation, rejectionReason?: string): void { if (this.reviewReservations.get(taskId) !== reservation) return; this.reviewReservations.delete(taskId); try { this.ports.host.releaseReview(reservation.hostReservation); } catch { /* cleanup cannot create authority */ } if (rejectionReason) this.rejectedReviewOperations.set(taskId, { operationId: reservation.operationId, reason: rejectionReason }); this.removeEvidence(reservation); } private removeEvidence(reservation: ReviewReservation): void { try { reservation.evidence.remove(); } catch { /* cleanup cannot create authority */ } } private cancelled(operation: "qa" | "review", operationId: string, reason: string): AssuranceAdvanceResult { return { state: "cancelled", operation, operation_id: operationId, reason }; } private reviewPreparationFailed( taskId: string, operationId: string, reason: string, ): Extract { const reservation = this.reviewReservations.get(taskId); if (reservation) this.releaseReviewReservation(taskId, reservation); this.rejectedReviewOperations.delete(taskId); return { state: "review_preparation_failed", operation: "review", operation_id: operationId, reason }; } /** * Prove that a failed ordinary mutation wrote nothing. * * Re-reads the projection once and compares the record revision with the one * observed immediately before the mutation. An unchanged revision means the * Kernel rejected a precondition, which is a deterministic failure the caller * must act on — not an unknown settlement to reconcile. Any read failure or * drift stays unknown, so the conservative answer is the default. */ private async mutationProvablyRejected(ctx: HostContext, taskId: string, baselineRevision: string): Promise { try { const fresh = await this.ports.projectTask(ctx.cwd, taskId); return !fresh.error && fresh.projection.record_revision === baselineRevision; } catch { return false; } } /** * Declared as the wider advance result, which made it unusable from * `submitReview` without an implicit widen. The value has always been the * settlement_unknown member both result unions share. */ private unknownAfterCommit( taskId: string, operation: "qa" | "review", operationId: string, reason: string, ): { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string } { this.unknownOperations.set(taskId, { operation, operationId, reason }); return { state: "settlement_unknown", operation, operation_id: operationId, reason }; } } function boundedAssuranceError(error: unknown): string { const raw = error instanceof Error ? error.message : String(error); const normalized = raw.replace(/\s+/g, " ").trim() || "unknown error"; return normalized.length <= 300 ? normalized : `${normalized.slice(0, 299)}...`; }