immune-brain 2.8.3 → 3.0.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 (66) hide show
  1. package/.claude-plugin/marketplace.json +16 -0
  2. package/README.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/package.json +9 -2
  5. package/plugins/immune-brain/.claude-plugin/plugin.json +8 -0
  6. package/plugins/immune-brain/.mcp.json +8 -0
  7. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +354 -64
  8. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +74 -706
  9. package/plugins/immune-brain/.pi-extension/pi-canary-invocations.ts +1 -90
  10. package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +13 -160
  11. package/plugins/immune-brain/.pi-extension/pi-canary-qa-findings.ts +1 -50
  12. package/plugins/immune-brain/.pi-extension/pi-canary-review-bundle.ts +1 -262
  13. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +3 -2
  14. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +8 -229
  15. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +23 -5
  16. package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -0
  17. package/plugins/immune-brain/dist/claude/mcp-server.mjs +7514 -0
  18. package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +58 -0
  19. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +1 -1
  20. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  21. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +19 -13
  22. package/plugins/immune-brain/dist/imm-loop.md +6 -7
  23. package/plugins/immune-brain/dist/imm-planner.md +1 -1
  24. package/plugins/immune-brain/dist/imm-pr-fix.md +9 -0
  25. package/plugins/immune-brain/dist/role-prompts/code-review.md +33 -13
  26. package/plugins/immune-brain/dist/role-prompts/executor.md +14 -0
  27. package/plugins/immune-brain/dist/role-prompts/pr-fix.md +9 -0
  28. package/plugins/immune-brain/dist/role-prompts/test-fixer.md +7 -0
  29. package/plugins/immune-brain/hooks/hooks.json +55 -0
  30. package/plugins/immune-brain/runtime/assurance/coordinator.ts +836 -0
  31. package/plugins/immune-brain/runtime/assurance/enrollment.ts +6 -0
  32. package/plugins/immune-brain/runtime/assurance/host_port.ts +18 -0
  33. package/plugins/immune-brain/runtime/assurance/invocations.ts +90 -0
  34. package/plugins/immune-brain/runtime/assurance/qa_findings.ts +50 -0
  35. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +596 -0
  36. package/plugins/immune-brain/runtime/assurance/verification.ts +233 -0
  37. package/plugins/immune-brain/runtime/claude/capability.ts +67 -0
  38. package/plugins/immune-brain/runtime/claude/interaction.ts +70 -0
  39. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +789 -0
  40. package/plugins/immune-brain/runtime/claude/mcp_server.ts +363 -0
  41. package/plugins/immune-brain/runtime/claude/review_host.ts +645 -0
  42. package/plugins/immune-brain/runtime/commands/kernel.ts +221 -3
  43. package/plugins/immune-brain/runtime/github_issue_tracker.ts +2 -2
  44. package/plugins/immune-brain/runtime/kernel/application.ts +8 -5
  45. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +24 -13
  46. package/plugins/immune-brain/runtime/kernel/authority_port.ts +78 -115
  47. package/plugins/immune-brain/runtime/kernel/canary_application.ts +6 -4
  48. package/plugins/immune-brain/runtime/kernel/capability_registry.ts +89 -0
  49. package/plugins/immune-brain/runtime/kernel/completion.ts +64 -6
  50. package/plugins/immune-brain/runtime/kernel/enrollment.ts +189 -100
  51. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +37 -80
  52. package/plugins/immune-brain/runtime/kernel/intent.ts +24 -0
  53. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +31 -0
  54. package/plugins/immune-brain/runtime/kernel/reducer.ts +36 -13
  55. package/plugins/immune-brain/runtime/kernel/storage.ts +16 -16
  56. package/plugins/immune-brain/runtime/kernel/types.ts +32 -2
  57. package/plugins/immune-brain/runtime/kernel/validation.ts +107 -16
  58. package/plugins/immune-brain/runtime/loop_contract.ts +17 -2
  59. package/plugins/immune-brain/runtime/prompts/code-review.md +33 -13
  60. package/plugins/immune-brain/runtime/prompts/executor.md +14 -0
  61. package/plugins/immune-brain/runtime/prompts/pr-fix.md +9 -0
  62. package/plugins/immune-brain/runtime/prompts/test-fixer.md +7 -0
  63. package/plugins/immune-brain/runtime/v4_runtime.ts +5 -2
  64. package/plugins/immune-brain/runtime/workspace_scope.ts +191 -5
  65. package/plugins/immune-brain/skills/imm-loop/SKILL.md +3 -4
  66. package/plugins/immune-brain/skills/imm-planner/SKILL.md +2 -0
