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.
@@ -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 = await this.ports.projectTask(ctx.cwd, taskId);
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") return { state: "completed" };
497
- if (projection.projection.lifecycle === "stopped") return { state: "stopped" };
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) { this.unknownOperations.delete(taskId); return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason }; }
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 { qaFindingId } from "../assurance/qa_findings";
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
- "plugins/immune-brain/runtime/kernel",
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.5.0" as const;
2
+ export const PLUGIN_VERSION = "3.6.0" as const;