immune-brain 3.5.0 → 3.6.1

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.
package/README.md CHANGED
@@ -139,7 +139,7 @@ Key invariants:
139
139
 
140
140
  ## Configuration
141
141
 
142
- Immune-Brain has **no separate config file**. Preferences live in `AGENTS.md` (repo root or `~/.pi/agent/AGENTS.md`):
142
+ Immune-Brain has **no separate config file**. Preferences live in your host's agent instruction file at the repo root — `AGENTS.md` (Pi) or `CLAUDE.md` (Claude Code):
143
143
 
144
144
  ```md
145
145
  ## Immune-Brain Preferences
@@ -150,10 +150,10 @@ Immune-Brain has **no separate config file**. Preferences live in `AGENTS.md` (r
150
150
  | Preference | Options | Default | Notes |
151
151
  |---|---|---|---|
152
152
  | Reply language | any natural language | repo `AGENTS.md` | Machine contracts / paths stay literal |
153
- | Initiative carrier | `local` / `github` | `github` | Only matters when a proposal splits across multiple TaskIntents |
153
+ | Initiative carrier | `local` / `github` | none — Planner asks | Only matters when a proposal splits across multiple TaskIntents |
154
154
  | Advisory subagents | allowed / solo | allowed | Respects Pi host policy + explicit user instruction |
155
155
 
156
- Precedence: **current message > repo `AGENTS.md` > `~/.pi/agent/AGENTS.md` > skill default**.
156
+ Precedence: **current message > repo agent instruction file > user-level agent instruction file > ask**. Skills read these files directly, so a preference works even when the host does not auto-load that file.
157
157
 
158
158
  See [`docs/reference/immune-brain-config.md`](docs/reference/immune-brain-config.md) for details.
159
159
 
package/README.zh-CN.md CHANGED
@@ -139,7 +139,7 @@ Executor、QA、Review、Compounder 等为 `imm-loop` 内部调度的角色,
139
139
 
140
140
  ## 配置
141
141
 
142
- Immune-Brain **没有独立配置文件**,偏好设置写在 `AGENTS.md`(仓库根目录或 `~/.pi/agent/AGENTS.md`):
142
+ Immune-Brain **没有独立配置文件**,偏好设置写在仓库根目录下当前 Host 的 agent 指令文件里——`AGENTS.md`(Pi)或 `CLAUDE.md`(Claude Code):
143
143
 
144
144
  ```md
145
145
  ## Immune-Brain Preferences
