immune-brain 3.5.0 → 3.6.0

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.0",
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.0",
4
4
  "description": "Immune-Brain Claude Code Host: native Enrollment, QA, Review, and Kernel settlement.",
5
5
  "author": {
6
6
  "name": "Immune-Brain Team"
@@ -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.0";
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();
@@ -1308,18 +1306,30 @@ class AssuranceCoordinator {
1308
1306
  progress(phase, `Preparing deterministic QA for ${taskId}`);
1309
1307
  await this.ports.advanceBeforeProjection?.();
1310
1308
  ensureOperationLive();
1311
- let projection = await this.ports.projectTask(ctx.cwd, taskId);
1309
+ let projection;
1310
+ try {
1311
+ projection = await this.ports.projectTask(ctx.cwd, taskId);
1312
+ } catch (error) {
1313
+ if (!(error instanceof Error) || !("code" in error) || !["EINTR", "EAGAIN"].includes(String(error.code)))
1314
+ throw error;
1315
+ ensureOperationLive();
1316
+ progress("retrying_projection", "Retrying the initial authority read once; no writes replayed", { retry_attempt: 1 });
1317
+ ensureOperationLive();
1318
+ projection = await this.ports.projectTask(ctx.cwd, taskId);
1319
+ }
1312
1320
  ensureOperationLive();
1313
1321
  if (projection.error)
1314
1322
  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" };
1323
+ if (projection.projection.lifecycle === "done" || projection.projection.lifecycle === "stopped") {
1324
+ this.unknownOperations.delete(taskId);
1325
+ return { state: projection.projection.lifecycle === "done" ? "completed" : "stopped" };
1326
+ }
1319
1327
  if (!projection.claim)
1320
1328
  return { state: "blocked", reason: "no active backend claim" };
1321
1329
  if (projection.claim.task_id !== taskId)
1322
1330
  return { state: "blocked", reason: `backend claim belongs to ${projection.claim.task_id}, not ${taskId}` };
1331
+ if (projection.projection.lifecycle === "active")
1332
+ this.unknownOperations.delete(taskId);
1323
1333
  const parked = await this.ports.readTaskRecord(ctx.cwd, taskId);
1324
1334
  ensureOperationLive();
1325
1335
  if (parked.record?.findings.some((finding) => finding.kind === "replan_required" && finding.status === "open"))
@@ -1533,10 +1543,8 @@ class AssuranceCoordinator {
1533
1543
  }
1534
1544
  async submitReview(taskId, ctx, verdictInput) {
1535
1545
  const unknown = this.unknownOperations.get(taskId);
1536
- if (unknown) {
1537
- this.unknownOperations.delete(taskId);
1546
+ if (unknown)
1538
1547
  return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason };
1539
- }
1540
1548
  const rejected = this.rejectedReviewOperations.get(taskId);
1541
1549
  if (rejected)
1542
1550
  return { state: "blocked", reason: rejected.reason };
@@ -2663,11 +2671,16 @@ var INTENT_SIDECAR_RELATIVE_PREFIX = "docs/plans/";
2663
2671
  var RISK_FLOOR_SCOPE_PREFIXES = [
2664
2672
  "plugins/immune-brain/runtime/kernel",
2665
2673
  "plugins/immune-brain/runtime/authority_commit_receipts.ts",
2674
+ "plugins/immune-brain/runtime/assurance",
2675
+ "plugins/immune-brain/runtime/claude/interaction.ts",
2676
+ "plugins/immune-brain/runtime/claude/capability.ts",
2677
+ "plugins/immune-brain/runtime/claude/review_host.ts",
2678
+ "plugins/immune-brain/runtime/claude/kernel_ports.ts",
2679
+ "plugins/immune-brain/runtime/claude/mcp_server.ts",
2666
2680
  "plugins/immune-brain/.pi-extension"
2667
2681
  ];
2668
2682
  var CHANGED_PATH_RISK_FLOOR_PREFIXES = [
2669
- "plugins/immune-brain/runtime/kernel",
2670
- "plugins/immune-brain/.pi-extension",
2683
+ ...RISK_FLOOR_SCOPE_PREFIXES,
2671
2684
  "docs/specs",
2672
2685
  "docs/plans"
2673
2686
  ];
@@ -6378,6 +6391,75 @@ function attemptRef(snapshotDigest2) {
6378
6391
  return `${digest8}-${randomUUID4().slice(0, 6)}`;
6379
6392
  }
6380
6393
 
6394
+ // plugins/immune-brain/runtime/assurance/qa.ts
6395
+ async function runDeterministicQa(snapshot, descriptors, runner, options = {}) {
6396
+ if (snapshot.role !== "qa")
6397
+ throw new Error("deterministic QA requires qa role");
6398
+ if (options.signal?.aborted)
6399
+ throw new VerificationAbortedError;
6400
+ const findings = [];
6401
+ const runVerification = options.runVerification ?? runFixedVerification;
6402
+ for (const [offset, item] of snapshot.acceptance.entries()) {
6403
+ if (options.signal?.aborted)
6404
+ throw new VerificationAbortedError;
6405
+ const descriptor = descriptors.get(item.id);
6406
+ if (!descriptor)
6407
+ throw new Error(`verification descriptor missing for ${item.id}`);
6408
+ const startedAt = Date.now();
6409
+ options.onProgress?.({
6410
+ index: offset + 1,
6411
+ total: snapshot.acceptance.length,
6412
+ acceptance_id: item.id,
6413
+ phase: "running",
6414
+ elapsed_ms: 0
6415
+ });
6416
+ const result = await runVerification(snapshot.root, descriptor, runner, {
6417
+ signal: options.signal
6418
+ });
6419
+ if (options.signal?.aborted)
6420
+ throw new VerificationAbortedError;
6421
+ const failed = result.exit_code !== 0 || result.timed_out;
6422
+ options.onProgress?.({
6423
+ index: offset + 1,
6424
+ total: snapshot.acceptance.length,
6425
+ acceptance_id: item.id,
6426
+ phase: failed ? "failed" : "passed",
6427
+ elapsed_ms: Date.now() - startedAt
6428
+ });
6429
+ if (failed) {
6430
+ findings.push({
6431
+ id: qaFindingId(item.id, snapshotDigest(snapshot)),
6432
+ kind: "blocking",
6433
+ acceptance_id: item.id,
6434
+ summary: `verification failed (exit ${result.exit_code}${result.timed_out ? ", timed out" : ""}) stdout=${Buffer.byteLength(result.stdout)}B stderr=${Buffer.byteLength(result.stderr)}B`,
6435
+ findings_digest: ""
6436
+ });
6437
+ }
6438
+ }
6439
+ if (findings.length > 0) {
6440
+ return {
6441
+ contract: "assurance_kernel/assurance_verdict/v2",
6442
+ role: "qa",
6443
+ task_id: snapshot.task_id,
6444
+ snapshot_digest: snapshotDigest(snapshot),
6445
+ decision: "rework",
6446
+ findings
6447
+ };
6448
+ }
6449
+ return {
6450
+ contract: "assurance_kernel/assurance_verdict/v2",
6451
+ role: "qa",
6452
+ task_id: snapshot.task_id,
6453
+ snapshot_digest: snapshotDigest(snapshot),
6454
+ decision: "pass",
6455
+ approval: {
6456
+ kind: "qa",
6457
+ authority_role: "qa",
6458
+ summary: `all ${snapshot.acceptance.length} fixed verification descriptor(s) passed`
6459
+ }
6460
+ };
6461
+ }
6462
+
6381
6463
  // plugins/immune-brain/runtime/claude/kernel_ports.ts
6382
6464
  function diffSnapshotOf(root, record) {
6383
6465
  if (record.contract === "assurance_kernel/task_record/v4") {
@@ -6442,45 +6524,6 @@ function assertProjectionBinding(before, after, allowDiffChange = false) {
6442
6524
  throw new Error("Task changed after native confirmation; authority aborted before capability issuance");
6443
6525
  }
6444
6526
  }
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
6527
  function qaOutcomes(record) {
6485
6528
  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
6529
  }
@@ -1,25 +1,34 @@
1
- # Immune-Brain Pi Preferences
1
+ # Immune-Brain Host Preferences
2
2
 
3
3
  Pi and Claude Code are supported code-agent hosts. Immune-Brain does not load an
4
4
  agent-local TOML file or Immune-Brain-specific environment overrides. User and
5
- project preferences belong in Pi-injected `AGENTS.md` instructions.
5
+ project preferences belong in the Host's agent instruction files.
6
6
 
7
7
  ## Precedence
8
8
 
9
9
  Planner preferences resolve in this order:
10
10
 
11
11
  1. a literal instruction in the current request;
12
- 2. the repository root `AGENTS.md`;
13
- 3. `~/.pi/agent/AGENTS.md`; or
14
- 4. the Skill's documented default or an explicit user question.
12
+ 2. the repository root agent instruction file, whichever the repository tracks:
13
+ `AGENTS.md` or `CLAUDE.md`;
14
+ 3. the Host's user-level agent instruction file; or
15
+ 4. an explicit user question.
15
16
 
16
- Invalid values are reported rather than guessed.
17
+ Hosts differ in which files they auto-load: Pi injects `AGENTS.md`, while Claude
18
+ Code auto-loads `CLAUDE.md` and does not read `~/.pi/agent/AGENTS.md` at all. A
19
+ Skill therefore reads sources 2 and 3 directly instead of assuming the Host
20
+ placed them in context, and reports which sources it checked.
21
+
22
+ Invalid values are reported rather than guessed. A preference with no documented
23
+ default resolves to a user question, never to a silently chosen value.
17
24
 
18
25
  ## Initiative Carrier
19
26
 
20
27
  The Initiative carrier preference applies only to proposals split across
21
28
  multiple TaskIntents. Ordinary TaskIntents remain tracked by Kernel
22
- TaskRecords. Set one of these fixed directives in `AGENTS.md`:
29
+ TaskRecords. There is no built-in carrier default; when no directive is found,
30
+ Planner asks. Set one of these fixed directives in the repository root
31
+ `AGENTS.md` or `CLAUDE.md`:
23
32
 
24
33
  ```md
25
34
  ## Immune-Brain Preferences
@@ -33,7 +42,7 @@ TaskRecords. Set one of these fixed directives in `AGENTS.md`:
33
42
  - Initiative carrier default: github
34
43
  ```
35
44
 
36
- A repository directive overrides the global directive. A configured `github`
45
+ A repository directive overrides the user-level directive. A configured `github`
37
46
  default is standing opt-in for GitHub projection, but the literal user still
38
47
  confirms the Initiative name, immutable slug, complete Parent/Child decomposition,
39
48
  granularity, and dependencies before the first remote mutation. Planner reports