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 +3 -3
- package/README.zh-CN.md +3 -3
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +3 -96
- package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +20 -5
- package/plugins/immune-brain/.pi-extension/pi-canary-native-review.ts +2 -2
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +95 -52
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +17 -8
- package/plugins/immune-brain/dist/imm-loop.md +134 -150
- package/plugins/immune-brain/dist/imm-planner.md +71 -77
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +19 -5
- package/plugins/immune-brain/runtime/assurance/qa.ts +85 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +1 -45
- package/plugins/immune-brain/runtime/kernel/intent.ts +7 -2
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
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` (
|
|
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` |
|
|
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
|
|
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
|
|
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` |
|
|
153
|
+
| Initiative 载体 | `local` / `github` | 无默认,Planner 询问 | 仅当提案拆分为多个 TaskIntent 时生效 |
|
|
154
154
|
| Advisory subagent | 允许 / 单人 | 允许 | 受 Pi host 策略与用户显式指令约束 |
|
|
155
155
|
|
|
156
|
-
优先级:**当前消息 > 仓库
|
|
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
|
@@ -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 {
|
|
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 "
|
|
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(
|
|
344
|
-
`${theme.fg("muted", "Result:")} ${theme.fg(
|
|
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 (
|
|
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: "
|
|
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: "
|
|
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.
|
|
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
|
|
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
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|