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 +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-enroll.ts +5 -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 +141 -59
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +17 -8
- package/plugins/immune-brain/dist/imm-loop.md +143 -150
- package/plugins/immune-brain/dist/imm-planner.md +71 -77
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +58 -6
- package/plugins/immune-brain/runtime/assurance/qa.ts +85 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +20 -51
- package/plugins/immune-brain/runtime/kernel/intent.ts +44 -3
- 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
|
@@ -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
|
-
|
|
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 {
|
|
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.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
|
|
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
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 {
|