immune-brain 2.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +239 -0
  2. package/README.zh-CN.md +239 -0
  3. package/package.json +84 -0
  4. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +666 -0
  5. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +1622 -0
  6. package/plugins/immune-brain/.pi-extension/package.json +11 -0
  7. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +711 -0
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance.ts +154 -0
  9. package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +349 -0
  10. package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +90 -0
  11. package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +214 -0
  12. package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +50 -0
  13. package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +262 -0
  14. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +28 -0
  15. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +237 -0
  16. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +414 -0
  17. package/plugins/immune-brain/.pi-extension/tsconfig.json +20 -0
  18. package/plugins/immune-brain/bin/imm-kernel +4 -0
  19. package/plugins/immune-brain/bin/imm-plan +4 -0
  20. package/plugins/immune-brain/bin/imm-pr-diag +230 -0
  21. package/plugins/immune-brain/bin/imm-retire-stale-wrapper +4 -0
  22. package/plugins/immune-brain/bin/imm-retired +4 -0
  23. package/plugins/immune-brain/bin/imm-tracker +4 -0
  24. package/plugins/immune-brain/dist/BASELINE.md +138 -0
  25. package/plugins/immune-brain/dist/docs/reference/HANDOFF-template.md +122 -0
  26. package/plugins/immune-brain/dist/docs/reference/design-contract-audit-rubric.md +149 -0
  27. package/plugins/immune-brain/dist/docs/reference/design-contract-review-checklist.md +55 -0
  28. package/plugins/immune-brain/dist/docs/reference/i18n-review-checklist.md +110 -0
  29. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +52 -0
  30. package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +94 -0
  31. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +44 -0
  32. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +105 -0
  33. package/plugins/immune-brain/dist/docs/reference/ux-heuristic-checklist.md +131 -0
  34. package/plugins/immune-brain/dist/imm-brainstorm.md +140 -0
  35. package/plugins/immune-brain/dist/imm-doc-prune.md +137 -0
  36. package/plugins/immune-brain/dist/imm-loop.md +158 -0
  37. package/plugins/immune-brain/dist/imm-planner.md +387 -0
  38. package/plugins/immune-brain/dist/imm-pr-fix.md +71 -0
  39. package/plugins/immune-brain/dist/registry.yaml +49 -0
  40. package/plugins/immune-brain/dist/role-prompts/advisory-reviewer.md +16 -0
  41. package/plugins/immune-brain/dist/role-prompts/arch-explorer.md +14 -0
  42. package/plugins/immune-brain/dist/role-prompts/code-review.md +15 -0
  43. package/plugins/immune-brain/dist/role-prompts/compounder.md +20 -0
  44. package/plugins/immune-brain/dist/role-prompts/executor.md +13 -0
  45. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +81 -0
  46. package/plugins/immune-brain/dist/role-prompts/qa.md +23 -0
  47. package/plugins/immune-brain/dist/role-prompts/test-fixer.md +3 -0
  48. package/plugins/immune-brain/dist/role-prompts/ui-review.md +14 -0
  49. package/plugins/immune-brain/runtime/authority_commit_receipts.ts +716 -0
  50. package/plugins/immune-brain/runtime/canonical_json.ts +19 -0
  51. package/plugins/immune-brain/runtime/commands/kernel.ts +1160 -0
  52. package/plugins/immune-brain/runtime/github_issue_tracker.ts +1009 -0
  53. package/plugins/immune-brain/runtime/kernel/application.ts +300 -0
  54. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +284 -0
  55. package/plugins/immune-brain/runtime/kernel/authority_port.ts +208 -0
  56. package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +451 -0
  57. package/plugins/immune-brain/runtime/kernel/backend_claim.ts +197 -0
  58. package/plugins/immune-brain/runtime/kernel/canary_application.ts +507 -0
  59. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +73 -0
  60. package/plugins/immune-brain/runtime/kernel/completion.ts +160 -0
  61. package/plugins/immune-brain/runtime/kernel/enrollment.ts +194 -0
  62. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +123 -0
  63. package/plugins/immune-brain/runtime/kernel/index.ts +29 -0
  64. package/plugins/immune-brain/runtime/kernel/intent.ts +563 -0
  65. package/plugins/immune-brain/runtime/kernel/intent_token_registry.ts +80 -0
  66. package/plugins/immune-brain/runtime/kernel/legacy.ts +299 -0
  67. package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +153 -0
  68. package/plugins/immune-brain/runtime/kernel/observation.ts +395 -0
  69. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +169 -0
  70. package/plugins/immune-brain/runtime/kernel/readiness.ts +282 -0
  71. package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +132 -0
  72. package/plugins/immune-brain/runtime/kernel/reducer.ts +624 -0
  73. package/plugins/immune-brain/runtime/kernel/storage.ts +1780 -0
  74. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +791 -0
  75. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +492 -0
  76. package/plugins/immune-brain/runtime/kernel/types.ts +295 -0
  77. package/plugins/immune-brain/runtime/kernel/validation.ts +963 -0
  78. package/plugins/immune-brain/runtime/loop_contract.ts +362 -0
  79. package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +462 -0
  80. package/plugins/immune-brain/runtime/plan_core.ts +1053 -0
  81. package/plugins/immune-brain/runtime/prompts/advisory-reviewer.md +16 -0
  82. package/plugins/immune-brain/runtime/prompts/arch-explorer.md +14 -0
  83. package/plugins/immune-brain/runtime/prompts/code-review.md +15 -0
  84. package/plugins/immune-brain/runtime/prompts/compounder.md +20 -0
  85. package/plugins/immune-brain/runtime/prompts/executor.md +13 -0
  86. package/plugins/immune-brain/runtime/prompts/pr-fix.md +81 -0
  87. package/plugins/immune-brain/runtime/prompts/qa.md +23 -0
  88. package/plugins/immune-brain/runtime/prompts/test-fixer.md +3 -0
  89. package/plugins/immune-brain/runtime/prompts/ui-review.md +14 -0
  90. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +160 -0
  91. package/plugins/immune-brain/runtime/v4_runtime.ts +295 -0
  92. package/plugins/immune-brain/runtime/verification_descriptor.ts +162 -0
  93. package/plugins/immune-brain/runtime/workspace_scope.ts +623 -0
  94. package/plugins/immune-brain/skills/.ignore +1 -0
  95. package/plugins/immune-brain/skills/BASELINE.md +138 -0
  96. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +66 -0
  97. package/plugins/immune-brain/skills/imm-doc-prune/SKILL.md +11 -0
  98. package/plugins/immune-brain/skills/imm-loop/SKILL.md +52 -0
  99. package/plugins/immune-brain/skills/imm-planner/SKILL.md +221 -0
  100. package/plugins/immune-brain/skills/imm-pr-fix/SKILL.md +10 -0
  101. package/plugins/immune-brain/skills/registry.yaml +49 -0