@@ -0,0 +1,789 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { execFileSync } from "node:child_process";
4
+ import { join } from "node:path";
5
+ import {
6
+ AssuranceCoordinator,
7
+ snapshotDigest,
8
+ type AssuranceCoordinatorPorts,
9
+ type AssuranceSubmitReviewResult,
10
+ type AssuranceVerdict,
11
+ type HostContext,
12
+ type SnapshotDescriptor,
13
+ } from "../assurance/coordinator";
14
+ import {
15
+ assertRunnerCompatible,
16
+ findingsDigest,
17
+ resolveBunRunner,
18
+ runFixedVerification,
19
+ VerificationAbortedError,
20
+ type FrozenRunner,
21
+ type VerificationDescriptor,
22
+ } from "../assurance/verification";
23
+ import {
24
+ captureReviewBundle,
25
+ captureReviewManifest,
26
+ ensureReviewRevision,
27
+ writeNativeReviewEvidence,
28
+ } from "../assurance/review_evidence";
29
+ import { parseVerificationDescriptor } from "../verification_descriptor";
30
+ import { projectAssurance, type AssuranceProjectionResult } from "../kernel/assurance_projection";
31
+ import type { TaskRecord } from "../kernel/types";
32
+ import { readTaskRecord } from "../kernel/storage";
33
+ import { canonicalIntentHash, parseTaskIntentV1, readTaskIntent } from "../kernel/intent";
34
+ import { capabilityActionFor, createCanaryApplication } from "../kernel/canary_application";
35
+ import {
36
+ createMutationAuthorityRegistry,
37
+ digestOfAction,
38
+ type CapabilityBindingV2,
39
+ type MutationAuthorityRegistry,
40
+ } from "../kernel/authority_port";
41
+ import {
42
+ createEnrollmentAuthorityRegistry,
43
+ type EnrollmentCapabilityBinding,
44
+ } from "../kernel/enrollment_authority";
45
+ import { enrollCanaryTask, runEnrollmentRehearsal } from "../kernel/enrollment";
46
+ import { reconcileKernelAuthority, repairKernelAuthority } from "../kernel/storage";
47
+ import { preparePiCanary, revalidatePiCanary } from "../kernel/pi_canary_prepare";
48
+ import { qaFindingId } from "../assurance/qa_findings";
49
+ import { taskDiffIdentity, taskRevisionIdentity } from "../workspace_scope";
50
+ import { confirmationRef, enrollmentNonce, evaluateNativeGate, isPrivilegedOperation, type NativeDecision } from "./interaction";
51
+ import { ClaudeReviewHost, FileHookEventLog, type ClaudeHookEvent } from "./review_host";
52
+ import { probeHost, type PermissionMode } from "./capability";
53
+
54
+ export function diffSnapshotOf(root: string, record: TaskRecord): {
55
+ diff_hash: string;
56
+ changed_paths: readonly string[];
57
+ } {
58
+ if (record.contract === "assurance_kernel/task_record/v4") {
59
+ if (!record.git_base_head) throw new Error("TaskRecord v4 is missing git_base_head");
60
+ return taskRevisionIdentity(root, record.intent_snapshot.scope_hint, record.git_base_head);
61
+ }
62
+ return taskDiffIdentity(root, record.intent_snapshot.scope_hint);
63
+ }
64
+
65
+ export function diffHashOf(root: string, record: TaskRecord): string {
66
+ return diffSnapshotOf(root, record).diff_hash;
67
+ }
68
+
69
+ function extractVerdictJson(input: unknown): Record<string, unknown> | null {
70
+ if (typeof input === "string") {
71
+ const cleaned = input.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("{") && line.endsWith("}")).join("");
72
+ if (!cleaned) return null;
73
+ try { return JSON.parse(cleaned) as Record<string, unknown>; } catch { return null; }
74
+ }
75
+ if (typeof input === "object" && input !== null && !Array.isArray(input)) return input as Record<string, unknown>;
76
+ return null;
77
+ }
78
+
79
+ function verdictFingerprint(raw: Record<string, unknown>): string {
80
+ return JSON.stringify({
81
+ contract: raw.contract ?? null,
82
+ role: raw.role ?? null,
83
+ task_id: raw.task_id ?? null,
84
+ snapshot_digest: raw.snapshot_digest ?? null,
85
+ decision: raw.decision ?? null,
86
+ approval: raw.approval ?? null,
87
+ findings: raw.findings ?? null,
88
+ });
89
+ }
90
+
91
+ export async function submitClaudeReview(
92
+ host: ClaudeReviewHost,
93
+ coordinator: AssuranceCoordinator,
94
+ ctx: HostContext,
95
+ taskId: string,
96
+ verdictInput: unknown,
97
+ ): Promise<AssuranceSubmitReviewResult> {
98
+ if (verdictInput === undefined) throw new Error("verdict is required");
99
+ const observed = host.inspectReviewForTask(taskId);
100
+ if (!observed.ok) {
101
+ if (observed.release) return coordinator.abandonReview(taskId, observed.reason);
102
+ return { state: "blocked", reason: observed.reason };
103
+ }
104
+ const parentJson = extractVerdictJson(verdictInput);
105
+ const receiptJson = extractVerdictJson(observed.receipt.result);
106
+ if (parentJson && receiptJson && verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
107
+ return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
108
+ }
109
+ if (parentJson && !receiptJson) {
110
+ return { state: "blocked", reason: "reviewer receipt is not a valid verdict" };
111
+ }
112
+ return coordinator.submitReview(taskId, ctx, verdictInput);
113
+ }
114
+
115
+ function assertProjectionBinding(before: AssuranceProjectionResult, after: AssuranceProjectionResult, allowDiffChange = false): void {
116
+ const fields = (allowDiffChange
117
+ ? ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash"]
118
+ : ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash", "diff_hash"]) as const;
119
+ if (before.error || !before.claim || after.error || !after.claim || before.claim.task_id !== after.claim.task_id
120
+ || fields.some((field) => before.projection[field] !== after.projection[field])) {
121
+ throw new Error("Task changed after native confirmation; authority aborted before capability issuance");
122
+ }
123
+ }
124
+
125
+ export async function runDeterministicQa(
126
+ snapshot: SnapshotDescriptor,
127
+ descriptors: Map<string, VerificationDescriptor>,
128
+ runner: FrozenRunner,
129
+ options: { signal?: AbortSignal; onProgress?: (progress: { index: number; total: number; acceptance_id: string; phase: "running" | "passed" | "failed"; elapsed_ms: number }) => void } = {},
130
+ ): Promise<AssuranceVerdict> {
131
+ if (snapshot.role !== "qa") throw new Error("deterministic QA requires qa role");
132
+ if (options.signal?.aborted) throw new VerificationAbortedError();
133
+ const findings: NonNullable<AssuranceVerdict["findings"]> = [];
134
+ for (const [offset, item] of snapshot.acceptance.entries()) {
135
+ if (options.signal?.aborted) throw new VerificationAbortedError();
136
+ const descriptor = descriptors.get(item.id);
137
+ if (!descriptor) throw new Error(`verification descriptor missing for ${item.id}`);
138
+ const startedAt = Date.now();
139
+ options.onProgress?.({ index: offset + 1, total: snapshot.acceptance.length, acceptance_id: item.id, phase: "running", elapsed_ms: 0 });
140
+ const result = await runFixedVerification(snapshot.root, descriptor, runner, { signal: options.signal });
141
+ const failed = result.exit_code !== 0 || result.timed_out;
142
+ options.onProgress?.({ index: offset + 1, total: snapshot.acceptance.length, acceptance_id: item.id, phase: failed ? "failed" : "passed", elapsed_ms: Date.now() - startedAt });
143
+ if (failed) {
144
+ findings.push({
145
+ id: qaFindingId(item.id, snapshotDigest(snapshot)),
146
+ kind: "blocking",
147
+ acceptance_id: item.id,
148
+ summary: `verification failed (exit ${result.exit_code}${result.timed_out ? ", timed out" : ""})`,
149
+ findings_digest: "",
150
+ });
151
+ }
152
+ }
153
+ if (findings.length > 0) {
154
+ return { contract: "assurance_kernel/assurance_verdict/v2", role: "qa", task_id: snapshot.task_id, snapshot_digest: snapshotDigest(snapshot), decision: "rework", findings };
155
+ }
156
+ return {
157
+ contract: "assurance_kernel/assurance_verdict/v2",
158
+ role: "qa",
159
+ task_id: snapshot.task_id,
160
+ snapshot_digest: snapshotDigest(snapshot),
161
+ decision: "pass",
162
+ approval: { kind: "qa", authority_role: "qa", summary: `all ${snapshot.acceptance.length} fixed verification descriptor(s) passed` },
163
+ };
164
+ }
165
+
166
+ function qaOutcomes(record: { attestations: Array<{ kind: string; acceptance_results: Array<{ acceptance_id: string; status: "passed" | "failed" | "blocked"; summary: string }> }> }) {
167
+ return Object.fromEntries(
168
+ record.attestations.filter((item) => item.kind === "qa").flatMap((item) => item.acceptance_results)
169
+ .map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]),
170
+ );
171
+ }
172
+
173
+ async function buildAssuranceSnapshot(
174
+ root: string,
175
+ taskId: string,
176
+ role: "qa" | "review",
177
+ projection: AssuranceProjectionResult,
178
+ runner: FrozenRunner,
179
+ ) {
180
+ const record = await readTaskRecord(root, taskId);
181
+ if (!record.record || record.revision !== projection.projection.record_revision) throw new Error("TaskRecord changed before assurance snapshot capture");
182
+ const intent = record.record.intent_snapshot;
183
+ const descriptors = new Map<string, VerificationDescriptor>();
184
+ for (const item of intent.acceptance) {
185
+ const descriptor = parseVerificationDescriptor(item.verification);
186
+ assertRunnerCompatible(descriptor, runner);
187
+ descriptors.set(item.id, descriptor);
188
+ }
189
+ const v4 = record.record.contract === "assurance_kernel/task_record/v4";
190
+ const reviewBundle = role === "review" && !v4
191
+ ? captureReviewBundle(root, intent.scope_hint, projection.projection.diff_hash, qaOutcomes(record.record))
192
+ : null;
193
+ const reviewManifest = role === "review" && v4
194
+ ? captureReviewManifest(root, {
195
+ taskId,
196
+ baseHead: record.record.git_base_head,
197
+ scopeHint: intent.scope_hint,
198
+ expectedDiffHash: projection.projection.diff_hash,
199
+ intentRevision: projection.projection.intent_revision,
200
+ intentContentHash: projection.projection.intent_content_hash,
201
+ recordRevision: projection.projection.record_revision,
202
+ workspaceRevision: projection.projection.workspace_revision,
203
+ lifecycle: projection.projection.lifecycle,
204
+ artifactState: projection.projection.artifact_state,
205
+ risk: intent.risk,
206
+ outcomes: qaOutcomes(record.record),
207
+ })
208
+ : null;
209
+ const dirtyFiles = reviewManifest ? Object.keys(reviewManifest.changed_paths) : reviewBundle ? Object.keys(reviewBundle.dirty_files) : [];
210
+ const snapshot: SnapshotDescriptor = {
211
+ contract: "assurance_kernel/assurance_snapshot/v2",
212
+ task_id: taskId,
213
+ role,
214
+ record_revision: projection.projection.record_revision,
215
+ workspace_revision: projection.projection.workspace_revision,
216
+ intent_revision: projection.projection.intent_revision,
217
+ intent_content_hash: projection.projection.intent_content_hash,
218
+ diff_hash: projection.projection.diff_hash,
219
+ lifecycle: projection.projection.lifecycle,
220
+ artifact_state: projection.projection.artifact_state,
221
+ risk: intent.risk,
222
+ fresh_acceptance_ids: projection.projection.fresh_acceptance_ids,
223
+ missing_acceptance_ids: projection.projection.missing_acceptance_ids,
224
+ stale_attestation_ids: projection.projection.stale_attestation_ids,
225
+ acceptance: intent.acceptance,
226
+ dirty_files: dirtyFiles,
227
+ review_bundle_digest: reviewManifest?.manifest_digest ?? reviewBundle?.bundle_digest ?? null,
228
+ root,
229
+ ...(reviewManifest
230
+ ? {
231
+ review_revision: {
232
+ contract: "assurance_kernel/review_revision_identity/v1",
233
+ base_head: reviewManifest.base_head,
234
+ review_commit: reviewManifest.review_commit,
235
+ review_tree: reviewManifest.review_tree,
236
+ manifest_digest: reviewManifest.manifest_digest,
237
+ },
238
+ }
239
+ : {}),
240
+ };
241
+ return { snapshot, descriptors, reviewBundle, reviewManifest };
242
+ }
243
+
244
+ function stagePlanningArtifactTransition(root: string, record: { intent_ref: { path: string }; intent_snapshot: { scope_hint: string[] } }): void {
245
+ const intentActive = record.intent_ref.path.replace("docs/plans/archive/", "docs/plans/");
246
+ const intentArchive = intentActive.replace("docs/plans/", "docs/plans/archive/");
247
+ const specActive = record.intent_snapshot.scope_hint.find((path) =>
248
+ /^docs\/specs\/(?!archive\/)[^/]+\.spec\.md$/.test(path)
249
+ && record.intent_snapshot.scope_hint.includes(path.replace("docs/specs/", "docs/specs/archive/")),
250
+ );
251
+ const candidates = [
252
+ intentActive,
253
+ intentArchive,
254
+ ...(specActive ? [specActive, specActive.replace("docs/specs/", "docs/specs/archive/")] : []),
255
+ ];
256
+ const paths = candidates.filter((path) => existsSync(join(root, path)) || execFileSync("git", ["ls-files", "--cached", "--", path], { cwd: root, encoding: "utf8" }).trim().length > 0);
257
+ if (paths.length === 0) return;
258
+ execFileSync("git", ["add", "--", ...paths], { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
259
+ }
260
+
261
+ async function mintCapability(
262
+ registry: MutationAuthorityRegistry,
263
+ input: {
264
+ authority_kind: "review" | "qa" | "user";
265
+ task_id: string;
266
+ action_kind: string;
267
+ expected_record_hash: string;
268
+ intent_revision: number;
269
+ intent_content_hash: string;
270
+ diff_hash: string;
271
+ actor_id: string;
272
+ findings?: unknown[];
273
+ approval?: unknown;
274
+ next_intent?: unknown;
275
+ next_intent_ref?: unknown;
276
+ reason?: string;
277
+ finding_id?: string;
278
+ resolution?: string;
279
+ now: string;
280
+ confirmation_ref: string;
281
+ },
282
+ ) {
283
+ const action = capabilityActionFor({
284
+ op: input.action_kind,
285
+ task_id: input.task_id,
286
+ at: input.now,
287
+ actor_id: input.actor_id,
288
+ ...(input.reason !== undefined ? { reason: input.reason } : {}),
289
+ ...(input.findings !== undefined ? { findings: input.findings } : {}),
290
+ ...(input.approval !== undefined ? { approval: input.approval } : {}),
291
+ ...(input.next_intent !== undefined ? { next_intent: input.next_intent } : {}),
292
+ ...(input.next_intent_ref !== undefined ? { next_intent_ref: input.next_intent_ref } : {}),
293
+ ...(input.finding_id !== undefined ? { finding_id: input.finding_id } : {}),
294
+ ...(input.resolution !== undefined ? { resolution: input.resolution } : {}),
295
+ });
296
+ const binding: CapabilityBindingV2 = {
297
+ authority_kind: input.authority_kind,
298
+ task_id: input.task_id,
299
+ action_digest: digestOfAction(action),
300
+ expected_record_hash: input.expected_record_hash,
301
+ intent_revision: input.intent_revision,
302
+ intent_content_hash: input.intent_content_hash,
303
+ diff_hash: input.diff_hash,
304
+ actor_id: input.actor_id,
305
+ confirmation_ref: input.confirmation_ref,
306
+ expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
307
+ findings_digest: input.action_kind === "request_rework"
308
+ ? await findingsDigest((input.findings as Array<{ id: string; kind: string; acceptance_id: string | null; summary: string }>))
309
+ : null,
310
+ };
311
+ return registry.issue(binding);
312
+ }
313
+
314
+ export interface ClaudeRuntimeOptions {
315
+ cwd: string;
316
+ env?: Record<string, string | undefined>;
317
+ host?: ClaudeReviewHost;
318
+ ports?: AssuranceCoordinatorPorts;
319
+ interactive?: boolean;
320
+ permissionMode?: PermissionMode;
321
+ decisions?: Map<string, NativeDecision>;
322
+ }
323
+
324
+ export class ClaudeRuntime {
325
+ readonly host: ClaudeReviewHost;
326
+ readonly coordinator: AssuranceCoordinator;
327
+ private readonly cwd: string;
328
+ private readonly env: Record<string, string | undefined>;
329
+ private readonly interactive: boolean;
330
+ private readonly permissionMode: PermissionMode;
331
+ private readonly decisions: Map<string, NativeDecision>;
332
+ private hostVersion: string | undefined;
333
+ private mutationRegistry: MutationAuthorityRegistry | null = null;
334
+ private enrollmentRegistry = createEnrollmentAuthorityRegistry();
335
+ private app: ReturnType<typeof createCanaryApplication> | null = null;
336
+
337
+ constructor(options: ClaudeRuntimeOptions) {
338
+ this.cwd = options.cwd;
339
+ this.env = options.env ?? process.env;
340
+ this.interactive = options.interactive ?? true;
341
+ this.permissionMode = options.permissionMode ?? "manual";
342
+ this.decisions = options.decisions ?? new Map();
343
+ this.host = options.host ?? new ClaudeReviewHost(new FileHookEventLog());
344
+ if (options.ports) {
345
+ this.coordinator = new AssuranceCoordinator({ ...options.ports, host: this.host });
346
+ return;
347
+ }
348
+ this.coordinator = new AssuranceCoordinator(this.createKernelPorts());
349
+ }
350
+
351
+ observe(event: ClaudeHookEvent): void {
352
+ this.host.observe(event);
353
+ }
354
+
355
+ /** Bind the version announced by the connected Host during the MCP handshake. */
356
+ bindHostVersion(version: string | undefined): void {
357
+ this.hostVersion = version || undefined;
358
+ }
359
+
360
+ async shutdown(): Promise<void> {
361
+ await this.coordinator.onSessionShutdown();
362
+ }
363
+
364
+ private createKernelPorts(): AssuranceCoordinatorPorts {
365
+ return {
366
+ host: this.host,
367
+ projectTask: (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
368
+ readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
369
+ readTaskIntent: (root, taskId) => readTaskIntent(root, taskId),
370
+ frozenRunner: async () => resolveBunRunner(),
371
+ buildAssurance: (root, taskId, role, projection, runner) => buildAssuranceSnapshot(root, taskId, role, projection, runner),
372
+ ensureReviewRevision: async (root, taskId, projection) => {
373
+ const current = await readTaskRecord(root, taskId);
374
+ if (!current.record) throw new Error(`task ${taskId} has no TaskRecord`);
375
+ if (current.record.contract !== "assurance_kernel/task_record/v4") return null;
376
+ return ensureReviewRevision(root, {
377
+ taskId,
378
+ baseHead: current.record.git_base_head,
379
+ scopeHint: current.record.intent_snapshot.scope_hint,
380
+ expectedDiffHash: projection.projection.diff_hash,
381
+ });
382
+ },
383
+ runQa: (snapshot, descriptors, runner, options) => runDeterministicQa(snapshot, descriptors, runner, options),
384
+ writeReviewEvidence: (input) => writeNativeReviewEvidence(input.evidence),
385
+ applyVerdict: (ctx, input) => this.applyVerdict(ctx, input),
386
+ applyOrdinaryOperation: (ctx, input) => this.executeOrdinary(ctx, input),
387
+ };
388
+ }
389
+
390
+ private authority() {
391
+ this.mutationRegistry ??= createMutationAuthorityRegistry();
392
+ this.app ??= createCanaryApplication(this.mutationRegistry);
393
+ return { registry: this.mutationRegistry, app: this.app };
394
+ }
395
+
396
+ private rejectBeforePreparation(operation: string, meta: ToolMeta): void {
397
+ // Zero-mutation fast path only: a configured deny/cancel rejects before
398
+ // any workspace preparation. The live accept elicitation is requested
399
+ // and consumed only after preparation computes the binding digests, so
400
+ // native confirmation is always bound to hashes computed before it.
401
+ const configured = this.decisions.get(operation) ?? meta.decision;
402
+ if (configured === "deny" || configured === "cancel") this.gate(operation, meta);
403
+ }
404
+
405
+ private gate(operation: string, meta: ToolMeta, binding: {
406
+ intentRevision?: number;
407
+ intentContentHash?: string;
408
+ bindingDigest?: string;
409
+ } = {}): { confirmation_ref: string } {
410
+ const probe = probeHost(this.env, process.platform, this.hostVersion);
411
+ if (!probe.ok) throw new Error(probe.reason);
412
+ const requiresUserInteraction = Boolean(meta.requiresUserInteraction);
413
+ const permissionMode = meta.permissionMode ?? this.permissionMode;
414
+ const configuredDecision = this.decisions.get(operation) ?? meta.decision;
415
+ const decision = configuredDecision ?? (
416
+ isPrivilegedOperation(operation)
417
+ && Boolean(meta.interactive ?? this.interactive)
418
+ && requiresUserInteraction
419
+ && permissionMode !== "dontAsk"
420
+ ? this.host.takeConfirmation(meta.sessionId, meta.toolCallId)
421
+ : undefined
422
+ );
423
+ const gate = evaluateNativeGate({
424
+ operation,
425
+ permissionMode,
426
+ requiresUserInteraction,
427
+ interactive: meta.interactive ?? this.interactive,
428
+ decision,
429
+ });
430
+ if (!gate.ok) throw new Error(gate.reason);
431
+ return {
432
+ confirmation_ref: confirmationRef({
433
+ sessionId: meta.sessionId,
434
+ toolCallId: meta.toolCallId,
435
+ operation,
436
+ taskId: meta.taskId,
437
+ ...binding,
438
+ }),
439
+ };
440
+ }
441
+
442
+ async status(taskId: string) {
443
+ return projectAssurance(this.cwd, taskId, diffSnapshotOf);
444
+ }
445
+
446
+ async enroll(taskId: string, meta: ToolMeta) {
447
+ this.rejectBeforePreparation("enroll", { ...meta, taskId });
448
+ const now = new Date().toISOString();
449
+ const preparation = await preparePiCanary(this.cwd, { task_id: taskId, now });
450
+ const gate = this.gate("enroll", { ...meta, taskId }, {
451
+ intentRevision: preparation.intent?.revision,
452
+ intentContentHash: preparation.intent?.content_hash,
453
+ bindingDigest: preparation.digest,
454
+ });
455
+ const { unchanged } = await revalidatePiCanary(this.cwd, { task_id: taskId, now }, preparation);
456
+ if (!unchanged) throw new Error("Workspace changed after confirmation; enrollment aborted before authority");
457
+ if (!preparation.intent) throw new Error("enrollment requires a readable TaskIntent");
458
+ const nonce = enrollmentNonce();
459
+ const binding: EnrollmentCapabilityBinding = {
460
+ task_id: taskId,
461
+ intent_path: preparation.intent.path,
462
+ intent_revision: preparation.intent.revision,
463
+ intent_content_hash: preparation.intent.content_hash,
464
+ preparation_digest: preparation.digest,
465
+ actor_id: "user",
466
+ confirmation_ref: gate.confirmation_ref,
467
+ expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
468
+ nonce,
469
+ };
470
+ const capability = this.enrollmentRegistry.issue(binding);
471
+ const input = {
472
+ task_id: taskId,
473
+ intent_path: binding.intent_path,
474
+ intent_revision: binding.intent_revision,
475
+ preparation_digest: binding.preparation_digest,
476
+ capability,
477
+ capability_binding: binding,
478
+ now,
479
+ };
480
+ const rehearsal = runEnrollmentRehearsal(this.cwd, input, capability, this.enrollmentRegistry);
481
+ if (!rehearsal.rehearsed || rehearsal.evidence.outcome !== "ready") {
482
+ throw new Error(`Kernel enrollment rehearsal failed: ${rehearsal.evidence.blockers.join("; ")}`);
483
+ }
484
+ return enrollCanaryTask(this.cwd, input, this.enrollmentRegistry);
485
+ }
486
+
487
+ async advance(taskId: string, signal?: AbortSignal) {
488
+ return this.coordinator.advance(taskId, { cwd: this.cwd }, signal);
489
+ }
490
+
491
+ async submitReview(taskId: string, verdictInput: unknown) {
492
+ return submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput);
493
+ }
494
+
495
+ async authorize(taskId: string, operation: string, meta: ToolMeta, extra: Record<string, unknown> = {}) {
496
+ if (!isPrivilegedOperation(operation) && operation !== "request_authorization") throw new Error(`unsupported privileged operation ${operation}`);
497
+ this.rejectBeforePreparation(operation, { ...meta, taskId });
498
+ if (operation === "repair_authority_state") {
499
+ const authority = reconcileKernelAuthority(this.cwd, taskId);
500
+ if (authority.state !== "repairable_stale_claim" || authority.owner_task_id !== taskId) {
501
+ throw new Error(authority.diagnostic ?? "authority repair requires a repairable stale claim");
502
+ }
503
+ const gate = this.gate(operation, { ...meta, taskId }, { bindingDigest: `repair:${authority.revision}` });
504
+ const current = reconcileKernelAuthority(this.cwd, taskId);
505
+ if (current.state !== authority.state || current.owner_task_id !== authority.owner_task_id || current.revision !== authority.revision) {
506
+ throw new Error("authority changed after native confirmation; repair aborted before capability issuance");
507
+ }
508
+ return repairKernelAuthority(this.cwd, taskId, current.revision);
509
+ }
510
+ let op = operation === "request_authorization" ? "record_user_approval" : operation;
511
+ let decisionOp: { finding_id: string; resolution: string } | undefined;
512
+ const projection = await this.status(taskId);
513
+ if (projection.error || !projection.claim) throw new Error(projection.error ?? "no active backend claim");
514
+ // Kernel projection is the sole source of authorization readiness:
515
+ // request_authorization submits the exact operation the projection
516
+ // derives, including the single bound user-decision resolution.
517
+ const readiness = projection.projection.authorization;
518
+ if (operation === "request_authorization") {
519
+ if (readiness.state === "resolve_user_decision") {
520
+ const record = await readTaskRecord(this.cwd, taskId);
521
+ const open = (record.record?.findings ?? []).filter(
522
+ (finding) => finding.kind === "unresolved_user_decision" && finding.status === "open",
523
+ );
524
+ if (open.length !== 1) throw new Error(`resolve-user-decision requires exactly one open user decision; found ${open.length}`);
525
+ op = "resolve_user_decision";
526
+ decisionOp = { finding_id: open[0].id, resolution: `resume after literal-user decision: ${open[0].summary}` };
527
+ } else if (readiness.state !== "record_user_approval") {
528
+ throw new Error(readiness.blocked ?? "no unique host-derived authorization operation");
529
+ }
530
+ }
531
+ const priorIntent = await readTaskIntent(this.cwd, taskId);
532
+ const now = new Date().toISOString();
533
+ const actorId = "user";
534
+ const nextIntent = extra.next_intent ? await parseTaskIntentV1(extra.next_intent) : undefined;
535
+ if (op === "approve_breaking_intent_revision" && !nextIntent) throw new Error("approve_breaking_intent_revision requires next_intent");
536
+ const nextIntentHash = nextIntent ? canonicalIntentHash(nextIntent) : undefined;
537
+ const nextIntentRef = nextIntent
538
+ ? { path: `docs/plans/${nextIntent.task_id}.intent.json`, content_hash: nextIntentHash! }
539
+ : undefined;
540
+ const sidecar = nextIntent ? join(this.cwd, priorIntent.intent_ref.path) : undefined;
541
+ const priorBytes = sidecar ? readFileSync(sidecar) : undefined;
542
+ const priorIndexState = sidecar
543
+ ? execFileSync("git", ["ls-files", "--stage", "-z", "--", priorIntent.intent_ref.path], {
544
+ cwd: this.cwd,
545
+ stdio: ["ignore", "pipe", "pipe"],
546
+ })
547
+ : undefined;
548
+ const restoreStagedIntent = (): void => {
549
+ if (!sidecar || !priorBytes || !priorIndexState) return;
550
+ writeFileSync(sidecar, priorBytes);
551
+ execFileSync("git", ["update-index", "--force-remove", "--", priorIntent.intent_ref.path], {
552
+ cwd: this.cwd,
553
+ stdio: ["ignore", "pipe", "pipe"],
554
+ });
555
+ if (priorIndexState.length > 0) {
556
+ execFileSync("git", ["update-index", "-z", "--index-info"], {
557
+ cwd: this.cwd,
558
+ input: priorIndexState,
559
+ stdio: ["pipe", "ignore", "pipe"],
560
+ });
561
+ }
562
+ };
563
+ let preparedDiffHash = projection.projection.diff_hash;
564
+ let gate: { confirmation_ref: string };
565
+ try {
566
+ if (sidecar && nextIntent) {
567
+ writeFileSync(sidecar, `${JSON.stringify(nextIntent, null, 2)}\n`);
568
+ execFileSync("git", ["add", "--", priorIntent.intent_ref.path], { cwd: this.cwd, stdio: ["ignore", "pipe", "pipe"] });
569
+ const preparedRecord = await readTaskRecord(this.cwd, taskId);
570
+ if (!preparedRecord.record) throw new Error("TaskRecord changed before the breaking revision digest");
571
+ preparedDiffHash = diffHashOf(this.cwd, preparedRecord.record);
572
+ }
573
+ const preparedProjection = await this.status(taskId);
574
+ assertProjectionBinding(projection, preparedProjection, Boolean(nextIntent));
575
+ if (preparedProjection.projection.diff_hash !== preparedDiffHash) {
576
+ throw new Error("Workspace changed while preparing the authority digest");
577
+ }
578
+ gate = this.gate(operation, { ...meta, taskId }, {
579
+ intentRevision: nextIntent?.revision ?? projection.projection.intent_revision,
580
+ intentContentHash: nextIntentHash ?? projection.projection.intent_content_hash,
581
+ bindingDigest: `${preparedDiffHash}:${nextIntentHash ?? ""}`,
582
+ });
583
+ } catch (error) {
584
+ if (sidecar && priorBytes && priorIndexState) {
585
+ const current = await readTaskRecord(this.cwd, taskId);
586
+ if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) {
587
+ restoreStagedIntent();
588
+ }
589
+ }
590
+ throw error;
591
+ }
592
+ const { registry, app } = this.authority();
593
+ const confirmation = gate.confirmation_ref;
594
+ const approval = op === "record_user_approval"
595
+ ? {
596
+ id: `approval-user-${randomUUID().slice(0, 8)}`,
597
+ kind: "user" as const,
598
+ authority_role: "user" as const,
599
+ task_revision: projection.projection.intent_revision,
600
+ intent_content_hash: projection.projection.intent_content_hash,
601
+ diff_hash: projection.projection.diff_hash,
602
+ actor_id: actorId,
603
+ summary: "literal user approval",
604
+ }
605
+ : undefined;
606
+ try {
607
+ const capabilityProjection = await this.status(taskId);
608
+ assertProjectionBinding(projection, capabilityProjection, Boolean(nextIntent));
609
+ const operationDiffHash = capabilityProjection.projection.diff_hash;
610
+ if (nextIntent && operationDiffHash !== preparedDiffHash) {
611
+ throw new Error("Workspace changed after native confirmation; authority aborted before capability issuance");
612
+ }
613
+ const capability = await mintCapability(registry, {
614
+ authority_kind: "user",
615
+ task_id: taskId,
616
+ action_kind: op,
617
+ expected_record_hash: capabilityProjection.projection.record_revision,
618
+ intent_revision: nextIntent?.revision ?? capabilityProjection.projection.intent_revision,
619
+ intent_content_hash: nextIntentHash ?? capabilityProjection.projection.intent_content_hash,
620
+ diff_hash: operationDiffHash,
621
+ actor_id: actorId,
622
+ now,
623
+ confirmation_ref: confirmation,
624
+ ...(approval ? { approval } : {}),
625
+ ...(op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {}),
626
+ ...(op === "resolve_user_decision" && decisionOp ? decisionOp : {}),
627
+ ...(op === "stop" ? { reason: extra.reason ?? "user stop" } : {}),
628
+ });
629
+ const result = app.execute({
630
+ root: this.cwd,
631
+ task_id: taskId,
632
+ operation: {
633
+ op,
634
+ capability,
635
+ actor_id: actorId,
636
+ ...(approval ? { approval } : {}),
637
+ ...(op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {}),
638
+ ...(op === "resolve_user_decision" && decisionOp ? decisionOp : {}),
639
+ ...(op === "stop" ? { reason: extra.reason ?? "user stop" } : {}),
640
+ } as never,
641
+ prior_intent_token: priorIntent.token,
642
+ diffProvider: diffSnapshotOf,
643
+ now,
644
+ });
645
+ if (op === "stop" || op === "approve_breaking_intent_revision") stagePlanningArtifactTransition(this.cwd, result.record);
646
+ return result;
647
+ } catch (error) {
648
+ if (sidecar && priorBytes && priorIndexState) {
649
+ const current = await readTaskRecord(this.cwd, taskId);
650
+ if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) {
651
+ restoreStagedIntent();
652
+ }
653
+ }
654
+ throw error;
655
+ }
656
+ }
657
+
658
+ private async applyVerdict(
659
+ ctx: HostContext,
660
+ input: {
661
+ taskId: string;
662
+ snapshot: SnapshotDescriptor;
663
+ verdict: AssuranceVerdict;
664
+ invocation: { /* token */ };
665
+ actorId: string;
666
+ hooks?: { beforeCommit?: () => Promise<void>; onCommit?: () => void; afterCommit?: () => Promise<void> };
667
+ },
668
+ ): Promise<void> {
669
+ const { registry, app } = await this.authority();
670
+ const priorIntentToken = (await readTaskIntent(ctx.cwd, input.taskId)).token;
671
+ const now = new Date().toISOString();
672
+ const commitAndApply = async <T>(apply: () => Promise<T>): Promise<T> => {
673
+ this.coordinator.commitInvocation(input.invocation as never);
674
+ const settlement = apply();
675
+ input.hooks?.onCommit?.();
676
+ const result = await settlement;
677
+ await input.hooks?.afterCommit?.();
678
+ return result;
679
+ };
680
+ if (input.verdict.decision === "rework") {
681
+ const findings = input.verdict.findings!.map((finding) => ({
682
+ id: finding.id,
683
+ kind: finding.kind,
684
+ status: "open",
685
+ acceptance_id: finding.acceptance_id,
686
+ source: "review",
687
+ review_round: null,
688
+ summary: finding.summary,
689
+ }));
690
+ const capability = await mintCapability(registry, {
691
+ authority_kind: input.snapshot.role,
692
+ task_id: input.taskId,
693
+ action_kind: "request_rework",
694
+ expected_record_hash: input.snapshot.record_revision,
695
+ intent_revision: input.snapshot.intent_revision,
696
+ intent_content_hash: input.snapshot.intent_content_hash,
697
+ diff_hash: input.snapshot.diff_hash,
698
+ actor_id: input.actorId,
699
+ findings,
700
+ now,
701
+ confirmation_ref: `claude:${input.actorId}`,
702
+ });
703
+ await input.hooks?.beforeCommit?.();
704
+ const result = await commitAndApply(async () => app.execute({
705
+ root: ctx.cwd,
706
+ task_id: input.taskId,
707
+ operation: { op: "request_rework", capability, findings: findings as never[], actor_id: input.actorId },
708
+ prior_intent_token: priorIntentToken,
709
+ diffProvider: diffSnapshotOf,
710
+ now,
711
+ }));
712
+ stagePlanningArtifactTransition(ctx.cwd, result.record);
713
+ return;
714
+ }
715
+ const approval = {
716
+ id: `approval-${input.snapshot.role}-${randomUUID().slice(0, 8)}`,
717
+ kind: input.snapshot.role === "qa" ? "qa" : "review",
718
+ authority_role: input.snapshot.role === "qa" ? "qa" : "reviewer",
719
+ task_revision: input.snapshot.intent_revision,
720
+ intent_content_hash: input.snapshot.intent_content_hash,
721
+ diff_hash: input.snapshot.diff_hash,
722
+ actor_id: input.actorId,
723
+ summary: input.verdict.approval!.summary,
724
+ ...(input.snapshot.role === "review" && input.snapshot.review_revision ? { review_revision: input.snapshot.review_revision } : {}),
725
+ };
726
+ const capability = await mintCapability(registry, {
727
+ authority_kind: input.snapshot.role,
728
+ task_id: input.taskId,
729
+ action_kind: "record_approval",
730
+ expected_record_hash: input.snapshot.record_revision,
731
+ intent_revision: input.snapshot.intent_revision,
732
+ intent_content_hash: input.snapshot.intent_content_hash,
733
+ diff_hash: input.snapshot.diff_hash,
734
+ actor_id: input.actorId,
735
+ approval,
736
+ now,
737
+ confirmation_ref: `claude:${input.actorId}`,
738
+ });
739
+ await input.hooks?.beforeCommit?.();
740
+ await commitAndApply(async () => app.execute({
741
+ root: ctx.cwd,
742
+ task_id: input.taskId,
743
+ operation: { op: "record_approval", capability, approval, actor_id: input.actorId },
744
+ prior_intent_token: priorIntentToken,
745
+ diffProvider: diffSnapshotOf,
746
+ now,
747
+ }));
748
+ }
749
+
750
+ private async executeOrdinary(ctx: HostContext, input: { taskId: string; operation: { op: string; actor_id: string; next_intent?: unknown } }) {
751
+ const { app } = await this.authority();
752
+ const operation = input.operation.op === "revise_intent"
753
+ ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) }
754
+ : input.operation;
755
+ const priorIntent = await readTaskIntent(ctx.cwd, input.taskId);
756
+ const sidecar = join(ctx.cwd, priorIntent.intent_ref.path);
757
+ const priorBytes = operation.op === "revise_intent" ? readFileSync(sidecar) : null;
758
+ try {
759
+ if (priorBytes) writeFileSync(sidecar, `${JSON.stringify(operation.next_intent, null, 2)}\n`);
760
+ const result = await app.execute({
761
+ root: ctx.cwd,
762
+ task_id: input.taskId,
763
+ operation: operation as never,
764
+ prior_intent_token: priorIntent.token,
765
+ diffProvider: diffSnapshotOf,
766
+ now: new Date().toISOString(),
767
+ });
768
+ if (operation.op === "freeze_artifacts" || operation.op === "stop") stagePlanningArtifactTransition(ctx.cwd, result.record);
769
+ return result;
770
+ } catch (error) {
771
+ if (priorBytes) {
772
+ const current = await readTaskRecord(ctx.cwd, input.taskId);
773
+ if (current.record?.intent_snapshot.revision === priorIntent.intent.revision) writeFileSync(sidecar, priorBytes);
774
+ }
775
+ throw error;
776
+ }
777
+ }
778
+ }
779
+
780
+ export interface ToolMeta {
781
+ sessionId: string;
782
+ toolCallId: string;
783
+ taskId: string;
784
+ requiresUserInteraction?: boolean;
785
+ permissionMode?: PermissionMode;
786
+ interactive?: boolean;
787
+ decision?: NativeDecision;
788
+ signal?: AbortSignal;
789
+ }