immune-brain 3.6.2 → 3.6.4

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.
@@ -42,7 +42,7 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
42
42
  }
43
43
 
44
44
  // plugins/immune-brain/runtime/plugin_version.ts
45
- var PLUGIN_VERSION = "3.6.2";
45
+ var PLUGIN_VERSION = "3.6.4";
46
46
 
47
47
  // plugins/immune-brain/runtime/claude/interaction.ts
48
48
  import { createHash, randomUUID } from "node:crypto";
@@ -99,7 +99,7 @@ function enrollmentNonce() {
99
99
 
100
100
  // plugins/immune-brain/runtime/claude/review_host.ts
101
101
  import { createHash as createHash2 } from "node:crypto";
102
- import { constants, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, rmSync, writeSync, closeSync } from "node:fs";
102
+ import { constants, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readSync, realpathSync, rmSync, writeSync, closeSync } from "node:fs";
103
103
  import { dirname, join } from "node:path";
104
104
  import { tmpdir } from "node:os";
105
105
  var REVIEWER_AGENT = "immune-brain-reviewer";
@@ -183,7 +183,7 @@ function appendPrivate(path, dir, line) {
183
183
  closeSync(fd);
184
184
  }
185
185
  }
186
- function readPrivate(path) {
186
+ function readPrivate(path, maxBytes = Number.POSITIVE_INFINITY) {
187
187
  let fd;
188
188
  try {
189
189
  fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
@@ -194,6 +194,8 @@ function readPrivate(path) {
194
194
  const stat = fstatSync(fd);
195
195
  if (!stat.isFile() || !ownedByUs(stat) || (stat.mode & 511) !== 384)
196
196
  return;
197
+ if (stat.size > maxBytes)
198
+ return;
197
199
  const buf = Buffer.alloc(stat.size);
198
200
  readSync(fd, buf, 0, stat.size, 0);
199
201
  return buf.toString("utf8");
@@ -201,6 +203,67 @@ function readPrivate(path) {
201
203
  closeSync(fd);
202
204
  }
203
205
  }
206
+ var MAX_TRANSCRIPT_BYTES = 32 * 1024 * 1024;
207
+ function parseAsyncAgentLaunch(result) {
208
+ let payload;
209
+ try {
210
+ payload = JSON.parse(result);
211
+ } catch {
212
+ return null;
213
+ }
214
+ if (!payload || typeof payload !== "object")
215
+ return null;
216
+ const obj = payload;
217
+ if (obj.isAsync !== true && obj.status !== "async_launched")
218
+ return null;
219
+ const agentId = typeof obj.agentId === "string" ? obj.agentId : "";
220
+ const outputFile = typeof obj.outputFile === "string" ? obj.outputFile : "";
221
+ if (!agentId || !outputFile)
222
+ return null;
223
+ return { agentId, outputFile };
224
+ }
225
+ function readAgentTranscriptResult(transcript, agentId) {
226
+ let last = null;
227
+ for (const line of transcript.split(`
228
+ `)) {
229
+ if (!line.trim())
230
+ continue;
231
+ let row;
232
+ try {
233
+ row = JSON.parse(line);
234
+ } catch {
235
+ continue;
236
+ }
237
+ if (row.type !== "assistant" || row.agentId !== agentId)
238
+ continue;
239
+ const message = row.message;
240
+ if (!message || typeof message !== "object")
241
+ continue;
242
+ const content = message.content;
243
+ if (!Array.isArray(content))
244
+ continue;
245
+ let text = "";
246
+ for (const block of content) {
247
+ if (!block || typeof block !== "object")
248
+ continue;
249
+ const part = block;
250
+ if (part.type === "text" && typeof part.text === "string")
251
+ text += part.text;
252
+ }
253
+ if (text.trim())
254
+ last = text;
255
+ }
256
+ return last;
257
+ }
258
+ function readAgentTranscript(path) {
259
+ let resolved;
260
+ try {
261
+ resolved = realpathSync(path);
262
+ } catch {
263
+ return;
264
+ }
265
+ return readPrivate(resolved, MAX_TRANSCRIPT_BYTES);
266
+ }
204
267
  function hookEventPath(sessionId, root = tmpdir()) {
205
268
  return join(cacheDir(root), `${sessionHash(sessionId)}.jsonl`);
206
269
  }
@@ -288,6 +351,7 @@ class ClaudeReviewHost {
288
351
  this.log = log;
289
352
  }
290
353
  prepareReview(request) {
354
+ this.drain();
291
355
  const initialCursors = new Map;
292
356
  const sessionCursors = new Map;
293
357
  for (const sessionId of this.log.sessions()) {
@@ -325,15 +389,23 @@ ${request.prompt}`,
325
389
  for (let i = start;i < events.length; i++) {
326
390
  const event = events[i];
327
391
  if (event.type === "SessionEnd") {
328
- this.log.clear(event.sessionId);
329
- ended = true;
330
- this.appliedBySession.delete(event.sessionId);
331
392
  for (const [id, state] of this.pending) {
332
393
  if (state.startEvent?.sessionId === event.sessionId || state.postEvent?.sessionId === event.sessionId || state.stopEvent?.sessionId === event.sessionId) {
333
394
  this.pending.delete(id);
334
395
  }
335
396
  }
336
- break;
397
+ for (const state of this.pending.values()) {
398
+ const current = state.initialCursors.get(sessionId) ?? 0;
399
+ if (i + 1 > current)
400
+ state.initialCursors.set(sessionId, i + 1);
401
+ }
402
+ if (i === events.length - 1) {
403
+ this.log.clear(event.sessionId);
404
+ ended = true;
405
+ this.appliedBySession.delete(event.sessionId);
406
+ break;
407
+ }
408
+ continue;
337
409
  }
338
410
  for (const state of this.pending.values()) {
339
411
  if (state.consumed || state.error || i < (state.initialCursors.get(sessionId) ?? 0))
@@ -460,13 +532,23 @@ ${request.prompt}`,
460
532
  if (state.startEvent.sessionId !== state.postEvent.sessionId || state.startEvent.sessionId !== state.stopEvent.sessionId || state.startEvent.agentId !== state.postEvent.agentId || state.startEvent.agentId !== state.stopEvent.agentId) {
461
533
  return { ok: false, reason: "foreground Agent terminal event correlation mismatch", release: true };
462
534
  }
463
- return {
464
- ok: true,
465
- receipt: {
466
- actorId: `claude:${state.startEvent.agentId ?? reservation.id}`,
467
- result: state.postEvent.result
468
- }
469
- };
535
+ const actorId = `claude:${state.startEvent.agentId ?? reservation.id}`;
536
+ const launch = parseAsyncAgentLaunch(state.postEvent.result);
537
+ if (!launch) {
538
+ return { ok: true, receipt: { actorId, result: state.postEvent.result } };
539
+ }
540
+ if (launch.agentId !== state.postEvent.agentId) {
541
+ return { ok: false, reason: "async Agent launch envelope names a different agent", release: true };
542
+ }
543
+ const transcript = readAgentTranscript(launch.outputFile);
544
+ if (transcript === undefined) {
545
+ return { ok: false, reason: "async Agent transcript is not readable", release: false };
546
+ }
547
+ const verdict = readAgentTranscriptResult(transcript, launch.agentId);
548
+ if (!verdict?.trim()) {
549
+ return { ok: false, reason: "async Agent transcript carries no reviewer result", release: false };
550
+ }
551
+ return { ok: true, receipt: { actorId, result: verdict } };
470
552
  }
471
553
  consumeReview(reservation) {
472
554
  const result = this.inspectReview(reservation);
@@ -514,6 +596,11 @@ function parseHookStdin(raw) {
514
596
  if (typeof toolInputObj.prompt === "string")
515
597
  prompt = toolInputObj.prompt;
516
598
  }
599
+ if (!prompt && typeof payload.tool_response === "object" && payload.tool_response !== null) {
600
+ const toolResponseObj = payload.tool_response;
601
+ if (typeof toolResponseObj.prompt === "string")
602
+ prompt = toolResponseObj.prompt;
603
+ }
517
604
  let extractedOpId = operationId;
518
605
  let extractedTaskId = taskId;
519
606
  for (const candidate of [payload.input, payload.tool_input, payload.tool_response, payload.toolResponse, payload.response, payload.result]) {
@@ -602,7 +689,7 @@ import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypt
602
689
  // plugins/immune-brain/runtime/assurance/verification.ts
603
690
  import { createHash as createHash3 } from "node:crypto";
604
691
  import { execFileSync, spawn } from "node:child_process";
605
- import { realpathSync, statSync } from "node:fs";
692
+ import { realpathSync as realpathSync2, statSync } from "node:fs";
606
693
  import { isAbsolute as isAbsolute2, resolve, sep as sep2, relative } from "node:path";
607
694
 
608
695
  // plugins/immune-brain/runtime/verification_descriptor.ts
@@ -705,7 +792,7 @@ function resolveBunRunner() {
705
792
  }
706
793
  let real;
707
794
  try {
708
- real = realpathSync(executable);
795
+ real = realpathSync2(executable);
709
796
  } catch {
710
797
  throw new VerificationDescriptorError("bun runner realpath is unresolvable");
711
798
  }
@@ -1710,7 +1797,7 @@ import {
1710
1797
  writeFileSync,
1711
1798
  statSync as statSync2,
1712
1799
  readFileSync as readFileSync3,
1713
- realpathSync as realpathSync3,
1800
+ realpathSync as realpathSync4,
1714
1801
  chmodSync
1715
1802
  } from "node:fs";
1716
1803
  import { tmpdir as tmpdir2 } from "node:os";
@@ -1724,7 +1811,7 @@ import {
1724
1811
  lstatSync as lstatSync2,
1725
1812
  readFileSync as readFileSync2,
1726
1813
  readlinkSync,
1727
- realpathSync as realpathSync2
1814
+ realpathSync as realpathSync3
1728
1815
  } from "node:fs";
1729
1816
  import { resolve as resolve2 } from "node:path";
1730
1817
  function git(root, args) {
@@ -1891,7 +1978,7 @@ function taskSnapshotOnce(root, scope) {
1891
1978
  const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
1892
1979
  if (!repositoryRoot || !head || !GIT_OBJECT_ID.test(head))
1893
1980
  throw new Error("cannot derive task snapshot outside a committed Git workspace");
1894
- if (realpathSync2(resolve2(repositoryRoot)) !== root)
1981
+ if (realpathSync3(resolve2(repositoryRoot)) !== root)
1895
1982
  throw new Error("task snapshot repository root does not match the project root");
1896
1983
  const sparseCheckout = git(root, ["config", "--bool", "core.sparseCheckout"])?.trim();
1897
1984
  const sparseIndex = git(root, ["config", "--bool", "index.sparse"])?.trim();
@@ -1935,7 +2022,7 @@ function captureGitTaskSnapshot(projectRoot, scopeHint) {
1935
2022
  const requestedStat = lstatSync2(requestedRoot);
1936
2023
  if (requestedStat.isSymbolicLink() || !requestedStat.isDirectory())
1937
2024
  throw new Error("task snapshot root must be a real directory");
1938
- const root = realpathSync2(requestedRoot);
2025
+ const root = realpathSync3(requestedRoot);
1939
2026
  const scope = assertCanonicalTaskScope(scopeHint);
1940
2027
  const before = taskSnapshotOnce(root, scope);
1941
2028
  gitTaskSnapshotTestHook?.();
@@ -1968,7 +2055,7 @@ function taskRevisionSnapshotOnce(root, scope, baseHead) {
1968
2055
  const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
1969
2056
  if (!repositoryRoot || !head || !GIT_OBJECT_ID.test(head))
1970
2057
  throw new Error("cannot derive a task revision outside a committed Git workspace");
1971
- if (realpathSync2(resolve2(repositoryRoot)) !== root)
2058
+ if (realpathSync3(resolve2(repositoryRoot)) !== root)
1972
2059
  throw new Error("task revision repository root does not match the project root");
1973
2060
  if (gitRequired(root, ["cat-file", "-t", baseHead], `task revision base is unreadable: ${baseHead}`) !== "commit")
1974
2061
  throw new Error(`task revision base is not a commit: ${baseHead}`);
@@ -2025,7 +2112,7 @@ function captureGitTaskRevisionSnapshot(projectRoot, scopeHint, baseHead) {
2025
2112
  const requestedStat = lstatSync2(requestedRoot);
2026
2113
  if (requestedStat.isSymbolicLink() || !requestedStat.isDirectory())
2027
2114
  throw new Error("task revision root must be a real directory");
2028
- const root = realpathSync2(requestedRoot);
2115
+ const root = realpathSync3(requestedRoot);
2029
2116
  if (typeof baseHead !== "string" || !GIT_OBJECT_ID.test(baseHead.toLowerCase()))
2030
2117
  throw new Error("task revision base must be a Git commit id");
2031
2118
  const scope = assertCanonicalTaskScope(scopeHint);
@@ -2397,13 +2484,10 @@ function captureReviewManifest(root, input) {
2397
2484
  throw new Error("immutable review manifest metadata exceeds bounded output limit");
2398
2485
  return manifest;
2399
2486
  }
2400
- function ensureReviewRevision(root, input) {
2401
- return publishInput(root, input).revision;
2402
- }
2403
2487
  function writeNativeReviewEvidence(payload) {
2404
2488
  const rawDirectory = mkdtempSync(join4(tmpdir2(), "imm-canary-native-review-"));
2405
2489
  try {
2406
- const directory = realpathSync3(rawDirectory);
2490
+ const directory = realpathSync4(rawDirectory);
2407
2491
  chmodSync(directory, 493);
2408
2492
  const path = join4(directory, "evidence.json");
2409
2493
  writeFileSync(path, JSON.stringify(payload), { encoding: "utf8", mode: 420, flag: "wx" });
@@ -2418,7 +2502,7 @@ function writeNativeReviewEvidence(payload) {
2418
2502
  }
2419
2503
  }
2420
2504
  function assertReviewArtifact(path) {
2421
- const targetPath = realpathSync3(path);
2505
+ const targetPath = realpathSync4(path);
2422
2506
  let stat;
2423
2507
  try {
2424
2508
  stat = statSync2(targetPath);
@@ -2612,7 +2696,7 @@ import {
2612
2696
  mkdirSync as mkdirSync2,
2613
2697
  openSync as openSync3,
2614
2698
  readFileSync as readFileSync6,
2615
- realpathSync as realpathSync5,
2699
+ realpathSync as realpathSync6,
2616
2700
  renameSync,
2617
2701
  rmSync as rmSync3,
2618
2702
  writeFileSync as writeFileSync2
@@ -2629,6 +2713,9 @@ var TASK_RECORD_CONTRACT_V2 = "assurance_kernel/task_record/v2";
2629
2713
  var TASK_RECORD_CONTRACT_V3 = "assurance_kernel/task_record/v3";
2630
2714
  var TASK_RECORD_CONTRACT_V4 = "assurance_kernel/task_record/v4";
2631
2715
  var REVIEW_REVISION_IDENTITY_CONTRACT = "assurance_kernel/review_revision_identity/v1";
2716
+ function isTaskRecordV4(record) {
2717
+ return record.contract === TASK_RECORD_CONTRACT_V4;
2718
+ }
2632
2719
  var REDUCED_MUTATION_BRAND = Symbol("assurance-kernel-reduced-mutation-v2");
2633
2720
  var MUTATION_AUTHORITY_CAPABILITY_BRAND = Symbol("assurance-kernel-mutation-authority-capability");
2634
2721
 
@@ -2641,7 +2728,7 @@ import {
2641
2728
  lstatSync as lstatSync4,
2642
2729
  openSync as openSync2,
2643
2730
  readFileSync as readFileSync5,
2644
- realpathSync as realpathSync4
2731
+ realpathSync as realpathSync5
2645
2732
  } from "node:fs";
2646
2733
  import { execFileSync as execFileSync3 } from "node:child_process";
2647
2734
  import { join as join6, resolve as resolve4, sep as sep3 } from "node:path";
@@ -2939,7 +3026,7 @@ function resolveCanonicalRoot(root) {
2939
3026
  const rootStat = lstatSync4(resolved);
2940
3027
  if (rootStat.isSymbolicLink() || !rootStat.isDirectory())
2941
3028
  throw new Error("project root must be a real directory, not a symlink");
2942
- return realpathSync4(resolved);
3029
+ return realpathSync5(resolved);
2943
3030
  }
2944
3031
  function resolveSidecarPath(canonicalRoot, activePath, archivedPath) {
2945
3032
  if (sidecarPresent(canonicalRoot, activePath))
@@ -3016,7 +3103,7 @@ function readTaskIntent(root, taskId, requestedPath) {
3016
3103
  const after = lstatSync4(target);
3017
3104
  assertSameIdentity(statIdentity(before), after, "intent sidecar");
3018
3105
  assertIdentitiesUnchanged(pathIdentities, canonicalRoot, sidecarPath);
3019
- const canonicalAgain = realpathSync4(root);
3106
+ const canonicalAgain = realpathSync5(root);
3020
3107
  if (canonicalAgain !== canonicalRoot)
3021
3108
  throw new Error("canonical project root drifted while being read");
3022
3109
  if (lstatSync4(canonicalAgain).isSymbolicLink())
@@ -3759,7 +3846,7 @@ function revisionFor(content) {
3759
3846
  }
3760
3847
  function canonicalRoot(root) {
3761
3848
  try {
3762
- return realpathSync5(root);
3849
+ return realpathSync6(root);
3763
3850
  } catch {
3764
3851
  throw new KernelStoreSecurityError("project root is unavailable");
3765
3852
  }
@@ -5650,6 +5737,8 @@ function applyTaskAction(input) {
5650
5737
  };
5651
5738
  }
5652
5739
  if (input.terminal) {
5740
+ if (nextRecord.lifecycle === "active")
5741
+ throw new Error("terminal settlement requires a done or stopped TaskRecord lifecycle");
5653
5742
  const tombstone = {
5654
5743
  contract: TASK_TOMBSTONE_CONTRACT,
5655
5744
  task_id,
@@ -6389,6 +6478,8 @@ function enrollCanaryTask(root, input, registry) {
6389
6478
  if (checks.gitBaseHead !== gitBaseHead)
6390
6479
  throw new Error("Git HEAD moved after the enrollment confirmation");
6391
6480
  registry.consume(input.capability, input.capability_binding);
6481
+ if (!gitBaseHead)
6482
+ throw new Error("enrollment requires a committed Git HEAD");
6392
6483
  const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
6393
6484
  const nextWorkspace = {
6394
6485
  ...checks.workspace.state,
@@ -6564,6 +6655,9 @@ async function submitClaudeReview(host, coordinator, ctx, taskId, verdictInput)
6564
6655
  }
6565
6656
  return coordinator.submitReview(taskId, ctx, verdictInput);
6566
6657
  }
6658
+ function stopReason(value) {
6659
+ return typeof value === "string" && value.length > 0 ? value : "user stop";
6660
+ }
6567
6661
  function assertProjectionBinding(before, after, allowDiffChange = false) {
6568
6662
  const fields = allowDiffChange ? ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash"] : ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash", "diff_hash"];
6569
6663
  if (before.error || !before.claim || after.error || !after.claim || before.claim.task_id !== after.claim.task_id || fields.some((field) => before.projection[field] !== after.projection[field])) {
@@ -6573,22 +6667,57 @@ function assertProjectionBinding(before, after, allowDiffChange = false) {
6573
6667
  function qaOutcomes(record) {
6574
6668
  return Object.fromEntries(record.attestations.filter((item) => item.kind === "qa").flatMap((item) => item.acceptance_results).map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]));
6575
6669
  }
6670
+ async function ensureClaudeReviewRevision(root, taskId, projection) {
6671
+ const current = await readTaskRecord(root, taskId);
6672
+ const record = current.record;
6673
+ if (!record)
6674
+ throw new Error(`task ${taskId} has no TaskRecord`);
6675
+ if (current.revision !== projection.projection.record_revision)
6676
+ throw new Error("TaskRecord changed before Review revision preparation");
6677
+ if (record.contract !== "assurance_kernel/task_record/v4")
6678
+ return null;
6679
+ if (!record.git_base_head)
6680
+ throw new Error("Review revision requires a TaskRecord v4 git_base_head");
6681
+ const manifest = captureReviewManifest(root, {
6682
+ taskId,
6683
+ baseHead: record.git_base_head,
6684
+ scopeHint: record.intent_snapshot.scope_hint,
6685
+ expectedDiffHash: projection.projection.diff_hash,
6686
+ intentRevision: projection.projection.intent_revision,
6687
+ intentContentHash: projection.projection.intent_content_hash,
6688
+ recordRevision: projection.projection.record_revision,
6689
+ workspaceRevision: projection.projection.workspace_revision,
6690
+ lifecycle: projection.projection.lifecycle,
6691
+ artifactState: projection.projection.artifact_state,
6692
+ risk: record.intent_snapshot.risk,
6693
+ outcomes: qaOutcomes(record)
6694
+ });
6695
+ return {
6696
+ contract: "assurance_kernel/review_revision/v1",
6697
+ base_head: manifest.base_head,
6698
+ review_tree: manifest.review_tree,
6699
+ review_commit: manifest.review_commit,
6700
+ review_ref: manifest.review_ref,
6701
+ diff_hash: manifest.diff_hash,
6702
+ manifest_digest: manifest.manifest_digest
6703
+ };
6704
+ }
6576
6705
  async function buildAssuranceSnapshot(root, taskId, role, projection, runner) {
6577
- const record = await readTaskRecord(root, taskId);
6578
- if (!record.record || record.revision !== projection.projection.record_revision)
6706
+ const read = await readTaskRecord(root, taskId);
6707
+ const record = read.record;
6708
+ if (!record || read.revision !== projection.projection.record_revision)
6579
6709
  throw new Error("TaskRecord changed before assurance snapshot capture");
6580
- const intent = record.record.intent_snapshot;
6710
+ const intent = record.intent_snapshot;
6581
6711
  const descriptors = new Map;
6582
6712
  for (const item of intent.acceptance) {
6583
6713
  const descriptor = parseVerificationDescriptor(item.verification);
6584
6714
  assertRunnerCompatible(descriptor, runner);
6585
6715
  descriptors.set(item.id, descriptor);
6586
6716
  }
6587
- const v4 = record.record.contract === "assurance_kernel/task_record/v4";
6588
- const reviewBundle = role === "review" && !v4 ? captureReviewBundle(root, intent.scope_hint, projection.projection.diff_hash, qaOutcomes(record.record)) : null;
6589
- const reviewManifest = role === "review" && v4 ? captureReviewManifest(root, {
6717
+ const reviewBundle = role === "review" && !isTaskRecordV4(record) ? captureReviewBundle(root, intent.scope_hint, projection.projection.diff_hash, qaOutcomes(record)) : null;
6718
+ const reviewManifest = role === "review" && isTaskRecordV4(record) ? captureReviewManifest(root, {
6590
6719
  taskId,
6591
- baseHead: record.record.git_base_head,
6720
+ baseHead: record.git_base_head,
6592
6721
  scopeHint: intent.scope_hint,
6593
6722
  expectedDiffHash: projection.projection.diff_hash,
6594
6723
  intentRevision: projection.projection.intent_revision,
@@ -6598,7 +6727,7 @@ async function buildAssuranceSnapshot(root, taskId, role, projection, runner) {
6598
6727
  lifecycle: projection.projection.lifecycle,
6599
6728
  artifactState: projection.projection.artifact_state,
6600
6729
  risk: intent.risk,
6601
- outcomes: qaOutcomes(record.record)
6730
+ outcomes: qaOutcomes(record)
6602
6731
  }) : null;
6603
6732
  const dirtyFiles = reviewManifest ? Object.keys(reviewManifest.changed_paths) : reviewBundle ? Object.keys(reviewBundle.dirty_files) : [];
6604
6733
  const snapshot = {
@@ -6697,11 +6826,11 @@ class ClaudeRuntime {
6697
6826
  this.interactive = options.interactive ?? true;
6698
6827
  this.requestConfirmation = options.requestConfirmation;
6699
6828
  this.host = options.host ?? new ClaudeReviewHost(new FileHookEventLog);
6700
- if (options.ports) {
6701
- this.coordinator = new AssuranceCoordinator({ ...options.ports, host: this.host });
6702
- return;
6703
- }
6704
- this.coordinator = new AssuranceCoordinator(this.createKernelPorts());
6829
+ this.coordinator = new AssuranceCoordinator({
6830
+ ...this.createKernelPorts(),
6831
+ ...options.ports,
6832
+ host: this.host
6833
+ });
6705
6834
  }
6706
6835
  observe(event) {
6707
6836
  this.host.observe(event);
@@ -6715,27 +6844,18 @@ class ClaudeRuntime {
6715
6844
  async shutdown() {
6716
6845
  await this.coordinator.onSessionShutdown();
6717
6846
  }
6847
+ kernelPorts() {
6848
+ return this.createKernelPorts();
6849
+ }
6718
6850
  createKernelPorts() {
6719
6851
  return {
6720
6852
  host: this.host,
6721
6853
  projectTask: (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
6722
- readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
6723
- readTaskIntent: (root, taskId) => readTaskIntentForRecord(root, taskId),
6854
+ readTaskRecord: async (root, taskId) => readTaskRecord(root, taskId),
6855
+ readTaskIntent: async (root, taskId) => readTaskIntentForRecord(root, taskId),
6724
6856
  frozenRunner: async () => resolveBunRunner(),
6725
6857
  buildAssurance: (root, taskId, role, projection, runner) => buildAssuranceSnapshot(root, taskId, role, projection, runner),
6726
- ensureReviewRevision: async (root, taskId, projection) => {
6727
- const current = await readTaskRecord(root, taskId);
6728
- if (!current.record)
6729
- throw new Error(`task ${taskId} has no TaskRecord`);
6730
- if (current.record.contract !== "assurance_kernel/task_record/v4")
6731
- return null;
6732
- return ensureReviewRevision(root, {
6733
- taskId,
6734
- baseHead: current.record.git_base_head,
6735
- scopeHint: current.record.intent_snapshot.scope_hint,
6736
- expectedDiffHash: projection.projection.diff_hash
6737
- });
6738
- },
6858
+ ensureReviewRevision: (root, taskId, projection) => ensureClaudeReviewRevision(root, taskId, projection),
6739
6859
  runQa: (snapshot, descriptors, runner, options) => runDeterministicQa(snapshot, descriptors, runner, options),
6740
6860
  writeReviewEvidence: (input) => writeNativeReviewEvidence(input.evidence),
6741
6861
  applyVerdict: (ctx, input) => this.applyVerdict(ctx, input),
@@ -6950,7 +7070,7 @@ class ClaudeRuntime {
6950
7070
  confirmation_ref: confirmation,
6951
7071
  ...op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {},
6952
7072
  ...op === "resolve_user_decision" && decisionOp ? decisionOp : {},
6953
- ...op === "stop" ? { reason: extra.reason ?? "user stop" } : {}
7073
+ ...op === "stop" ? { reason: stopReason(extra.reason) } : {}
6954
7074
  });
6955
7075
  throwIfCancelled(meta.signal);
6956
7076
  const result = app.execute({
@@ -6962,7 +7082,7 @@ class ClaudeRuntime {
6962
7082
  actor_id: actorId,
6963
7083
  ...op === "approve_breaking_intent_revision" ? { next_intent: nextIntent, next_intent_ref: nextIntentRef } : {},
6964
7084
  ...op === "resolve_user_decision" && decisionOp ? decisionOp : {},
6965
- ...op === "stop" ? { reason: extra.reason ?? "user stop" } : {}
7085
+ ...op === "stop" ? { reason: stopReason(extra.reason) } : {}
6966
7086
  },
6967
7087
  prior_intent_token: priorIntent.token,
6968
7088
  diffProvider: diffSnapshotOf,
@@ -17,6 +17,7 @@ import { createInvocationRegistry, type InvocationState, type InvocationToken }
17
17
  import type { ReviewBundle, ReviewManifestV5, ReviewRevision } from "./review_evidence";
18
18
  import { buildRoleDelegationPacket } from "../role_prompt_bridge";
19
19
  import type { AssuranceProjectionResult } from "../kernel/assurance_projection";
20
+ import type { TaskIntentIdentityToken } from "../kernel/intent_token_registry";
20
21
  import type { AssuranceHostPort, HostReviewReservation } from "./host_port";
21
22
 
22
23
  export type AssuranceRole = "qa" | "review";
@@ -38,7 +39,11 @@ export interface TaskRecordRead {
38
39
  record?: { contract?: string; findings: Array<{ kind: string; status: string }> } | null;
39
40
  }
40
41
  export interface TaskIntentRead {
41
- token?: string;
42
+ /**
43
+ * The kernel's own intent identity token. Declared as `string` before this
44
+ * port was ever type checked, which is a shape no host reader has produced.
45
+ */
46
+ token?: TaskIntentIdentityToken;
42
47
  }
43
48
 
44
49
  export interface GithubTerminalProjectionInput {
@@ -873,7 +878,17 @@ export class AssuranceCoordinator {
873
878
  }
874
879
  }
875
880
 
876
- private unknownAfterCommit(taskId: string, operation: "qa" | "review", operationId: string, reason: string): AssuranceAdvanceResult {
881
+ /**
882
+ * Declared as the wider advance result, which made it unusable from
883
+ * `submitReview` without an implicit widen. The value has always been the
884
+ * settlement_unknown member both result unions share.
885
+ */
886
+ private unknownAfterCommit(
887
+ taskId: string,
888
+ operation: "qa" | "review",
889
+ operationId: string,
890
+ reason: string,
891
+ ): { state: "settlement_unknown"; operation: "qa" | "review"; operation_id: string; reason: string } {
877
892
  this.unknownOperations.set(taskId, { operation, operationId, reason });
878
893
  return { state: "settlement_unknown", operation, operation_id: operationId, reason };
879
894
  }
@@ -240,14 +240,32 @@ const SNAPSHOT_IDENTITY = {
240
240
  date: "1970-01-01T00:00:00 +0000",
241
241
  };
242
242
 
243
- export interface ReviewRevision {
243
+ /**
244
+ * The bare synthetic-commit identity. `publishReviewRevision` can prove these
245
+ * five fields from Git alone; it has no manifest inputs and therefore cannot
246
+ * produce a digest.
247
+ */
248
+ export interface ReviewRevisionCommit {
244
249
  contract: "assurance_kernel/review_revision/v1";
245
250
  base_head: string;
246
251
  review_tree: string;
247
252
  review_commit: string;
248
253
  review_ref: string;
249
254
  diff_hash: string;
250
- manifest_digest?: string;
255
+ }
256
+
257
+ /**
258
+ * The full Review revision identity a host port must return.
259
+ *
260
+ * `manifest_digest` was optional here, which is the type-level hole that let a
261
+ * host return the bare commit identity and still satisfy
262
+ * `AssuranceCoordinatorPorts.ensureReviewRevision`. `submitReview` compares all
263
+ * four identity fields against the reservation, so an absent digest compared
264
+ * against a real one and failed every v4 submission at runtime instead of at
265
+ * build time. Requiring it makes that omission a compile error.
266
+ */
267
+ export interface ReviewRevision extends ReviewRevisionCommit {
268
+ manifest_digest: string;
251
269
  }
252
270
 
253
271
  export interface ReviewManifestV5 {
@@ -354,7 +372,7 @@ export function publishReviewRevision(
354
372
  snapshot: GitTaskRevisionSnapshot,
355
373
  diffHash: string,
356
374
  taskId: string,
357
- ): ReviewRevision {
375
+ ): ReviewRevisionCommit {
358
376
  if (snapshot.base_head !== snapshot.base_head.toLowerCase() || !GIT_COMMIT_ID.test(snapshot.base_head))
359
377
  throw new Error("review revision base has invalid identity");
360
378
  if (!REVISION_DIFF_HASH.test(diffHash)) throw new Error("review revision diff hash has invalid identity");
@@ -429,7 +447,7 @@ function publishInput(
429
447
  scopeHint: unknown;
430
448
  expectedDiffHash: string;
431
449
  },
432
- ): { snapshot: GitTaskRevisionSnapshot; revision: ReviewRevision } {
450
+ ): { snapshot: GitTaskRevisionSnapshot; revision: ReviewRevisionCommit } {
433
451
  if (typeof input.baseHead !== "string" || !GIT_COMMIT_ID.test(input.baseHead))
434
452
  throw new Error("review requires a TaskRecord v4 git_base_head");
435
453
  if (!REVISION_DIFF_HASH.test(input.expectedDiffHash))
@@ -486,14 +504,6 @@ export function captureReviewManifest(
486
504
  return manifest;
487
505
  }
488
506
 
489
- /** Publish and return the exact revision a v4 task must use. */
490
- export function ensureReviewRevision(
491
- root: string,
492
- input: { taskId: string; baseHead: string; scopeHint: unknown; expectedDiffHash: string },
493
- ): ReviewRevision {
494
- return publishInput(root, input).revision;
495
- }
496
-
497
507
  export function listReviewRefs(root: string): Array<{ ref: string; commit: string; taskId: string }> {
498
508
  const output = gitEvidence(root, ["for-each-ref", "--format=%(refname) %(objectname)", `${REVIEW_REF_NAMESPACE}/`]);
499
509
  const refs: Array<{ ref: string; commit: string; taskId: string }> = [];
@@ -534,7 +544,7 @@ export function reconcileReviewRefs(
534
544
  return { removed, failed };
535
545
  }
536
546
 
537
- export function deleteReviewRef(root: string, revision: ReviewRevision): void {
547
+ export function deleteReviewRef(root: string, revision: ReviewRevisionCommit): void {
538
548
  const parts = revision.review_ref.split("/");
539
549
  let validRef = false;
540
550
  if (