@@ -150,10 +150,10 @@ Immune-Brain **没有独立配置文件**,偏好设置写在 `AGENTS.md`(仓
150
150
  | 偏好 | 选项 | 默认 | 说明 |
151
151
  |---|---|---|---|
152
152
  | 回复语言 | 任意自然语言 | 仓库 `AGENTS.md` | 机器契约/路径/标识符保持原文 |
153
- | Initiative 载体 | `local` / `github` | `github` | 仅当提案拆分为多个 TaskIntent 时生效 |
153
+ | Initiative 载体 | `local` / `github` | 无默认,Planner 询问 | 仅当提案拆分为多个 TaskIntent 时生效 |
154
154
  | Advisory subagent | 允许 / 单人 | 允许 | 受 Pi host 策略与用户显式指令约束 |
155
155
 
156
- 优先级:**当前消息 > 仓库 `AGENTS.md` > `~/.pi/agent/AGENTS.md` > Skill 默认值**。
156
+ 优先级:**当前消息 > 仓库 agent 指令文件 > 用户级 agent 指令文件 > 询问**。Skill 会直接读取这些文件,因此即使 Host 不自动加载该文件,偏好依然生效。
157
157
 
158
158
  详见 [`docs/reference/immune-brain-config.md`](docs/reference/immune-brain-config.md)。
159
159
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "3.5.0",
3
+ "version": "3.6.1",
4
4
  "description": "Immune-Brain agent skill system",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "immune-brain",
3
- "version": "3.5.0",
3
+ "version": "3.6.1",
4
4
  "description": "Immune-Brain Claude Code Host: native Enrollment, QA, Review, and Kernel settlement.",
5
5
  "author": {
6
6
  "name": "Immune-Brain Team"
@@ -381,7 +381,11 @@ async function executeForegroundEnrollment(
381
381
  taskIntent = await readTaskIntent(root, taskId);
382
382
  } catch (error) {
383
383
  const message = errorMessage(error);
384
- if (/not Git-tracked|ENOENT|no such file/i.test(message))
384
+ // `readTaskIntent` reports an absent sidecar semantically now, so the
385
+ // "missing" classification must recognise that wording as well as the raw
386
+ // filesystem errors; otherwise a missing file is misreported as a schema
387
+ // defect and the operator is told to repair fields that do not exist.
388
+ if (/not Git-tracked|ENOENT|no such file|sidecar is missing/i.test(message))
385
389
  return terminal(action, taskId, "blocked", stage, "A Git-tracked TaskIntent is required for Kernel enrollment", "author and stage the canonical TaskIntent");
386
390
  return terminal(action, taskId, "blocked", stage, `TaskIntent validation failed before rehearsal: ${message}`, "repair the reported TaskIntent schema errors");
387
391
  }
@@ -26,8 +26,6 @@ import {
26
26
  canonicalDescriptorBytes,
27
27
  resolveBunRunner,
28
28
  assertRunnerCompatible,
29
- runFixedVerification,
30
- VerificationAbortedError,
31
29
  findingsDigest,
32
30
  type FrozenRunner,
33
31
  type VerificationDescriptor,
@@ -43,7 +41,7 @@ import {
43
41
  type ReviewRevision,
44
42
  } from "./pi-canary-review-bundle";
45
43
  import type { InvocationToken } from "./pi-canary-invocations";
46
- import { qaFindingId } from "./pi-canary-qa-findings";
44
+ import { runDeterministicQa } from "../runtime/assurance/qa";
47
45
  import {
48
46
  reservedAgentParams,
49
47
  type ReservedAgentParams,
@@ -1268,97 +1266,6 @@ async function reconcileReviewRevisionRefs(root: string): Promise<{ removed: str
1268
1266
  return reconcileReviewRefs(root, live);
1269
1267
  }
1270
1268
 
1271
- export interface QaVerificationProgressInput {
1272
- index: number;
1273
- total: number;
1274
- acceptance_id: string;
1275
- phase: "running" | "passed" | "failed";
1276
- elapsed_ms: number;
1277
- }
1278
-
1279
- export function boundedVerificationFailureDetail(stdout: string, stderr: string): string {
1280
- const output = (stderr || stdout).trim();
1281
- const limit = 500;
1282
- if (output.length <= limit) return output;
1283
- const marker = "\n... output omitted ...\n";
1284
- const available = limit - marker.length;
1285
- const headLength = Math.floor(available / 3);
1286
- const tailLength = available - headLength;
1287
- return `${output.slice(0, headLength)}${marker}${output.slice(-tailLength)}`;
1288
- }
1289
-
1290
- export async function runDeterministicQa(
1291
- snapshot: SnapshotDescriptor,
1292
- descriptors: Map<string, VerificationDescriptor>,
1293
- runner: FrozenRunner,
1294
- options: {
1295
- signal?: AbortSignal;
1296
- onProgress?: (progress: QaVerificationProgressInput) => void;
1297
- runVerification?: typeof runFixedVerification;
1298
- } = {},
1299
- ): Promise<AssuranceVerdict> {
1300
- if (snapshot.role !== "qa") throw new Error("deterministic QA requires qa role");
1301
- if (options.signal?.aborted) throw new VerificationAbortedError();
1302
- const findings: NonNullable<AssuranceVerdict["findings"]> = [];
1303
- const runVerification = options.runVerification ?? runFixedVerification;
1304
- for (const [offset, item] of snapshot.acceptance.entries()) {
1305
- if (options.signal?.aborted) throw new VerificationAbortedError();
1306
- const descriptor = descriptors.get(item.id);
1307
- if (!descriptor) throw new Error(`verification descriptor missing for ${item.id}`);
1308
- const startedAt = Date.now();
1309
- options.onProgress?.({
1310
- index: offset + 1,
1311
- total: snapshot.acceptance.length,
1312
- acceptance_id: item.id,
1313
- phase: "running",
1314
- elapsed_ms: 0,
1315
- });
1316
- const result = await runVerification(snapshot.root, descriptor, runner, {
1317
- signal: options.signal,
1318
- });
1319
- const failed = result.exit_code !== 0 || result.timed_out;
1320
- options.onProgress?.({
1321
- index: offset + 1,
1322
- total: snapshot.acceptance.length,
1323
- acceptance_id: item.id,
1324
- phase: failed ? "failed" : "passed",
1325
- elapsed_ms: Date.now() - startedAt,
1326
- });
1327
- if (failed) {
1328
- const detail = boundedVerificationFailureDetail(result.stdout, result.stderr);
1329
- findings.push({
1330
- id: qaFindingId(item.id, snapshotDigest(snapshot)),
1331
- kind: "blocking",
1332
- acceptance_id: item.id,
1333
- summary: `verification failed (exit ${result.exit_code}${result.timed_out ? ", timed out" : ""}) stdout=${result.stdout.length}B stderr=${result.stderr.length}B${detail ? `: ${detail}` : ""}`,
1334
- findings_digest: "",
1335
- });
1336
- }
1337
- }
1338
- if (findings.length > 0) {
1339
- return {
1340
- contract: "assurance_kernel/assurance_verdict/v2",
1341
- role: "qa",
1342
- task_id: snapshot.task_id,
1343
- snapshot_digest: snapshotDigest(snapshot),
1344
- decision: "rework",
1345
- findings,
1346
- };
1347
- }
1348
- return {
1349
- contract: "assurance_kernel/assurance_verdict/v2",
1350
- role: "qa",
1351
- task_id: snapshot.task_id,
1352
- snapshot_digest: snapshotDigest(snapshot),
1353
- decision: "pass",
1354
- approval: {
1355
- kind: "qa",
1356
- authority_role: "qa",
1357
- summary: `all ${snapshot.acceptance.length} fixed verification descriptor(s) passed`,
1358
- },
1359
- };
1360
- }
1361
-
1362
1269
  async function applyAssuranceVerdict(
1363
1270
  ctx: ExtensionContext,
1364
1271
  snapshot: SnapshotDescriptor,
@@ -1795,7 +1702,7 @@ async function enrichAssuranceResult(
1795
1702
 
1796
1703
  function nextActionForAssuranceResult(result: Record<string, unknown>, taskState: AssuranceTaskState): string {
1797
1704
  if ("error" in taskState) return "inspect authority state";
1798
- if (result.state === "review_preparation_failed") return "retry advance_assurance";
1705
+ if (result.state === "review_preparation_failed") return "repair Review preparation, then retry advance_assurance; QA is already committed";
1799
1706
  if (result.code === "verdict_invalid") return "fix the verdict payload and resubmit submit_review; the Review reservation remains active; do not re-dispatch the reviewer";
1800
1707
  if (taskState.lifecycle === "done" || taskState.lifecycle === "stopped") return "none";
1801
1708
  switch (result.state) {
@@ -1806,7 +1713,7 @@ function nextActionForAssuranceResult(result: Record<string, unknown>, taskState
1806
1713
  case "stopped": return "none";
1807
1714
  case "rework": return "repair findings, then advance assurance";
1808
1715
  case "cancelled": return "retry the interrupted foreground operation";
1809
- case "settlement_unknown": return "inspect authority state";
1716
+ case "settlement_unknown": return "advance_assurance to reconcile Kernel state; do not replay the uncertain write";
1810
1717
  case "blocked":
1811
1718
  case "failed":
1812
1719
  default: return taskState.next_obligation;
@@ -43,6 +43,7 @@ export interface TaskRailView {
43
43
  next: string;
44
44
  /** Assurance phase label derived from the normalized Rail state. */
45
45
  phase?: string;
46
+ recovery?: string;
46
47
  /** Latest per-descriptor QA fact; rendered only while present. */
47
48
  acceptance_progress?: TaskRailAcceptanceProgress;
48
49
  }
@@ -209,7 +210,7 @@ export function presentTaskRailResult(
209
210
  const lifecycle = string(taskState?.lifecycle) ?? string(details.lifecycle) ?? string(details.stage);
210
211
  const operation = string(details.operation);
211
212
  const rawState = string(details.state);
212
- const result = string(details.result) ?? string(details.reason) ?? operation ?? rawState ?? "Task state updated";
213
+ const result = string(details.result) ?? string(details.reason) ?? string(details.summary) ?? operation ?? rawState ?? "Task state updated";
213
214
  const next = string(details.next_action) ?? "Follow the projected Obligation";
214
215
  const current = details.current;
215
216
  const total = details.total;
@@ -231,6 +232,7 @@ export function presentTaskRailResult(
231
232
  result,
232
233
  next,
233
234
  phase: string(details.stage),
235
+ recovery: recoveryHint(details),
234
236
  acceptance_progress: hasAcceptanceProgress
235
237
  ? {
236
238
  current,
@@ -336,14 +338,17 @@ export function renderStructuredResult(
336
338
  const taskState = record(details.task_state);
337
339
  const lifecycle = string(taskState?.lifecycle) ?? string(details.lifecycle) ?? string(details.stage);
338
340
  const state = string(details.state) ?? "unknown";
339
- const summary = string(details.result) ?? string(details.reason) ?? string(details.operation) ?? state;
341
+ const summary = string(details.result) ?? string(details.reason) ?? string(details.summary) ?? string(details.operation) ?? state;
340
342
  const next = string(details.next_action) ?? "No action reported";
341
343
  const terminal = lifecycle === "done" || lifecycle === "stopped";
344
+ const blocked = railState({ state }) === "Blocked";
342
345
  const lines = [
343
- `${theme.fg("muted", "State:")} ${theme.fg(state === "blocked" || state === "failed" ? "warning" : "accent", lifecycle ?? state)}`,
344
- `${theme.fg("muted", "Result:")} ${theme.fg(state === "blocked" || state === "failed" ? "warning" : "dim", summary)}`,
346
+ `${theme.fg("muted", "State:")} ${theme.fg(blocked ? "warning" : "accent", blocked ? "Blocked" : lifecycle ?? state)}`,
347
+ `${theme.fg("muted", "Result:")} ${theme.fg(blocked ? "warning" : "dim", summary)}`,
345
348
  `${theme.fg("muted", "Next:")} ${theme.fg("dim", next)}`,
346
349
  ];
350
+ const recovery = recoveryHint(details);
351
+ if (recovery) lines.push(`${theme.fg("muted", "Recovery:")} ${theme.fg("dim", recovery)}`);
347
352
  if (terminal && taskState) lines.push(...renderFinalLines(taskState, theme));
348
353
  return new Text(lines.join("\n"), 0, 0);
349
354
  }
@@ -396,6 +401,7 @@ function renderTaskRail(view: TaskRailView, width = 120, theme?: Theme): string[
396
401
  if (view.phase) {
397
402
  lines.push(`${label("Phase:")} ${body(bounded(view.phase, availableContentWidth))}`);
398
403
  }
404
+ if (view.recovery) lines.push(`${label("Recovery:")} ${body(bounded(view.recovery, availableContentWidth))}`);
399
405
  if (view.acceptance_progress) {
400
406
  const progress = view.acceptance_progress;
401
407
  const symbol = progress.state === "passed" ? "✓" : progress.state === "failed" ? "✗" : "●";
@@ -441,8 +447,17 @@ export function renderTaskOverview(view: TaskOverviewView, width = 120, theme?:
441
447
  return lines;
442
448
  }
443
449
 
450
+ function recoveryHint(details: Record<string, unknown>): string | undefined {
451
+ if (details.code === "verdict_invalid") return "Correct Review payload; reservation retained";
452
+ if (details.state === "review_preparation_failed") return "Repair Review preparation; QA already committed";
453
+ if (details.state === "settlement_unknown") return "Reconcile Kernel state; do not replay writes";
454
+ if (details.state === "authority_conflict") return "Resolve authority conflict before resuming";
455
+ if (details.state === "rework") return "Repair blocking findings; retain unrelated fresh evidence";
456
+ return undefined;
457
+ }
458
+
444
459
  function railState(input: { lifecycle?: string; obligation?: string; operation?: string; state?: string; stage?: string }): TaskRailState {
445
- if (input.state === "blocked" || input.state === "failed" || input.state === "settlement_unknown") return "Blocked";
460
+ if (["blocked", "failed", "settlement_unknown", "review_preparation_failed", "authority_conflict", "rework"].includes(input.state ?? "")) return "Blocked";
446
461
  if (input.lifecycle === "done") return "Completed";
447
462
  if (input.lifecycle === "stopped") return "Stopped";
448
463
  if (input.state === "awaiting_user" || input.operation === "request_authorization") return "Approval required";
@@ -9,7 +9,7 @@ export interface ReservedAgentParams {
9
9
  thinking: "";
10
10
  inherit_context: false;
11
11
  isolated: true;
12
- isolation: "worktree";
12
+ isolation: "off";
13
13
  run_in_background: false;
14
14
  max_turns: number;
15
15
  resume: "";
@@ -50,7 +50,7 @@ export function reservedAgentParams(input: {
50
50
  thinking: "",
51
51
  inherit_context: false,
52
52
  isolated: true,
53
- isolation: "worktree",
53
+ isolation: "off",
54
54
  run_in_background: false,
55
55
  max_turns: input.max_turns ?? 16,
56
56
  resume: "",
@@ -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.5.0";
45
+ var PLUGIN_VERSION = "3.6.1";
46
46
 
47
47
  // plugins/immune-brain/runtime/claude/interaction.ts
48
48
  import { createHash, randomUUID } from "node:crypto";
@@ -1275,8 +1275,6 @@ class AssuranceCoordinator {
1275
1275
  this.rejectedReviewOperations.delete(taskId);
1276
1276
  }
1277
1277
  const refreshed = this.active(taskId);
1278
- if (refreshed?.state === "settlement_unknown")
1279
- return refreshed;
1280
1278
  if (refreshed?.state === "running")
1281
1279
  return { state: "blocked", reason: `assurance operation ${refreshed.operation_id} is already running` };
1282
1280
  const operationId = randomUUID2();
@@ -1291,6 +1289,7 @@ class AssuranceCoordinator {
1291
1289
  this.rejectedReviewOperations.delete(taskId);
1292
1290
  let authorityCommitted = false;
1293
1291
  let authorityBoundaryStarted = false;
1292
+ let boundaryBaseline = null;
1294
1293
  let reviewPreparationStarted = false;
1295
1294
  let phase = "preparing";
1296
1295
  const operationLive = () => this.sessionActive && this.sessionGeneration === operationGeneration && this.activeOperations.get(taskId) === operationId && !operationController.signal.aborted;
@@ -1308,18 +1307,30 @@ class AssuranceCoordinator {
1308
1307
  progress(phase, `Preparing deterministic QA for ${taskId}`);
1309
1308
  await this.ports.advanceBeforeProjection?.();
1310
1309
  ensureOperationLive();
1311
- let projection = await this.ports.projectTask(ctx.cwd, taskId);
1310
+ let projection;
1311
+ try {
1312
+ projection = await this.ports.projectTask(ctx.cwd, taskId);
1313
+ } catch (error) {
1314
+ if (!(error instanceof Error) || !("code" in error) || !["EINTR", "EAGAIN"].includes(String(error.code)))
1315
+ throw error;
1316
+ ensureOperationLive();
1317
+ progress("retrying_projection", "Retrying the initial authority read once; no writes replayed", { retry_attempt: 1 });
1318
+ ensureOperationLive();
1319
+ projection = await this.ports.projectTask(ctx.cwd, taskId);
1320
+ }
1312
1321
  ensureOperationLive();
1313
1322
  if (projection.error)
1314
1323
  return { state: "blocked", reason: projection.error };
1315
- if (projection.projection.lifecycle === "done")
1316
- return { state: "completed" };
1317
- if (projection.projection.lifecycle === "stopped")
1318
- return { state: "stopped" };
1324
+ if (projection.projection.lifecycle === "done" || projection.projection.lifecycle === "stopped") {
1325
+ this.unknownOperations.delete(taskId);
1326
+ return { state: projection.projection.lifecycle === "done" ? "completed" : "stopped" };
1327
+ }
1319
1328
  if (!projection.claim)
1320
1329
  return { state: "blocked", reason: "no active backend claim" };
1321
1330
  if (projection.claim.task_id !== taskId)
1322
1331
  return { state: "blocked", reason: `backend claim belongs to ${projection.claim.task_id}, not ${taskId}` };
1332
+ if (projection.projection.lifecycle === "active")
1333
+ this.unknownOperations.delete(taskId);
1323
1334
  const parked = await this.ports.readTaskRecord(ctx.cwd, taskId);
1324
1335
  ensureOperationLive();
1325
1336
  if (parked.record?.findings.some((finding) => finding.kind === "replan_required" && finding.status === "open"))
@@ -1330,6 +1341,7 @@ class AssuranceCoordinator {
1330
1341
  if (aborted())
1331
1342
  return this.cancelled("qa", operationId, "host cancellation before artifact freeze");
1332
1343
  progress("freezing_artifacts", "Freezing planning artifacts for deterministic assurance");
1344
+ boundaryBaseline = projection.projection.record_revision;
1333
1345
  const freeze = this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "freeze_artifacts", actor_id: "executor" } });
1334
1346
  authorityBoundaryStarted = true;
1335
1347
  await freeze;
@@ -1339,13 +1351,17 @@ class AssuranceCoordinator {
1339
1351
  if (projection.error || projection.projection.lifecycle !== "active" || projection.projection.artifact_state !== "frozen")
1340
1352
  return this.unknownAfterCommit(taskId, "qa", operationId, projection.error ?? "artifact freeze did not settle");
1341
1353
  authorityBoundaryStarted = false;
1354
+ boundaryBaseline = null;
1342
1355
  }
1343
1356
  if (projection.projection.next_obligation === "complete") {
1344
1357
  progress("completing", "Completing the routine task after deterministic QA");
1358
+ const completionBaseline = projection.projection.record_revision;
1345
1359
  try {
1346
1360
  await this.ports.applyOrdinaryOperation(ctx, { taskId, operation: { op: "complete", actor_id: "kernel-assurance" } });
1347
1361
  return { state: "completed" };
1348
1362
  } catch (error) {
1363
+ if (await this.mutationProvablyRejected(ctx, taskId, completionBaseline))
1364
+ return { state: "failed", operation: "qa", operation_id: operationId, reason: `${phase}: ${boundedAssuranceError(error)}` };
1349
1365
  return this.unknownAfterCommit(taskId, "qa", operationId, boundedAssuranceError(error));
1350
1366
  }
1351
1367
  }
@@ -1517,8 +1533,12 @@ class AssuranceCoordinator {
1517
1533
  const reason = aborted() || error instanceof VerificationAbortedError ? `${phase}: host cancellation` : `${phase}: ${boundedAssuranceError(error)}`;
1518
1534
  return this.reviewPreparationFailed(taskId, operationId, reason);
1519
1535
  }
1520
- if (authorityCommitted || authorityBoundaryStarted)
1536
+ if (authorityCommitted || authorityBoundaryStarted) {
1537
+ const cancelling = aborted() || error instanceof VerificationAbortedError;
1538
+ if (!authorityCommitted && boundaryBaseline !== null && !cancelling && await this.mutationProvablyRejected(ctx, taskId, boundaryBaseline))
1539
+ return { state: "failed", operation: "qa", operation_id: operationId, reason: `${phase}: ${boundedAssuranceError(error)}` };
1521
1540
  return this.unknownAfterCommit(taskId, "qa", operationId, `${phase}: ${boundedAssuranceError(error)}`);
1541
+ }
1522
1542
  if (aborted() || error instanceof VerificationAbortedError)
1523
1543
  return this.cancelled("qa", operationId, `${phase}: host cancellation`);
1524
1544
  return { state: "failed", operation: "qa", operation_id: operationId, reason: `${phase}: ${boundedAssuranceError(error)}` };
@@ -1533,10 +1553,8 @@ class AssuranceCoordinator {
1533
1553
  }
1534
1554
  async submitReview(taskId, ctx, verdictInput) {
1535
1555
  const unknown = this.unknownOperations.get(taskId);
1536
- if (unknown) {
1537
- this.unknownOperations.delete(taskId);
1556
+ if (unknown)
1538
1557
  return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason };
1539
- }
1540
1558
  const rejected = this.rejectedReviewOperations.get(taskId);
1541
1559
  if (rejected)
1542
1560
  return { state: "blocked", reason: rejected.reason };
@@ -1657,6 +1675,14 @@ class AssuranceCoordinator {
1657
1675
  this.rejectedReviewOperations.delete(taskId);
1658
1676
  return { state: "review_preparation_failed", operation: "review", operation_id: operationId, reason };
1659
1677
  }
1678
+ async mutationProvablyRejected(ctx, taskId, baselineRevision) {
1679
+ try {
1680
+ const fresh = await this.ports.projectTask(ctx.cwd, taskId);
1681
+ return !fresh.error && fresh.projection.record_revision === baselineRevision;
1682
+ } catch {
1683
+ return false;
1684
+ }
1685
+ }
1660
1686
  unknownAfterCommit(taskId, operation, operationId, reason) {
1661
1687
  this.unknownOperations.set(taskId, { operation, operationId, reason });
1662
1688
  return { state: "settlement_unknown", operation, operation_id: operationId, reason };
@@ -2663,11 +2689,16 @@ var INTENT_SIDECAR_RELATIVE_PREFIX = "docs/plans/";
2663
2689
  var RISK_FLOOR_SCOPE_PREFIXES = [
2664
2690
  "plugins/immune-brain/runtime/kernel",
2665
2691
  "plugins/immune-brain/runtime/authority_commit_receipts.ts",
2692
+ "plugins/immune-brain/runtime/assurance",
2693
+ "plugins/immune-brain/runtime/claude/interaction.ts",
2694
+ "plugins/immune-brain/runtime/claude/capability.ts",
2695
+ "plugins/immune-brain/runtime/claude/review_host.ts",
2696
+ "plugins/immune-brain/runtime/claude/kernel_ports.ts",
2697
+ "plugins/immune-brain/runtime/claude/mcp_server.ts",
2666
2698
  "plugins/immune-brain/.pi-extension"
2667
2699
  ];
2668
2700
  var CHANGED_PATH_RISK_FLOOR_PREFIXES = [
2669
- "plugins/immune-brain/runtime/kernel",
2670
- "plugins/immune-brain/.pi-extension",
2701
+ ...RISK_FLOOR_SCOPE_PREFIXES,
2671
2702
  "docs/specs",
2672
2703
  "docs/plans"
2673
2704
  ];
@@ -2903,6 +2934,21 @@ function resolveCanonicalRoot(root) {
2903
2934
  throw new Error("project root must be a real directory, not a symlink");
2904
2935
  return realpathSync4(resolved);
2905
2936
  }
2937
+ function resolveSidecarPath(canonicalRoot, activePath, archivedPath) {
2938
+ if (sidecarPresent(canonicalRoot, activePath))
2939
+ return activePath;
2940
+ if (sidecarPresent(canonicalRoot, archivedPath))
2941
+ return archivedPath;
2942
+ return activePath;
2943
+ }
2944
+ function sidecarPresent(canonicalRoot, relativePath) {
2945
+ try {
2946
+ lstatSync4(join6(canonicalRoot, relativePath));
2947
+ return true;
2948
+ } catch {
2949
+ return false;
2950
+ }
2951
+ }
2906
2952
  function collectPathIdentities(canonicalRoot, relativePath) {
2907
2953
  const identities = [];
2908
2954
  let current = canonicalRoot;
@@ -2930,12 +2976,14 @@ function readTaskIntent(root, taskId, requestedPath) {
2930
2976
  const canonicalRoot = resolveCanonicalRoot(root);
2931
2977
  const activePath = `${INTENT_SIDECAR_RELATIVE_PREFIX}${taskId}.intent.json`;
2932
2978
  const archivedPath = `${INTENT_SIDECAR_RELATIVE_PREFIX}archive/${taskId}.intent.json`;
2933
- const sidecarPath = requestedPath ?? activePath;
2979
+ const sidecarPath = requestedPath ?? resolveSidecarPath(canonicalRoot, activePath, archivedPath);
2934
2980
  if (sidecarPath !== activePath && sidecarPath !== archivedPath)
2935
2981
  throw new Error("intent sidecar path is not the active or archived task path");
2936
2982
  const target = join6(canonicalRoot, sidecarPath);
2937
2983
  if (!target.startsWith(canonicalRoot + sep3))
2938
2984
  throw new Error("intent sidecar escapes project root");
2985
+ if (!sidecarPresent(canonicalRoot, sidecarPath))
2986
+ throw new Error(`TaskIntent sidecar is missing at ${sidecarPath}`);
2939
2987
  const pathIdentities = collectPathIdentities(canonicalRoot, sidecarPath);
2940
2988
  const fileIdentity = pathIdentities[pathIdentities.length - 1];
2941
2989
  try {
@@ -6378,6 +6426,75 @@ function attemptRef(snapshotDigest2) {
6378
6426
  return `${digest8}-${randomUUID4().slice(0, 6)}`;
6379
6427
  }
6380
6428
 
6429
+ // plugins/immune-brain/runtime/assurance/qa.ts
6430
+ async function runDeterministicQa(snapshot, descriptors, runner, options = {}) {
6431
+ if (snapshot.role !== "qa")
6432
+ throw new Error("deterministic QA requires qa role");
6433
+ if (options.signal?.aborted)
6434
+ throw new VerificationAbortedError;
6435
+ const findings = [];
6436
+ const runVerification = options.runVerification ?? runFixedVerification;
6437
+ for (const [offset, item] of snapshot.acceptance.entries()) {
6438
+ if (options.signal?.aborted)
6439
+ throw new VerificationAbortedError;
6440
+ const descriptor = descriptors.get(item.id);
6441
+ if (!descriptor)
6442
+ throw new Error(`verification descriptor missing for ${item.id}`);
6443
+ const startedAt = Date.now();
6444
+ options.onProgress?.({
6445
+ index: offset + 1,
6446
+ total: snapshot.acceptance.length,
6447
+ acceptance_id: item.id,
6448
+ phase: "running",
6449
+ elapsed_ms: 0
6450
+ });
6451
+ const result = await runVerification(snapshot.root, descriptor, runner, {
6452
+ signal: options.signal
6453
+ });
6454
+ if (options.signal?.aborted)
6455
+ throw new VerificationAbortedError;
6456
+ const failed = result.exit_code !== 0 || result.timed_out;
6457
+ options.onProgress?.({
6458
+ index: offset + 1,
6459
+ total: snapshot.acceptance.length,
6460
+ acceptance_id: item.id,
6461
+ phase: failed ? "failed" : "passed",
6462
+ elapsed_ms: Date.now() - startedAt
6463
+ });
6464
+ if (failed) {
6465
+ findings.push({
6466
+ id: qaFindingId(item.id, snapshotDigest(snapshot)),
6467
+ kind: "blocking",
6468
+ acceptance_id: item.id,
6469
+ summary: `verification failed (exit ${result.exit_code}${result.timed_out ? ", timed out" : ""}) stdout=${Buffer.byteLength(result.stdout)}B stderr=${Buffer.byteLength(result.stderr)}B`,
6470
+ findings_digest: ""
6471
+ });
6472
+ }
6473
+ }
6474
+ if (findings.length > 0) {
6475
+ return {
6476
+ contract: "assurance_kernel/assurance_verdict/v2",
6477
+ role: "qa",
6478
+ task_id: snapshot.task_id,
6479
+ snapshot_digest: snapshotDigest(snapshot),
6480
+ decision: "rework",
6481
+ findings
6482
+ };
6483
+ }
6484
+ return {
6485
+ contract: "assurance_kernel/assurance_verdict/v2",
6486
+ role: "qa",
6487
+ task_id: snapshot.task_id,
6488
+ snapshot_digest: snapshotDigest(snapshot),
6489
+ decision: "pass",
6490
+ approval: {
6491
+ kind: "qa",
6492
+ authority_role: "qa",
6493
+ summary: `all ${snapshot.acceptance.length} fixed verification descriptor(s) passed`
6494
+ }
6495
+ };
6496
+ }
6497
+
6381
6498
  // plugins/immune-brain/runtime/claude/kernel_ports.ts
6382
6499
  function diffSnapshotOf(root, record) {
6383
6500
  if (record.contract === "assurance_kernel/task_record/v4") {
@@ -6390,6 +6507,10 @@ function diffSnapshotOf(root, record) {
6390
6507
  function diffHashOf(root, record) {
6391
6508
  return diffSnapshotOf(root, record).diff_hash;
6392
6509
  }
6510
+ function readTaskIntentForRecord(root, taskId) {
6511
+ const currentPath = readTaskRecordRaw(root, taskId).record?.intent_ref?.path;
6512
+ return readTaskIntent(root, taskId, currentPath);
6513
+ }
6393
6514
  function extractVerdictJson(input) {
6394
6515
  if (typeof input === "string") {
6395
6516
  const cleaned = input.split(`
@@ -6442,45 +6563,6 @@ function assertProjectionBinding(before, after, allowDiffChange = false) {
6442
6563
  throw new Error("Task changed after native confirmation; authority aborted before capability issuance");
6443
6564
  }
6444
6565
  }
6445
- async function runDeterministicQa(snapshot, descriptors, runner, options = {}) {
6446
- if (snapshot.role !== "qa")
6447
- throw new Error("deterministic QA requires qa role");
6448
- if (options.signal?.aborted)
6449
- throw new VerificationAbortedError;
6450
- const findings = [];
6451
- for (const [offset, item] of snapshot.acceptance.entries()) {
6452
- if (options.signal?.aborted)
6453
- throw new VerificationAbortedError;
6454
- const descriptor = descriptors.get(item.id);
6455
- if (!descriptor)
6456
- throw new Error(`verification descriptor missing for ${item.id}`);
6457
- const startedAt = Date.now();
6458
- options.onProgress?.({ index: offset + 1, total: snapshot.acceptance.length, acceptance_id: item.id, phase: "running", elapsed_ms: 0 });
6459
- const result = await runFixedVerification(snapshot.root, descriptor, runner, { signal: options.signal });
6460
- const failed = result.exit_code !== 0 || result.timed_out;
6461
- options.onProgress?.({ index: offset + 1, total: snapshot.acceptance.length, acceptance_id: item.id, phase: failed ? "failed" : "passed", elapsed_ms: Date.now() - startedAt });
6462
- if (failed) {
6463
- findings.push({
6464
- id: qaFindingId(item.id, snapshotDigest(snapshot)),
6465
- kind: "blocking",
6466
- acceptance_id: item.id,
6467
- summary: `verification failed (exit ${result.exit_code}${result.timed_out ? ", timed out" : ""})`,
6468
- findings_digest: ""
6469
- });
6470
- }
6471
- }
6472
- if (findings.length > 0) {
6473
- return { contract: "assurance_kernel/assurance_verdict/v2", role: "qa", task_id: snapshot.task_id, snapshot_digest: snapshotDigest(snapshot), decision: "rework", findings };
6474
- }
6475
- return {
6476
- contract: "assurance_kernel/assurance_verdict/v2",
6477
- role: "qa",
6478
- task_id: snapshot.task_id,
6479
- snapshot_digest: snapshotDigest(snapshot),
6480
- decision: "pass",
6481
- approval: { kind: "qa", authority_role: "qa", summary: `all ${snapshot.acceptance.length} fixed verification descriptor(s) passed` }
6482
- };
6483
- }
6484
6566
  function qaOutcomes(record) {
6485
6567
  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 }]));
6486
6568
  }
@@ -6631,7 +6713,7 @@ class ClaudeRuntime {
6631
6713
  host: this.host,
6632
6714
  projectTask: (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
6633
6715
  readTaskRecord: (root, taskId) => readTaskRecord(root, taskId),
6634
- readTaskIntent: (root, taskId) => readTaskIntent(root, taskId),
6716
+ readTaskIntent: (root, taskId) => readTaskIntentForRecord(root, taskId),
6635
6717
  frozenRunner: async () => resolveBunRunner(),
6636
6718
  buildAssurance: (root, taskId, role, projection, runner) => buildAssuranceSnapshot(root, taskId, role, projection, runner),
6637
6719
  ensureReviewRevision: async (root, taskId, projection) => {
@@ -6692,7 +6774,7 @@ class ClaudeRuntime {
6692
6774
  async enroll(taskId, meta) {
6693
6775
  const now = new Date().toISOString();
6694
6776
  const preparation = await preparePiCanary(this.cwd, { task_id: taskId, now });
6695
- const intent = await readTaskIntent(this.cwd, taskId);
6777
+ const intent = await readTaskIntentForRecord(this.cwd, taskId);
6696
6778
  const gate = await this.gate("enroll", { ...meta, taskId }, {
6697
6779
  risk: intent.intent.risk,
6698
6780
  intentRevision: preparation.intent?.revision,
@@ -6767,7 +6849,7 @@ class ClaudeRuntime {
6767
6849
  throw new Error(readiness.blocked ?? "no unique host-derived authorization operation");
6768
6850
  }
6769
6851
  }
6770
- const priorIntent = await readTaskIntent(this.cwd, taskId);
6852
+ const priorIntent = await readTaskIntentForRecord(this.cwd, taskId);
6771
6853
  const now = new Date().toISOString();
6772
6854
  const actorId = "user";
6773
6855
  const nextIntent = extra.next_intent ? await parseTaskIntentV1(extra.next_intent) : undefined;
@@ -6894,7 +6976,7 @@ class ClaudeRuntime {
6894
6976
  }
6895
6977
  async applyVerdict(ctx, input) {
6896
6978
  const { registry, app } = await this.authority();
6897
- const priorIntentToken = (await readTaskIntent(ctx.cwd, input.taskId)).token;
6979
+ const priorIntentToken = (await readTaskIntentForRecord(ctx.cwd, input.taskId)).token;
6898
6980
  const now = new Date().toISOString();
6899
6981
  const commitAndApply = async (apply) => {
6900
6982
  this.coordinator.commitInvocation(input.invocation);
@@ -6976,7 +7058,7 @@ class ClaudeRuntime {
6976
7058
  async executeOrdinary(ctx, input) {
6977
7059
  const { app } = await this.authority();
6978
7060
  const operation = input.operation.op === "revise_intent" ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) } : input.operation;
6979
- const priorIntent = await readTaskIntent(ctx.cwd, input.taskId);
7061
+ const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
6980
7062
  const sidecar = join7(ctx.cwd, priorIntent.intent_ref.path);
6981
7063
  const priorBytes = operation.op === "revise_intent" ? readFileSync7(sidecar) : null;
6982
7064
  try {