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
|
@@ -456,7 +456,6 @@ export class AssuranceCoordinator {
|
|
|
456
456
|
this.rejectedReviewOperations.delete(taskId);
|
|
457
457
|
}
|
|
458
458
|
const refreshed = this.active(taskId);
|
|
459
|
-
if (refreshed?.state === "settlement_unknown") return refreshed;
|
|
460
459
|
if (refreshed?.state === "running") return { state: "blocked", reason: `assurance operation ${refreshed.operation_id} is already running` };
|
|
461
460
|
const operationId = randomUUID();
|
|
462
461
|
const operationGeneration = this.sessionGeneration;
|
|
@@ -490,13 +489,28 @@ export class AssuranceCoordinator {
|
|
|
490
489
|
progress(phase, `Preparing deterministic QA for ${taskId}`);
|
|
491
490
|
await this.ports.advanceBeforeProjection?.();
|
|
492
491
|
ensureOperationLive();
|
|
493
|
-
let projection
|
|
492
|
+
let projection: AssuranceProjectionResult;
|
|
493
|
+
try {
|
|
494
|
+
projection = await this.ports.projectTask(ctx.cwd, taskId);
|
|
495
|
+
} catch (error) {
|
|
496
|
+
// Retry only an explicit transient read failure, before any write.
|
|
497
|
+
if (!(error instanceof Error) || !("code" in error) || !["EINTR", "EAGAIN"].includes(String(error.code))) throw error;
|
|
498
|
+
ensureOperationLive();
|
|
499
|
+
progress("retrying_projection", "Retrying the initial authority read once; no writes replayed", { retry_attempt: 1 });
|
|
500
|
+
ensureOperationLive();
|
|
501
|
+
projection = await this.ports.projectTask(ctx.cwd, taskId);
|
|
502
|
+
}
|
|
494
503
|
ensureOperationLive();
|
|
495
504
|
if (projection.error) return { state: "blocked", reason: projection.error };
|
|
496
|
-
if (projection.projection.lifecycle === "done"
|
|
497
|
-
|
|
505
|
+
if (projection.projection.lifecycle === "done" || projection.projection.lifecycle === "stopped") {
|
|
506
|
+
this.unknownOperations.delete(taskId);
|
|
507
|
+
return { state: projection.projection.lifecycle === "done" ? "completed" : "stopped" };
|
|
508
|
+
}
|
|
498
509
|
if (!projection.claim) return { state: "blocked", reason: "no active backend claim" };
|
|
499
510
|
if (projection.claim.task_id !== taskId) return { state: "blocked", reason: `backend claim belongs to ${projection.claim.task_id}, not ${taskId}` };
|
|
511
|
+
// A closed Kernel projection, not the lost host reply, decides what
|
|
512
|
+
// remains. Committed QA/Review is never replayed from local memory.
|
|
513
|
+
if (projection.projection.lifecycle === "active") this.unknownOperations.delete(taskId);
|
|
500
514
|
const parked = await this.ports.readTaskRecord(ctx.cwd, taskId);
|
|
501
515
|
ensureOperationLive();
|
|
502
516
|
if (parked.record?.findings.some((finding) => finding.kind === "replan_required" && finding.status === "open")) return { state: "blocked", reason: "review rework limit reached; a durable replan is required" };
|
|
@@ -700,7 +714,7 @@ export class AssuranceCoordinator {
|
|
|
700
714
|
|
|
701
715
|
async submitReview(taskId: string, ctx: HostContext, verdictInput: unknown): Promise<AssuranceSubmitReviewResult> {
|
|
702
716
|
const unknown = this.unknownOperations.get(taskId);
|
|
703
|
-
if (unknown)
|
|
717
|
+
if (unknown) return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason };
|
|
704
718
|
const rejected = this.rejectedReviewOperations.get(taskId);
|
|
705
719
|
if (rejected) return { state: "blocked", reason: rejected.reason };
|
|
706
720
|
const reservation = this.reviewReservations.get(taskId);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { snapshotDigest, type SnapshotDescriptor, type AssuranceVerdict } from "./coordinator";
|
|
2
|
+
import { runFixedVerification, VerificationAbortedError, type FrozenRunner, type VerificationDescriptor } from "./verification";
|
|
3
|
+
import { qaFindingId } from "./qa_findings";
|
|
4
|
+
|
|
5
|
+
export interface QaVerificationProgressInput {
|
|
6
|
+
index: number;
|
|
7
|
+
total: number;
|
|
8
|
+
acceptance_id: string;
|
|
9
|
+
phase: "running" | "passed" | "failed";
|
|
10
|
+
elapsed_ms: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function runDeterministicQa(
|
|
14
|
+
snapshot: SnapshotDescriptor,
|
|
15
|
+
descriptors: Map<string, VerificationDescriptor>,
|
|
16
|
+
runner: FrozenRunner,
|
|
17
|
+
options: {
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
onProgress?: (progress: QaVerificationProgressInput) => void;
|
|
20
|
+
runVerification?: typeof runFixedVerification;
|
|
21
|
+
} = {},
|
|
22
|
+
): Promise<AssuranceVerdict> {
|
|
23
|
+
if (snapshot.role !== "qa") throw new Error("deterministic QA requires qa role");
|
|
24
|
+
if (options.signal?.aborted) throw new VerificationAbortedError();
|
|
25
|
+
const findings: NonNullable<AssuranceVerdict["findings"]> = [];
|
|
26
|
+
const runVerification = options.runVerification ?? runFixedVerification;
|
|
27
|
+
for (const [offset, item] of snapshot.acceptance.entries()) {
|
|
28
|
+
if (options.signal?.aborted) throw new VerificationAbortedError();
|
|
29
|
+
const descriptor = descriptors.get(item.id);
|
|
30
|
+
if (!descriptor) throw new Error(`verification descriptor missing for ${item.id}`);
|
|
31
|
+
const startedAt = Date.now();
|
|
32
|
+
options.onProgress?.({
|
|
33
|
+
index: offset + 1,
|
|
34
|
+
total: snapshot.acceptance.length,
|
|
35
|
+
acceptance_id: item.id,
|
|
36
|
+
phase: "running",
|
|
37
|
+
elapsed_ms: 0,
|
|
38
|
+
});
|
|
39
|
+
const result = await runVerification(snapshot.root, descriptor, runner, {
|
|
40
|
+
signal: options.signal,
|
|
41
|
+
});
|
|
42
|
+
if (options.signal?.aborted) throw new VerificationAbortedError();
|
|
43
|
+
const failed = result.exit_code !== 0 || result.timed_out;
|
|
44
|
+
options.onProgress?.({
|
|
45
|
+
index: offset + 1,
|
|
46
|
+
total: snapshot.acceptance.length,
|
|
47
|
+
acceptance_id: item.id,
|
|
48
|
+
phase: failed ? "failed" : "passed",
|
|
49
|
+
elapsed_ms: Date.now() - startedAt,
|
|
50
|
+
});
|
|
51
|
+
if (failed) {
|
|
52
|
+
// Findings become durable authority records; never include verifier output.
|
|
53
|
+
findings.push({
|
|
54
|
+
id: qaFindingId(item.id, snapshotDigest(snapshot)),
|
|
55
|
+
kind: "blocking",
|
|
56
|
+
acceptance_id: item.id,
|
|
57
|
+
summary: `verification failed (exit ${result.exit_code}${result.timed_out ? ", timed out" : ""}) stdout=${Buffer.byteLength(result.stdout)}B stderr=${Buffer.byteLength(result.stderr)}B`,
|
|
58
|
+
findings_digest: "",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (findings.length > 0) {
|
|
63
|
+
return {
|
|
64
|
+
contract: "assurance_kernel/assurance_verdict/v2",
|
|
65
|
+
role: "qa",
|
|
66
|
+
task_id: snapshot.task_id,
|
|
67
|
+
snapshot_digest: snapshotDigest(snapshot),
|
|
68
|
+
decision: "rework",
|
|
69
|
+
findings,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
contract: "assurance_kernel/assurance_verdict/v2",
|
|
74
|
+
role: "qa",
|
|
75
|
+
task_id: snapshot.task_id,
|
|
76
|
+
snapshot_digest: snapshotDigest(snapshot),
|
|
77
|
+
decision: "pass",
|
|
78
|
+
approval: {
|
|
79
|
+
kind: "qa",
|
|
80
|
+
authority_role: "qa",
|
|
81
|
+
summary: `all ${snapshot.acceptance.length} fixed verification descriptor(s) passed`,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -4,7 +4,6 @@ import { execFileSync } from "node:child_process";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import {
|
|
6
6
|
AssuranceCoordinator,
|
|
7
|
-
snapshotDigest,
|
|
8
7
|
type AssuranceCoordinatorPorts,
|
|
9
8
|
type AssuranceSubmitReviewResult,
|
|
10
9
|
type AssuranceVerdict,
|
|
@@ -15,8 +14,6 @@ import {
|
|
|
15
14
|
assertRunnerCompatible,
|
|
16
15
|
findingsDigest,
|
|
17
16
|
resolveBunRunner,
|
|
18
|
-
runFixedVerification,
|
|
19
|
-
VerificationAbortedError,
|
|
20
17
|
type FrozenRunner,
|
|
21
18
|
type VerificationDescriptor,
|
|
22
19
|
} from "../assurance/verification";
|
|
@@ -45,7 +42,7 @@ import {
|
|
|
45
42
|
import { enrollCanaryTask, runEnrollmentRehearsal } from "../kernel/enrollment";
|
|
46
43
|
import { reconcileKernelAuthority, repairKernelAuthority } from "../kernel/storage";
|
|
47
44
|
import { preparePiCanary, revalidatePiCanary } from "../kernel/pi_canary_prepare";
|
|
48
|
-
import {
|
|
45
|
+
import { runDeterministicQa } from "../assurance/qa";
|
|
49
46
|
import { taskDiffIdentity, taskRevisionIdentity } from "../workspace_scope";
|
|
50
47
|
import {
|
|
51
48
|
confirmationRef,
|
|
@@ -129,47 +126,6 @@ function assertProjectionBinding(before: AssuranceProjectionResult, after: Assur
|
|
|
129
126
|
}
|
|
130
127
|
}
|
|
131
128
|
|
|
132
|
-
export async function runDeterministicQa(
|
|
133
|
-
snapshot: SnapshotDescriptor,
|
|
134
|
-
descriptors: Map<string, VerificationDescriptor>,
|
|
135
|
-
runner: FrozenRunner,
|
|
136
|
-
options: { signal?: AbortSignal; onProgress?: (progress: { index: number; total: number; acceptance_id: string; phase: "running" | "passed" | "failed"; elapsed_ms: number }) => void } = {},
|
|
137
|
-
): Promise<AssuranceVerdict> {
|
|
138
|
-
if (snapshot.role !== "qa") throw new Error("deterministic QA requires qa role");
|
|
139
|
-
if (options.signal?.aborted) throw new VerificationAbortedError();
|
|
140
|
-
const findings: NonNullable<AssuranceVerdict["findings"]> = [];
|
|
141
|
-
for (const [offset, item] of snapshot.acceptance.entries()) {
|
|
142
|
-
if (options.signal?.aborted) throw new VerificationAbortedError();
|
|
143
|
-
const descriptor = descriptors.get(item.id);
|
|
144
|
-
if (!descriptor) throw new Error(`verification descriptor missing for ${item.id}`);
|
|
145
|
-
const startedAt = Date.now();
|
|
146
|
-
options.onProgress?.({ index: offset + 1, total: snapshot.acceptance.length, acceptance_id: item.id, phase: "running", elapsed_ms: 0 });
|
|
147
|
-
const result = await runFixedVerification(snapshot.root, descriptor, runner, { signal: options.signal });
|
|
148
|
-
const failed = result.exit_code !== 0 || result.timed_out;
|
|
149
|
-
options.onProgress?.({ index: offset + 1, total: snapshot.acceptance.length, acceptance_id: item.id, phase: failed ? "failed" : "passed", elapsed_ms: Date.now() - startedAt });
|
|
150
|
-
if (failed) {
|
|
151
|
-
findings.push({
|
|
152
|
-
id: qaFindingId(item.id, snapshotDigest(snapshot)),
|
|
153
|
-
kind: "blocking",
|
|
154
|
-
acceptance_id: item.id,
|
|
155
|
-
summary: `verification failed (exit ${result.exit_code}${result.timed_out ? ", timed out" : ""})`,
|
|
156
|
-
findings_digest: "",
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
if (findings.length > 0) {
|
|
161
|
-
return { contract: "assurance_kernel/assurance_verdict/v2", role: "qa", task_id: snapshot.task_id, snapshot_digest: snapshotDigest(snapshot), decision: "rework", findings };
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
contract: "assurance_kernel/assurance_verdict/v2",
|
|
165
|
-
role: "qa",
|
|
166
|
-
task_id: snapshot.task_id,
|
|
167
|
-
snapshot_digest: snapshotDigest(snapshot),
|
|
168
|
-
decision: "pass",
|
|
169
|
-
approval: { kind: "qa", authority_role: "qa", summary: `all ${snapshot.acceptance.length} fixed verification descriptor(s) passed` },
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
129
|
function qaOutcomes(record: { attestations: Array<{ kind: string; acceptance_results: Array<{ acceptance_id: string; status: "passed" | "failed" | "blocked"; summary: string }> }> }) {
|
|
174
130
|
return Object.fromEntries(
|
|
175
131
|
record.attestations.filter((item) => item.kind === "qa").flatMap((item) => item.acceptance_results)
|
|
@@ -43,6 +43,12 @@ export const INTENT_SIDECAR_RELATIVE_PREFIX = "docs/plans/";
|
|
|
43
43
|
export const RISK_FLOOR_SCOPE_PREFIXES = [
|
|
44
44
|
"plugins/immune-brain/runtime/kernel",
|
|
45
45
|
"plugins/immune-brain/runtime/authority_commit_receipts.ts",
|
|
46
|
+
"plugins/immune-brain/runtime/assurance",
|
|
47
|
+
"plugins/immune-brain/runtime/claude/interaction.ts",
|
|
48
|
+
"plugins/immune-brain/runtime/claude/capability.ts",
|
|
49
|
+
"plugins/immune-brain/runtime/claude/review_host.ts",
|
|
50
|
+
"plugins/immune-brain/runtime/claude/kernel_ports.ts",
|
|
51
|
+
"plugins/immune-brain/runtime/claude/mcp_server.ts",
|
|
46
52
|
"plugins/immune-brain/.pi-extension",
|
|
47
53
|
] as const;
|
|
48
54
|
|
|
@@ -50,8 +56,7 @@ export const RISK_FLOOR_SCOPE_PREFIXES = [
|
|
|
50
56
|
// separate from the glob-aware scope_hint policy above: the two inputs have
|
|
51
57
|
// different contracts and docs-only scope remains routine today.
|
|
52
58
|
export const CHANGED_PATH_RISK_FLOOR_PREFIXES = [
|
|
53
|
-
|
|
54
|
-
"plugins/immune-brain/.pi-extension",
|
|
59
|
+
...RISK_FLOOR_SCOPE_PREFIXES,
|
|
55
60
|
"docs/specs",
|
|
56
61
|
"docs/plans",
|
|
57
62
|
] as const;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/plugin_versioning.ts from the root package.json.
|
|
2
|
-
export const PLUGIN_VERSION = "3.
|
|
2
|
+
export const PLUGIN_VERSION = "3.6.0" as const;
|