@@ -0,0 +1,711 @@
1
+ // Foreground Assurance progression for one Pi session.
2
+ //
3
+ // Deterministic QA runs to completion inside the caller's Tool execution. A
4
+ // successful QA pass creates one short-lived native Review reservation; the
5
+ // Parent then invokes the exact foreground Agent arguments returned by the
6
+ // Tool and explicitly calls submit_review. No lifecycle work survives a Tool
7
+ // call except that reservation and its uncommitted native receipt.
8
+
9
+ import { createHash, randomUUID } from "node:crypto";
10
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
11
+ import {
12
+ findingsDigest,
13
+ parseVerificationDescriptor,
14
+ type FrozenRunner,
15
+ type VerificationDescriptor,
16
+ VerificationAbortedError,
17
+ } from "./pi-canary-verification";
18
+ import { createInvocationRegistry, type InvocationState, type InvocationToken } from "./pi-canary-invocations";
19
+ import { describeQaFailure } from "./pi-canary-qa-findings";
20
+ import type { ReviewBundle } from "./pi-canary-review-bundle";
21
+ import {
22
+ matchesReservedAgentArgs,
23
+ parseForegroundAgentResult,
24
+ reservedAgentParams,
25
+ STANDARD_AGENT_TOOL,
26
+ nativeReviewResultIsFailure,
27
+ type NativeReviewResult,
28
+ type ReservedAgentParams,
29
+ type ToolResultLike,
30
+ type ToolExecutionEndLike,
31
+ } from "./pi-canary-native-review";
32
+ import type { AssuranceCorrelation, AssuranceRole, AssuranceResultPresentation } from "./pi-canary-assurance";
33
+ import {
34
+ buildRoleDelegationPacket,
35
+ } from "../runtime/role_prompt_bridge";
36
+ import type { AssuranceProjectionResult, TaskIntentRead, TaskRecordRead, TaskTombstoneRead } from "./runtime-stub";
37
+
38
+ export interface GithubTerminalProjectionInput {
39
+ task_id: string;
40
+ phase: "done" | "stopped";
41
+ terminal_event_id: string;
42
+ }
43
+
44
+ export function deriveGithubTerminalProjectionInput(
45
+ taskId: string,
46
+ projection: AssuranceProjectionResult,
47
+ tombstone: TaskTombstoneRead | null,
48
+ ): GithubTerminalProjectionInput | null {
49
+ if (
50
+ projection.error
51
+ || projection.claim !== null
52
+ || (projection.projection.lifecycle !== "done" && projection.projection.lifecycle !== "stopped")
53
+ || tombstone?.task_id !== taskId
54
+ || tombstone.lifecycle_status !== "terminal"
55
+ || tombstone.terminal_lifecycle !== projection.projection.lifecycle
56
+ ) return null;
57
+ return {
58
+ task_id: taskId,
59
+ phase: projection.projection.lifecycle,
60
+ terminal_event_id: tombstone.terminal_event_id,
61
+ };
62
+ }
63
+
64
+ export interface AssuranceVerdict {
65
+ contract: "assurance_kernel/assurance_verdict/v2";
66
+ role: AssuranceRole;
67
+ task_id: string;
68
+ snapshot_digest: string;
69
+ decision: "pass" | "rework";
70
+ approval?: {
71
+ kind: "qa" | "review";
72
+ authority_role: "qa" | "reviewer";
73
+ summary: string;
74
+ };
75
+ findings?: Array<{
76
+ id: string;
77
+ kind: "blocking" | "advisory";
78
+ acceptance_id: string | null;
79
+ summary: string;
80
+ findings_digest: string;
81
+ }>;
82
+ }
83
+
84
+ export interface SnapshotDescriptor {
85
+ contract: "assurance_kernel/assurance_snapshot/v2";
86
+ task_id: string;
87
+ role: AssuranceRole;
88
+ record_revision: string;
89
+ workspace_revision: string;
90
+ intent_revision: number;
91
+ intent_content_hash: string;
92
+ diff_hash: string;
93
+ lifecycle: string;
94
+ artifact_state: string;
95
+ risk: "routine" | "material" | "critical";
96
+ fresh_acceptance_ids: string[];
97
+ missing_acceptance_ids: string[];
98
+ stale_attestation_ids: string[];
99
+ acceptance: Array<{ id: string; assertion: string; verification: string }>;
100
+ dirty_files: string[];
101
+ review_bundle_digest: string | null;
102
+ root: string;
103
+ }
104
+
105
+ export interface QaVerificationProgress {
106
+ index: number;
107
+ total: number;
108
+ acceptance_id: string;
109
+ phase: "running" | "passed" | "failed";
110
+ elapsed_ms: number;
111
+ }
112
+
113
+ export interface ForegroundToolUpdate {
114
+ content: Array<{ type: "text"; text: string }>;
115
+ details: Record<string, unknown>;
116
+ }
117
+
118
+ export type AssuranceAdvanceResult =
119
+ | { state: "review_ready"; operation: "review"; operation_id: string; snapshot_digest: string; review_bundle_digest: string; agent_params: ReservedAgentParams }
120
+ | { state: "awaiting_user"; operation: "record-user-approval"; operation_id: string }
121
+ | { state: "rework"; operation: "qa"; operation_id: string; summary: string }
122
+ | { state: "cancelled"; operation: "qa" | "review"; operation_id: string; reason: string }
123
+ | { state: "failed"; operation: "qa" | "review"; operation_id: string; reason: string }
124
+ | { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string }
125
+ | { state: "blocked"; reason: string }
126
+ | { state: "completed" }
127
+ | { state: "stopped" };
128
+
129
+ export type AssuranceSubmitReviewResult =
130
+ | { state: "rework"; operation: "review"; operation_id: string; summary: string }
131
+ | { state: "awaiting_user"; operation: "record-user-approval"; operation_id: string }
132
+ | { state: "completed" }
133
+ | { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string }
134
+ | { state: "blocked"; reason: string };
135
+
136
+ export type ActiveAssuranceState =
137
+ | { state: "running"; operation: "qa"; operation_id: string; deadline_seconds: number }
138
+ | { state: "review_ready"; operation: "review"; operation_id: string }
139
+ | { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string };
140
+
141
+ export interface AssuranceProgressionPorts {
142
+ projectTask(root: string, taskId: string): Promise<AssuranceProjectionResult>;
143
+ readTaskRecord(root: string, taskId: string): Promise<TaskRecordRead>;
144
+ readTaskIntent(root: string, taskId: string): Promise<TaskIntentRead>;
145
+ frozenRunner(): Promise<FrozenRunner>;
146
+ buildAssurance(
147
+ root: string,
148
+ taskId: string,
149
+ role: AssuranceRole,
150
+ projection: AssuranceProjectionResult,
151
+ runner: FrozenRunner,
152
+ ): Promise<{
153
+ snapshot: SnapshotDescriptor;
154
+ descriptors: Map<string, VerificationDescriptor>;
155
+ reviewBundle: ReviewBundle | null;
156
+ }>;
157
+ runQa(
158
+ snapshot: SnapshotDescriptor,
159
+ descriptors: Map<string, VerificationDescriptor>,
160
+ runner: FrozenRunner,
161
+ options: { signal?: AbortSignal; onProgress?: (progress: QaVerificationProgress) => void },
162
+ ): Promise<AssuranceVerdict>;
163
+ writeReviewEvidence(input: { snapshot: SnapshotDescriptor; review_bundle: ReviewBundle }): { path: string; remove(): void };
164
+ applyVerdict(
165
+ ctx: ExtensionContext,
166
+ input: {
167
+ taskId: string;
168
+ snapshot: SnapshotDescriptor;
169
+ verdict: AssuranceVerdict;
170
+ invocation: InvocationToken;
171
+ actorId: string;
172
+ hooks?: {
173
+ beforeCommit?: () => Promise<void>;
174
+ onCommit?: () => void;
175
+ afterCommit?: () => Promise<void>;
176
+ };
177
+ },
178
+ ): Promise<void>;
179
+ applyOrdinaryOperation(ctx: ExtensionContext, input: { taskId: string; operation: { op: string; actor_id: string } }): Promise<unknown>;
180
+ advanceBeforeProjection?: () => Promise<void>;
181
+ qaBeforeProjection?: () => Promise<void>;
182
+ qaBeforeAuthorityCommit?: () => Promise<void>;
183
+ qaOnAuthorityCommit?: () => void;
184
+ qaAfterAuthorityCommit?: () => Promise<void>;
185
+ qaJobTimeoutMs?: number;
186
+ }
187
+
188
+ export const QA_MIN_JOB_TIMEOUT_SECONDS = 15 * 60;
189
+ export const QA_MAX_JOB_TIMEOUT_SECONDS = 60 * 60;
190
+ export const QA_JOB_OVERHEAD_SECONDS = 2 * 60;
191
+ export const QA_JOB_TIMEOUT_SECONDS = QA_MIN_JOB_TIMEOUT_SECONDS;
192
+ export const REVIEW_PREPARATION_TIMEOUT_MS = 30_000;
193
+ export const REVIEW_DISPATCH_TIMEOUT_MS = 120_000;
194
+ export const REVIEW_VERDICT_VALIDATION_TIMEOUT_MS = 30_000;
195
+ export const ASSURANCE_STALL_MS = 30_000;
196
+
197
+ type ReviewWorkload = "quick" | "standard" | "heavy";
198
+ export interface ReviewTimingProfile { softDeadlineSeconds: number; stopThresholdSeconds: number }
199
+ export const REVIEW_TIMING_PROFILES: Readonly<Record<ReviewWorkload, ReviewTimingProfile>> = {
200
+ quick: { softDeadlineSeconds: 5 * 60, stopThresholdSeconds: 15 * 60 },
201
+ standard: { softDeadlineSeconds: 10 * 60, stopThresholdSeconds: 30 * 60 },
202
+ heavy: { softDeadlineSeconds: 20 * 60, stopThresholdSeconds: 60 * 60 },
203
+ };
204
+
205
+ function declaredQaJobTimeoutMs(descriptors: Iterable<Pick<VerificationDescriptor, "timeout_ms">>): number {
206
+ let declaredMs = 0;
207
+ for (const descriptor of descriptors) declaredMs += descriptor.timeout_ms;
208
+ return Math.max(QA_MIN_JOB_TIMEOUT_SECONDS * 1000, declaredMs + QA_JOB_OVERHEAD_SECONDS * 1000);
209
+ }
210
+
211
+ export function deriveQaJobTimeoutMs(descriptors: Iterable<Pick<VerificationDescriptor, "timeout_ms">>): number {
212
+ const derivedMs = declaredQaJobTimeoutMs(descriptors);
213
+ if (derivedMs > QA_MAX_JOB_TIMEOUT_SECONDS * 1000)
214
+ throw new Error(`declared QA budget ${Math.ceil(derivedMs / 60_000)} minutes exceeds the maximum of 60 minutes`);
215
+ return derivedMs;
216
+ }
217
+
218
+ const QUICK_REVIEW_MAX_ACCEPTANCE = 3;
219
+ const QUICK_REVIEW_MAX_FILES = 5;
220
+ const QUICK_REVIEW_MAX_BYTES = 64 * 1024;
221
+ const HEAVY_REVIEW_MIN_ACCEPTANCE = 9;
222
+ const HEAVY_REVIEW_MIN_BYTES = 512 * 1024 + 1;
223
+
224
+ export function classifyReviewWorkload(snapshot: Pick<SnapshotDescriptor, "risk" | "acceptance">, bundle: ReviewBundle): ReviewWorkload {
225
+ const bytes = Buffer.byteLength(JSON.stringify(bundle));
226
+ if (snapshot.risk === "critical" || snapshot.acceptance.length >= HEAVY_REVIEW_MIN_ACCEPTANCE || bytes >= HEAVY_REVIEW_MIN_BYTES) return "heavy";
227
+ if (snapshot.risk === "routine" && snapshot.acceptance.length <= QUICK_REVIEW_MAX_ACCEPTANCE && Object.keys(bundle.dirty_files).length <= QUICK_REVIEW_MAX_FILES && bytes <= QUICK_REVIEW_MAX_BYTES) return "quick";
228
+ return "standard";
229
+ }
230
+
231
+ export function reviewTurnBudget(workload: ReviewWorkload): number {
232
+ return workload === "quick" ? 12 : workload === "standard" ? 16 : 24;
233
+ }
234
+
235
+ export function snapshotDigest(snapshot: SnapshotDescriptor): string {
236
+ return `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
237
+ }
238
+
239
+ export function buildReviewPrompt(snapshot: SnapshotDescriptor, evidencePath?: string): string {
240
+ if (snapshot.role !== "review") throw new Error("native review prompt requires review role");
241
+ const acceptance = snapshot.acceptance.map((item) => `- ${item.id}: ${item.assertion}`).join("\n");
242
+ const digest = snapshotDigest(snapshot);
243
+ const rolePacket = buildRoleDelegationPacket({
244
+ role: "code-review",
245
+ context: {
246
+ task_id: snapshot.task_id,
247
+ review_gate: "imm-code-review",
248
+ changed_files_signature: snapshot.diff_hash,
249
+ snapshot_digest: digest,
250
+ },
251
+ });
252
+ return [
253
+ rolePacket.prompt,
254
+
255
+ `Verify immutable bundle provenance before analyzing findings. Review the immutable evidence JSON at ${evidencePath ?? "<evidence-path>"}. 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:<path> 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.`,
256
+ `Limit repository inspection to the acceptance assertions and dirty_files contents in the immutable bundle. Do not explore unrelated repository paths.`,
257
+ `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.`,
258
+ `Do not edit files, create files, run mutating commands, or change Git state. Focus on correctness, regressions, security, and missing tests.`,
259
+ `Execution outcomes for every acceptance were verified deterministically by the Kernel QA layer before this review and are embedded in this bundle under outcomes (acceptance_id -> {status, summary}); do not re-execute descriptors and do not treat the absence of local test runs as a finding. Your review covers bundle provenance, code correctness, regressions, security, and missing tests against the embedded assertions and code bytes.`,
260
+ `Snapshot digest: ${digest}`,
261
+ `TaskRecord revision: ${snapshot.record_revision}`,
262
+ `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}.`,
263
+ "Acceptance assertions:", acceptance,
264
+ "Reserve the final turn for exactly one strict JSON verdict. Reply with ONLY that object, without markdown fences or commentary.",
265
+ `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":"<one line>"}}`,
266
+ `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|advisory","acceptance_id":"<id|null>","summary":"<one line>"}]}`,
267
+ ].join("\n");
268
+ }
269
+
270
+ export function parseAssuranceVerdict(text: string, snapshot: SnapshotDescriptor): AssuranceVerdict {
271
+ const cleaned = text.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("{") && line.endsWith("}")).join("");
272
+ if (!cleaned) throw new Error("child returned no strict JSON verdict");
273
+ let raw: Record<string, unknown>;
274
+ try { raw = JSON.parse(cleaned) as Record<string, unknown>; } catch { throw new Error("child verdict is not valid JSON"); }
275
+ const allowed = ["contract", "role", "task_id", "snapshot_digest", "decision", "approval", "findings"];
276
+ const unknown = Object.keys(raw).find((key) => !allowed.includes(key));
277
+ if (unknown) throw new Error(`child verdict has unknown field: ${unknown}`);
278
+ if (raw.contract !== "assurance_kernel/assurance_verdict/v2") throw new Error("assurance verdict contract is invalid");
279
+ if (raw.role !== snapshot.role) throw new Error("child verdict role mismatch");
280
+ if (raw.task_id !== snapshot.task_id) throw new Error("child verdict task mismatch");
281
+ if (raw.snapshot_digest !== snapshotDigest(snapshot)) throw new Error("child verdict snapshot digest mismatch");
282
+ if (raw.decision !== "pass" && raw.decision !== "rework") throw new Error("child verdict decision must be pass or rework");
283
+ if (raw.decision === "pass") {
284
+ const approval = raw.approval as Record<string, unknown> | undefined;
285
+ const expectedKind = snapshot.role === "qa" ? "qa" : "review";
286
+ const expectedRole = snapshot.role === "qa" ? "qa" : "reviewer";
287
+ if (!approval || approval.kind !== expectedKind || approval.authority_role !== expectedRole || typeof approval.summary !== "string" || !approval.summary.trim()) throw new Error("pass verdict approval is invalid");
288
+ if (raw.findings !== undefined) throw new Error("pass verdict must omit findings");
289
+ 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 } };
290
+ }
291
+ if (!Array.isArray(raw.findings) || raw.findings.length === 0 || raw.approval !== undefined) throw new Error("rework verdict findings are invalid");
292
+ const findings = raw.findings.map((item, index) => {
293
+ const finding = item as Record<string, unknown>;
294
+ const unknownFinding = Object.keys(finding).find((key) => !["id", "kind", "acceptance_id", "summary"].includes(key));
295
+ if (unknownFinding) throw new Error(`finding ${index} has unknown field: ${unknownFinding}`);
296
+ 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`);
297
+ const id = `review-${snapshotDigest(snapshot).slice(7, 19)}-${index + 1}-${finding.id.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 48)}`;
298
+ const normalized = { id, kind: finding.kind as "blocking" | "advisory", acceptance_id: finding.acceptance_id as string | null, summary: finding.summary as string };
299
+ return { ...normalized, findings_digest: findingsDigest([normalized]) };
300
+ });
301
+ return { contract: "assurance_kernel/assurance_verdict/v2", role: snapshot.role, task_id: snapshot.task_id, snapshot_digest: snapshotDigest(snapshot), decision: "rework", findings };
302
+ }
303
+
304
+ interface ReviewReservation {
305
+ taskId: string;
306
+ operationId: string;
307
+ correlation: AssuranceCorrelation;
308
+ snapshot: SnapshotDescriptor;
309
+ params: ReservedAgentParams;
310
+ evidence: { path: string; remove(): void };
311
+ toolCallId?: string;
312
+ resultObserved: boolean;
313
+ ended: boolean;
314
+ endedBeforeResult: boolean;
315
+ receipt?: NativeReviewResult;
316
+ receiptError?: string;
317
+ }
318
+
319
+ export const invocationRegistry = createInvocationRegistry();
320
+
321
+ export class AssuranceProgression {
322
+ private readonly activeOperations = new Map<string, string>();
323
+ private readonly operationControllers = new Map<string, { operationId: string; controller: AbortController }>();
324
+ private readonly reviewReservations = new Map<string, ReviewReservation>();
325
+ private readonly rejectedReviewOperations = new Map<string, { operationId: string; reason: string }>();
326
+ private readonly unknownOperations = new Map<string, { operation: "qa" | "review"; operationId: string; reason: string }>();
327
+ private readonly sessionInvocations = new Set<InvocationToken>();
328
+ private sessionActive = true;
329
+ private sessionGeneration = 0;
330
+
331
+ constructor(private readonly ports: AssuranceProgressionPorts) {}
332
+
333
+ onSessionStart(): void {
334
+ this.sessionActive = true;
335
+ this.sessionGeneration += 1;
336
+ }
337
+
338
+ async onSessionShutdown(): Promise<void> {
339
+ this.sessionActive = false;
340
+ this.sessionGeneration += 1;
341
+ for (const { controller } of this.operationControllers.values()) {
342
+ if (!controller.signal.aborted) controller.abort(new Error("session shutdown"));
343
+ }
344
+ this.operationControllers.clear();
345
+ this.activeOperations.clear();
346
+ this.rejectedReviewOperations.clear();
347
+ for (const reservation of this.reviewReservations.values()) this.removeEvidence(reservation);
348
+ this.reviewReservations.clear();
349
+ for (const invocation of [...this.sessionInvocations]) this.closeSessionInvocation(invocation);
350
+ }
351
+
352
+ observeToolCall(event: { toolName?: string; input?: unknown; toolCallId?: string }): { block: boolean; reason?: string } | undefined {
353
+ if (event.toolName !== STANDARD_AGENT_TOOL || !event.toolCallId) return undefined;
354
+ for (const reservation of this.reviewReservations.values()) {
355
+ if (!matchesReservedAgentArgs(event.input, reservation.params)) continue;
356
+ if (reservation.toolCallId || reservation.ended || reservation.resultObserved) {
357
+ return { block: true, reason: "reserved Review already has a native tool call" };
358
+ }
359
+ reservation.toolCallId = event.toolCallId;
360
+ return undefined;
361
+ }
362
+ return undefined;
363
+ }
364
+
365
+ observeToolResult(event: ToolResultLike): void {
366
+ if (event.toolName !== STANDARD_AGENT_TOOL || !event.toolCallId) return;
367
+ const reservation = this.reservationForCall(event.toolCallId);
368
+ if (!reservation || reservation.endedBeforeResult || reservation.resultObserved) return;
369
+ if (!matchesReservedAgentArgs(event.input ?? event.args, reservation.params)) return;
370
+ reservation.resultObserved = true;
371
+ try {
372
+ reservation.receipt = parseForegroundAgentResult(event, event.toolCallId);
373
+ if (nativeReviewResultIsFailure(reservation.receipt)) reservation.receiptError = `native Agent returned ${reservation.receipt.status}`;
374
+ } catch (error) {
375
+ reservation.receiptError = boundedAssuranceError(error);
376
+ }
377
+ }
378
+
379
+ observeToolEnd(event: ToolExecutionEndLike): void {
380
+ if (event.toolName !== STANDARD_AGENT_TOOL || !event.toolCallId) return;
381
+ const reservation = this.reservationForCall(event.toolCallId);
382
+ if (!reservation) return;
383
+ if (!reservation.resultObserved) reservation.endedBeforeResult = true;
384
+ reservation.ended = true;
385
+ }
386
+
387
+ active(taskId: string): ActiveAssuranceState | null {
388
+ const operationId = this.activeOperations.get(taskId);
389
+ if (operationId) return { state: "running", operation: "qa", operation_id: operationId, deadline_seconds: QA_JOB_TIMEOUT_SECONDS };
390
+ const reservation = this.reviewReservations.get(taskId);
391
+ if (reservation) return { state: "review_ready", operation: "review", operation_id: reservation.operationId };
392
+ const unknown = this.unknownOperations.get(taskId);
393
+ if (unknown) return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason };
394
+ return null;
395
+ }
396
+
397
+ openInvocation(taskId: string): InvocationToken {
398
+ const invocation = invocationRegistry.open(taskId);
399
+ this.sessionInvocations.add(invocation);
400
+ return invocation;
401
+ }
402
+ closeInvocation(invocation: InvocationToken): void { this.closeSessionInvocation(invocation); }
403
+ private closeSessionInvocation(invocation: InvocationToken): void {
404
+ try { invocationRegistry.cancel(invocation); } catch { /* already terminal */ }
405
+ this.sessionInvocations.delete(invocation);
406
+ }
407
+ commitInvocation(invocation: InvocationToken): void { invocationRegistry.commit(invocation); }
408
+ invocationState(invocation: InvocationToken): InvocationState { return invocationRegistry.stateOf(invocation); }
409
+ isInvocationOpen(taskId: string): boolean { return invocationRegistry.isOpen(taskId); }
410
+ sessionActiveValue(): boolean { return this.sessionActive; }
411
+ sessionGenerationValue(): number { return this.sessionGeneration; }
412
+
413
+ async advance(taskId: string, ctx: ExtensionContext, signal?: AbortSignal, onUpdate?: (update: ForegroundToolUpdate) => void): Promise<AssuranceAdvanceResult> {
414
+ const active = this.active(taskId);
415
+ if (active?.state === "review_ready") return this.reviewReadyResult(taskId);
416
+ if (active?.state === "settlement_unknown") return active;
417
+ if (active?.state === "running") return { state: "blocked", reason: `assurance operation ${active.operation_id} is already running` };
418
+ const operationId = randomUUID();
419
+ const operationGeneration = this.sessionGeneration;
420
+ const operationController = new AbortController();
421
+ const relayExternalAbort = () => operationController.abort(
422
+ signal?.reason instanceof Error ? signal.reason : new Error("assurance operation cancelled"),
423
+ );
424
+ signal?.addEventListener("abort", relayExternalAbort, { once: true });
425
+ if (signal?.aborted) relayExternalAbort();
426
+ this.activeOperations.set(taskId, operationId);
427
+ this.operationControllers.set(taskId, { operationId, controller: operationController });
428
+ this.rejectedReviewOperations.delete(taskId);
429
+ let authorityCommitted = false;
430
+ let authorityBoundaryStarted = false;
431
+ let phase = "preparing";
432
+ const operationLive = () => this.sessionActive
433
+ && this.sessionGeneration === operationGeneration
434
+ && this.activeOperations.get(taskId) === operationId
435
+ && !operationController.signal.aborted;
436
+ const ensureOperationLive = () => {
437
+ if (!operationLive()) throw new VerificationAbortedError();
438
+ };
439
+ const progress = (stage: string, summary: string, details: Record<string, unknown> = {}) => {
440
+ phase = stage;
441
+ onUpdate?.({ content: [{ type: "text", text: summary }], details: { state: "running", operation: "qa", operation_id: operationId, stage, ...details } });
442
+ };
443
+ const aborted = () => operationController.signal.aborted;
444
+ try {
445
+ ensureOperationLive();
446
+ progress(phase, `Preparing deterministic QA for ${taskId}`);
447
+ await this.ports.advanceBeforeProjection?.();
448
+ ensureOperationLive();
449
+ let projection = await this.ports.projectTask(ctx.cwd, taskId);
450
+ ensureOperationLive();
451
+ if (projection.error) return { state: "blocked", reason: projection.error };
452
+ if (projection.projection.lifecycle === "done") return { state: "completed" };
453
+ if (projection.projection.lifecycle === "stopped") return { state: "stopped" };
454
+ if (!projection.claim) return { state: "blocked", reason: "no active backend claim" };
455
+ if (projection.claim.task_id !== taskId) return { state: "blocked", reason: `backend claim belongs to ${projection.claim.task_id}, not ${taskId}` };
456
+ const parked = await this.ports.readTaskRecord(ctx.cwd, taskId);
457
+ ensureOperationLive();
458
+ if (parked.record?.findings.some((finding) => finding.kind === "replan_required" && finding.status === "open")) return { state: "blocked", reason: "review rework limit reached; a durable replan is required" };
459
+ if (projection.projection.artifact_state === "active") {
460
+ if (projection.projection.next_obligation !== "submit_assurance")
461
+ return { state: "blocked", reason: `Kernel requires ${projection.projection.next_obligation}` };
462
+ if (aborted()) return this.cancelled("qa", operationId, "host cancellation before artifact freeze");
463
+ progress("freezing_artifacts", "Freezing planning artifacts for deterministic assurance");
464
+ const freeze = this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "freeze_artifacts", actor_id: "executor" } });
465
+ authorityBoundaryStarted = true;
466
+ await freeze;
467
+ authorityCommitted = true;
468
+ ensureOperationLive();
469
+ projection = await this.ports.projectTask(ctx.cwd, taskId);
470
+ ensureOperationLive();
471
+ if (projection.error || projection.projection.lifecycle !== "active" || projection.projection.artifact_state !== "frozen")
472
+ return this.unknownAfterCommit(taskId, "qa", operationId, projection.error ?? "artifact freeze did not settle");
473
+ }
474
+ if (projection.projection.next_obligation === "complete") {
475
+ progress("completing", "Completing the routine task after deterministic QA");
476
+ try {
477
+ await this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "complete", actor_id: "kernel-assurance" } });
478
+ return { state: "completed" };
479
+ } catch (error) {
480
+ return this.unknownAfterCommit(taskId, "qa", operationId, boundedAssuranceError(error));
481
+ }
482
+ }
483
+ if (projection.projection.next_obligation === "authorize_user")
484
+ return { state: "awaiting_user", operation: "record-user-approval", operation_id: operationId };
485
+ if (projection.projection.next_obligation !== "run_qa" && projection.projection.next_obligation !== "run_review")
486
+ return { state: "blocked", reason: `Kernel requires ${projection.projection.next_obligation}` };
487
+ const qaAlreadySettled = projection.projection.next_obligation === "run_review";
488
+ const runner = await this.ports.frozenRunner();
489
+ ensureOperationLive();
490
+ let qaVerdict: AssuranceVerdict | undefined;
491
+ if (qaAlreadySettled) {
492
+ progress("resuming_review", "Resuming Review preparation from the Kernel assurance projection");
493
+ } else {
494
+ progress("capturing_snapshot", "Capturing the immutable QA snapshot");
495
+ await this.ports.qaBeforeProjection?.();
496
+ ensureOperationLive();
497
+ const assurance = await this.ports.buildAssurance(ctx.cwd, taskId, "qa", projection, runner);
498
+ ensureOperationLive();
499
+ qaVerdict = await this.ports.runQa(assurance.snapshot, assurance.descriptors, runner, {
500
+ signal: operationController.signal,
501
+ 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 }),
502
+ });
503
+ ensureOperationLive();
504
+ const invocation = this.openInvocation(taskId);
505
+ try {
506
+ progress("settling_qa", "Settling deterministic QA through the Kernel revision boundary");
507
+ authorityBoundaryStarted = true;
508
+ await this.ports.applyVerdict(ctx, {
509
+ taskId,
510
+ snapshot: assurance.snapshot,
511
+ verdict: qaVerdict,
512
+ invocation,
513
+ actorId: "deterministic-qa",
514
+ hooks: {
515
+ beforeCommit: async () => {
516
+ ensureOperationLive();
517
+ await this.ports.qaBeforeAuthorityCommit?.();
518
+ ensureOperationLive();
519
+ },
520
+ onCommit: () => { authorityCommitted = true; this.ports.qaOnAuthorityCommit?.(); },
521
+ afterCommit: async () => {
522
+ await this.ports.qaAfterAuthorityCommit?.();
523
+ },
524
+ },
525
+ });
526
+ ensureOperationLive();
527
+ } catch (error) {
528
+ if (!authorityCommitted && (aborted() || error instanceof VerificationAbortedError)) return this.cancelled("qa", operationId, "host cancellation before QA authority commit");
529
+ return authorityCommitted
530
+ ? this.unknownAfterCommit(taskId, "qa", operationId, boundedAssuranceError(error))
531
+ : { state: "failed", operation: "qa", operation_id: operationId, reason: boundedAssuranceError(error) };
532
+ } finally {
533
+ if (this.invocationState(invocation) === "open") this.closeInvocation(invocation);
534
+ }
535
+ }
536
+ if (qaVerdict?.decision === "rework") return { state: "rework", operation: "qa", operation_id: operationId, summary: qaVerdict.findings?.map((finding) => finding.summary).join("; ") ?? "deterministic QA requested rework" };
537
+ ensureOperationLive();
538
+ progress("preparing_review", "Preparing the reserved foreground Review bundle");
539
+ const fresh = await this.ports.projectTask(ctx.cwd, taskId);
540
+ ensureOperationLive();
541
+ if (fresh.error || !fresh.claim) return this.unknownAfterCommit(taskId, "qa", operationId, fresh.error ?? "claim disappeared after QA settlement");
542
+ if (fresh.projection.next_obligation === "complete") {
543
+ progress("completing", "Deterministic QA passed; completing the routine task");
544
+ try {
545
+ await this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "complete", actor_id: "kernel-assurance" } });
546
+ return { state: "completed" };
547
+ } catch (error) {
548
+ return this.unknownAfterCommit(taskId, "qa", operationId, boundedAssuranceError(error));
549
+ }
550
+ }
551
+ if (fresh.projection.next_obligation === "authorize_user")
552
+ return { state: "awaiting_user", operation: "record-user-approval", operation_id: operationId };
553
+ if (fresh.projection.next_obligation !== "run_review")
554
+ return { state: "blocked", reason: `Kernel requires ${fresh.projection.next_obligation} after QA` };
555
+ const review = await this.ports.buildAssurance(ctx.cwd, taskId, "review", fresh, runner);
556
+ ensureOperationLive();
557
+ if (!review.reviewBundle) return this.unknownAfterCommit(taskId, "qa", operationId, "review bundle is missing after QA settlement");
558
+ const evidence = this.ports.writeReviewEvidence({ snapshot: review.snapshot, review_bundle: review.reviewBundle });
559
+ const reservation: ReviewReservation = {
560
+ taskId,
561
+ operationId,
562
+ correlation: { record_revision: review.snapshot.record_revision, intent_content_hash: review.snapshot.intent_content_hash, diff_hash: review.snapshot.diff_hash },
563
+ snapshot: review.snapshot,
564
+ params: { subagent_type: "Review", description: "", prompt: "", inherit_context: false, isolated: true, isolation: "worktree", run_in_background: false, max_turns: 0 },
565
+ evidence,
566
+ resultObserved: false,
567
+ ended: false,
568
+ endedBeforeResult: false,
569
+ };
570
+ this.reviewReservations.set(taskId, reservation);
571
+ try {
572
+ ensureOperationLive();
573
+ const workload = classifyReviewWorkload(review.snapshot, review.reviewBundle);
574
+ reservation.params = reservedAgentParams({ taskId, operationId, prompt: buildReviewPrompt(review.snapshot, evidence.path), max_turns: reviewTurnBudget(workload) });
575
+ ensureOperationLive();
576
+ } catch (error) {
577
+ this.releaseReviewReservation(taskId, reservation);
578
+ throw error;
579
+ }
580
+ 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: reservation.params });
581
+ return { state: "review_ready", operation: "review", operation_id: operationId, snapshot_digest: snapshotDigest(review.snapshot), review_bundle_digest: review.snapshot.review_bundle_digest ?? "", agent_params: reservation.params };
582
+ } catch (error) {
583
+ if (authorityCommitted || (authorityBoundaryStarted && aborted())) return this.unknownAfterCommit(taskId, "qa", operationId, `${phase}: ${boundedAssuranceError(error)}`);
584
+ if (aborted() || error instanceof VerificationAbortedError) return this.cancelled("qa", operationId, `${phase}: host cancellation`);
585
+ return { state: "failed", operation: "qa", operation_id: operationId, reason: `${phase}: ${boundedAssuranceError(error)}` };
586
+ } finally {
587
+ if (this.activeOperations.get(taskId) === operationId) this.activeOperations.delete(taskId);
588
+ const controller = this.operationControllers.get(taskId);
589
+ if (controller?.operationId === operationId) this.operationControllers.delete(taskId);
590
+ signal?.removeEventListener("abort", relayExternalAbort);
591
+ }
592
+ }
593
+
594
+ async submitReview(taskId: string, ctx: ExtensionContext): Promise<AssuranceSubmitReviewResult> {
595
+ const unknown = this.unknownOperations.get(taskId);
596
+ if (unknown) { this.unknownOperations.delete(taskId); return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason }; }
597
+ const rejected = this.rejectedReviewOperations.get(taskId);
598
+ if (rejected) return { state: "blocked", reason: rejected.reason };
599
+ const reservation = this.reviewReservations.get(taskId);
600
+ if (!reservation) return { state: "blocked", reason: "no reserved foreground Review operation" };
601
+ if (!reservation.toolCallId) return { state: "blocked", reason: "reserved foreground Agent was not observed" };
602
+ if (reservation.endedBeforeResult) {
603
+ const reason = "foreground Agent terminal event arrived before its result";
604
+ this.releaseReviewReservation(taskId, reservation, reason);
605
+ return { state: "blocked", reason };
606
+ }
607
+ if (!reservation.ended) return { state: "blocked", reason: "foreground Agent terminal event order is incomplete" };
608
+ if (reservation.receiptError || !reservation.receipt) {
609
+ const reason = reservation.receiptError ?? "foreground Agent result is missing";
610
+ this.releaseReviewReservation(taskId, reservation, reason);
611
+ return { state: "blocked", reason };
612
+ }
613
+ let fresh: AssuranceProjectionResult;
614
+ try {
615
+ fresh = await this.ports.projectTask(ctx.cwd, taskId);
616
+ } catch (error) {
617
+ const reason = boundedAssuranceError(error);
618
+ this.releaseReviewReservation(taskId, reservation, reason);
619
+ return { state: "blocked", reason };
620
+ }
621
+ if (fresh.error || !fresh.claim || fresh.claim.task_id !== taskId || fresh.projection.record_revision !== reservation.snapshot.record_revision || fresh.projection.workspace_revision !== reservation.snapshot.workspace_revision || fresh.projection.intent_revision !== reservation.snapshot.intent_revision || fresh.projection.intent_content_hash !== reservation.snapshot.intent_content_hash || fresh.projection.diff_hash !== reservation.snapshot.diff_hash || fresh.projection.lifecycle !== reservation.snapshot.lifecycle || fresh.projection.artifact_state !== reservation.snapshot.artifact_state) {
622
+ const reason = fresh.error ?? "assurance snapshot changed before Review submission";
623
+ this.releaseReviewReservation(taskId, reservation, reason);
624
+ return { state: "blocked", reason };
625
+ }
626
+ let verdict: AssuranceVerdict;
627
+ try { verdict = parseAssuranceVerdict(reservation.receipt.result, reservation.snapshot); }
628
+ catch (error) {
629
+ const reason = boundedAssuranceError(error);
630
+ this.releaseReviewReservation(taskId, reservation, reason);
631
+ return { state: "blocked", reason };
632
+ }
633
+ const invocation = this.openInvocation(taskId);
634
+ this.releaseReviewReservation(taskId, reservation);
635
+ try {
636
+ await this.ports.applyVerdict(ctx, {
637
+ taskId,
638
+ snapshot: reservation.snapshot,
639
+ verdict,
640
+ invocation,
641
+ actorId: `native-review-${reservation.receipt.agentId}`,
642
+ });
643
+ } catch (error) {
644
+ const reason = boundedAssuranceError(error);
645
+ return this.invocationState(invocation) === "committed"
646
+ ? this.unknownAfterCommit(taskId, "review", reservation.operationId, reason)
647
+ : { state: "blocked", reason };
648
+ } finally {
649
+ this.closeInvocation(invocation);
650
+ }
651
+ if (verdict.decision === "rework") {
652
+ return {
653
+ state: "rework",
654
+ operation: "review",
655
+ operation_id: reservation.operationId,
656
+ summary: verdict.findings?.map((finding) => finding.summary).join("; ") ?? "independent Review requested rework",
657
+ };
658
+ }
659
+ const settled = await this.ports.projectTask(ctx.cwd, taskId);
660
+ if (settled.error || !settled.claim)
661
+ return this.unknownAfterCommit(taskId, "review", reservation.operationId, settled.error ?? "claim disappeared after Review settlement");
662
+ if (settled.projection.next_obligation === "complete") {
663
+ try {
664
+ await this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "complete", actor_id: "kernel-assurance" } });
665
+ return { state: "completed" };
666
+ } catch (error) {
667
+ return this.unknownAfterCommit(taskId, "review", reservation.operationId, boundedAssuranceError(error));
668
+ }
669
+ }
670
+ if (settled.projection.next_obligation === "authorize_user")
671
+ return { state: "awaiting_user", operation: "record-user-approval", operation_id: reservation.operationId };
672
+ return { state: "blocked", reason: `Kernel requires ${settled.projection.next_obligation} after Review` };
673
+ }
674
+
675
+ private reviewReadyResult(taskId: string): AssuranceAdvanceResult {
676
+ const reservation = this.reviewReservations.get(taskId);
677
+ if (!reservation) return { state: "blocked", reason: "Review reservation disappeared" };
678
+ 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.params };
679
+ }
680
+
681
+ private reservationForCall(toolCallId: string): ReviewReservation | undefined {
682
+ for (const reservation of this.reviewReservations.values()) if (reservation.toolCallId === toolCallId) return reservation;
683
+ return undefined;
684
+ }
685
+
686
+ private releaseReviewReservation(taskId: string, reservation: ReviewReservation, rejectionReason?: string): void {
687
+ if (this.reviewReservations.get(taskId) !== reservation) return;
688
+ this.reviewReservations.delete(taskId);
689
+ if (rejectionReason) this.rejectedReviewOperations.set(taskId, { operationId: reservation.operationId, reason: rejectionReason });
690
+ this.removeEvidence(reservation);
691
+ }
692
+
693
+ private removeEvidence(reservation: ReviewReservation): void {
694
+ try { reservation.evidence.remove(); } catch { /* cleanup cannot create authority */ }
695
+ }
696
+
697
+ private cancelled(operation: "qa" | "review", operationId: string, reason: string): AssuranceAdvanceResult {
698
+ return { state: "cancelled", operation, operation_id: operationId, reason };
699
+ }
700
+
701
+ private unknownAfterCommit(taskId: string, operation: "qa" | "review", operationId: string, reason: string): AssuranceAdvanceResult {
702
+ this.unknownOperations.set(taskId, { operation, operationId, reason });
703
+ return { state: "settlement_unknown", operation, operation_id: operationId, reason };
704
+ }
705
+ }
706
+
707
+ function boundedAssuranceError(error: unknown): string {
708
+ const raw = error instanceof Error ? error.message : String(error);
709
+ const normalized = raw.replace(/\s+/g, " ").trim() || "unknown error";
710
+ return normalized.length <= 300 ? normalized : `${normalized.slice(0, 299)}...`;
711
+ }