immune-brain 3.6.9 → 4.0.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.
Files changed (76) hide show
  1. package/package.json +3 -2
  2. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  3. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +18 -2
  4. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +76 -121
  5. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +106 -600
  6. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +1 -0
  7. package/plugins/immune-brain/.pi-extension/pi-canary-verification.ts +3 -3
  8. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +17 -43
  9. package/plugins/immune-brain/dist/claude/mcp-server.mjs +7587 -5049
  10. package/plugins/immune-brain/dist/docs/reference/planning-artifact-retention.md +11 -12
  11. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +1 -1
  12. package/plugins/immune-brain/dist/imm-loop.md +33 -25
  13. package/plugins/immune-brain/dist/imm-planner.md +41 -27
  14. package/plugins/immune-brain/dist/imm-review-retro.md +2 -2
  15. package/plugins/immune-brain/dist/role-prompts/code-review.md +11 -1
  16. package/plugins/immune-brain/dist/role-prompts/executor.md +4 -4
  17. package/plugins/immune-brain/runtime/assurance/coordinator.ts +183 -40
  18. package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +240 -0
  19. package/plugins/immune-brain/runtime/assurance/qa.ts +132 -58
  20. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +15 -7
  21. package/plugins/immune-brain/runtime/assurance/verification.ts +246 -206
  22. package/plugins/immune-brain/runtime/authorization_operation.ts +20 -0
  23. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +288 -721
  24. package/plugins/immune-brain/runtime/commands/kernel.ts +158 -67
  25. package/plugins/immune-brain/runtime/github_issue_tracker.ts +1 -1
  26. package/plugins/immune-brain/runtime/kernel/actor_identity.ts +33 -0
  27. package/plugins/immune-brain/runtime/kernel/application.ts +22 -6
  28. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +94 -5
  29. package/plugins/immune-brain/runtime/kernel/authority_port.ts +27 -6
  30. package/plugins/immune-brain/runtime/kernel/backend_claim.ts +43 -16
  31. package/plugins/immune-brain/runtime/kernel/batch_authority.ts +10 -6
  32. package/plugins/immune-brain/runtime/kernel/canary_application.ts +50 -63
  33. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +13 -4
  34. package/plugins/immune-brain/runtime/kernel/completion.ts +5 -14
  35. package/plugins/immune-brain/runtime/kernel/enrollment.ts +124 -34
  36. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +13 -5
  37. package/plugins/immune-brain/runtime/kernel/index.ts +3 -1
  38. package/plugins/immune-brain/runtime/kernel/intent.ts +7 -11
  39. package/plugins/immune-brain/runtime/kernel/legacy_audit.ts +4 -1
  40. package/plugins/immune-brain/runtime/kernel/legacy_task_record.ts +323 -0
  41. package/plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts +10 -1
  42. package/plugins/immune-brain/runtime/kernel/reducer.ts +32 -31
  43. package/plugins/immune-brain/runtime/kernel/run_identity.ts +121 -0
  44. package/plugins/immune-brain/runtime/kernel/spec_binding.ts +100 -0
  45. package/plugins/immune-brain/runtime/kernel/sqlite_migration.ts +950 -0
  46. package/plugins/immune-brain/runtime/kernel/sqlite_store.ts +1193 -0
  47. package/plugins/immune-brain/runtime/kernel/storage.ts +1254 -1206
  48. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +129 -755
  49. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +419 -46
  50. package/plugins/immune-brain/runtime/kernel/types.ts +12 -43
  51. package/plugins/immune-brain/runtime/kernel/validation.ts +60 -274
  52. package/plugins/immune-brain/runtime/managed_task_routing_policy.ts +0 -1
  53. package/plugins/immune-brain/runtime/plan_core.ts +27 -65
  54. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  55. package/plugins/immune-brain/runtime/prompts/code-review.md +11 -1
  56. package/plugins/immune-brain/runtime/prompts/executor.md +4 -4
  57. package/plugins/immune-brain/runtime/staged_intent.ts +58 -0
  58. package/plugins/immune-brain/runtime/unattended/batch_git.ts +37 -7
  59. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +42 -2
  60. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +771 -0
  61. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +189 -0
  62. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +35 -0
  63. package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +33 -0
  64. package/plugins/immune-brain/runtime/unattended/types.ts +14 -1
  65. package/plugins/immune-brain/runtime/v4_runtime.ts +19 -23
  66. package/plugins/immune-brain/runtime/verification_descriptor.ts +92 -136
  67. package/plugins/immune-brain/runtime/workspace_scope.ts +98 -13
  68. package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -3
  69. package/plugins/immune-brain/bin/imm-retire-stale-wrapper +0 -4
  70. package/plugins/immune-brain/bin/imm-retired +0 -4
  71. package/plugins/immune-brain/runtime/authority_commit_receipts.ts +0 -716
  72. package/plugins/immune-brain/runtime/kernel/automatic_observations.ts +0 -451
  73. package/plugins/immune-brain/runtime/kernel/legacy.ts +0 -299
  74. package/plugins/immune-brain/runtime/kernel/observation.ts +0 -397
  75. package/plugins/immune-brain/runtime/kernel/readiness.ts +0 -282
  76. package/plugins/immune-brain/runtime/kernel/readiness_evidence.ts +0 -132
@@ -1,6 +1,13 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { snapshotDigest, type SnapshotDescriptor, type AssuranceVerdict } from "./coordinator";
2
- import { runFixedVerification, VerificationAbortedError, type FrozenRunner, type VerificationDescriptor } from "./verification";
3
+ import { assertDeliveryClean, materializeDeliveryWorkspace, removeTaskOwnedTree, writeDeliveryTree } from "./delivery_workspace";
4
+ import { runFixedVerification, resolveVerificationCommand, assertCommandIdentity, verificationPath, VerificationAbortedError, VerificationCleanupError, VerificationLaunchError, type FrozenCommand, type VerificationDescriptor, type VerificationCommand } from "./verification";
5
+ import { mkdtempSync, realpathSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { join, sep } from "node:path";
3
8
  import { qaFindingId } from "./qa_findings";
9
+ import { captureGitTaskRevisionSnapshot } from "../workspace_scope";
10
+ import { readTaskRecord } from "../kernel/storage";
4
11
 
5
12
  export interface QaVerificationProgressInput {
6
13
  index: number;
@@ -10,76 +17,143 @@ export interface QaVerificationProgressInput {
10
17
  elapsed_ms: number;
11
18
  }
12
19
 
20
+ function deliveryTreeForSnapshot(
21
+ snapshot: SnapshotDescriptor,
22
+ writeTree: typeof writeDeliveryTree,
23
+ ): string {
24
+ if (snapshot.review_revision?.review_tree) return snapshot.review_revision.review_tree;
25
+ const record = readTaskRecord(snapshot.root, snapshot.task_id).record;
26
+ if (!record || record.contract !== "assurance_kernel/task_record/v4" || !record.git_base_head)
27
+ throw new Error("QA delivery requires a TaskRecord v4 git_base_head");
28
+ const captured = captureGitTaskRevisionSnapshot(
29
+ snapshot.root,
30
+ record.intent_snapshot.scope_hint,
31
+ record.git_base_head,
32
+ snapshot.task_id,
33
+ );
34
+ const digest = `sha256:${createHash("sha256").update(JSON.stringify(captured)).digest("hex")}`;
35
+ if (digest !== snapshot.diff_hash)
36
+ throw new Error("QA delivery identity does not match the frozen snapshot");
37
+ return writeDeliveryTree(snapshot.root, captured);
38
+ }
39
+
40
+ export class QaPreparationError extends Error {
41
+ constructor(public readonly stage: "resolution" | "prepare" | "integrity", public readonly acceptance_ids: string[], public readonly reason: string) {
42
+ super(`QA ${stage} failed (${reason}); affected checks=${acceptance_ids.join(",")}`);
43
+ this.name = "QaPreparationError";
44
+ }
45
+ }
46
+
13
47
  export async function runDeterministicQa(
14
48
  snapshot: SnapshotDescriptor,
15
49
  descriptors: Map<string, VerificationDescriptor>,
16
- runner: FrozenRunner,
17
50
  options: {
18
51
  signal?: AbortSignal;
19
52
  onProgress?: (progress: QaVerificationProgressInput) => void;
20
- runVerification?: typeof runFixedVerification;
53
+ // Coordination tests inject a fake executor without creating a real delivery.
54
+ _runFixedVerification?: typeof runFixedVerification;
55
+ _resolveVerificationCommand?: typeof resolveVerificationCommand;
56
+ _materializeDeliveryWorkspace?: typeof materializeDeliveryWorkspace;
57
+ _writeDeliveryTree?: typeof writeDeliveryTree;
21
58
  } = {},
22
59
  ): Promise<AssuranceVerdict> {
23
60
  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();
61
+ const live = () => { if (options.signal?.aborted) throw new VerificationAbortedError(); };
62
+ live();
63
+ const groups = new Map<string, Array<{ id: string; index: number; descriptor: VerificationDescriptor }>>();
64
+ for (const [index, item] of snapshot.acceptance.entries()) {
29
65
  const descriptor = descriptors.get(item.id);
30
66
  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
- }
67
+ const key = JSON.stringify(descriptor.environment);
68
+ const group = groups.get(key) ?? [];
69
+ group.push({ id: item.id, index, descriptor });
70
+ groups.set(key, group);
61
71
  }
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,
72
+ const tree = snapshot.review_revision?.review_tree
73
+ ?? (options._writeDeliveryTree ? "tree" : deliveryTreeForSnapshot(snapshot, writeDeliveryTree));
74
+ const path = verificationPath();
75
+ const findings: NonNullable<AssuranceVerdict["findings"]> = [];
76
+ const evidence: unknown[] = [];
77
+ const allCommands: FrozenCommand[] = [];
78
+ for (const [environmentKey, group] of groups) {
79
+ live();
80
+ const ids = group.map(item => item.id);
81
+ const environment = group[0]!.descriptor.environment;
82
+ const delivery = (options._materializeDeliveryWorkspace ?? materializeDeliveryWorkspace)(snapshot.root, tree);
83
+ const home = mkdtempSync(join(tmpdir(), "imm-qa-home-"));
84
+ const clean = () => {
85
+ if (options._materializeDeliveryWorkspace) return;
86
+ try { assertDeliveryClean(delivery.root, tree, delivery.seal, environment.writable_paths); }
87
+ catch { throw new QaPreparationError("integrity", ids, "protected_input_or_output_drift"); }
70
88
  };
89
+ const groupCommands: FrozenCommand[] = [];
90
+ const deliveryPrefix = `${realpathSync(delivery.root)}${sep}`;
91
+ const execute = async (command: VerificationCommand, stage: "prepare" | "check") => {
92
+ live(); clean();
93
+ let frozen: FrozenCommand;
94
+ try { frozen = (options._resolveVerificationCommand ?? resolveVerificationCommand)(delivery.root, command, path); }
95
+ catch { throw new QaPreparationError("resolution", ids, "executable_or_cwd_unavailable"); }
96
+ let result;
97
+ try { result = await (options._runFixedVerification ?? runFixedVerification)(delivery.root, command, frozen, { signal: options.signal, home, path }); }
98
+ catch (error) {
99
+ if (error instanceof VerificationLaunchError) throw new QaPreparationError("resolution", ids, "process_launch_failed");
100
+ if (error instanceof VerificationCleanupError) throw new QaPreparationError("resolution", ids, "process_cleanup_failed");
101
+ throw error;
102
+ }
103
+ live(); clean();
104
+ groupCommands.push(frozen);
105
+ if (frozen?.entry) {
106
+ if (!command.executable.startsWith("./")) allCommands.push(frozen);
107
+ else if (frozen.interpreter && !frozen.interpreter.path.startsWith(deliveryPrefix))
108
+ allCommands.push({ entry: frozen.interpreter, interpreter: null, interpreter_args: [] });
109
+ }
110
+ // Paths and process output are not persisted. Identity hashes describe
111
+ // observed entries, not a claim about every transitive dependency.
112
+ evidence.push({ stage, environment: createHash("sha256").update(environmentKey).digest("hex"),
113
+ command: createHash("sha256").update(JSON.stringify(command)).digest("hex"),
114
+ entry: frozen?.entry?.content_hash ?? null, interpreter: frozen?.interpreter?.content_hash ?? null,
115
+ exit_code: result.exit_code, timed_out: result.timed_out, output_limited: result.output_limited });
116
+ if (Buffer.byteLength(JSON.stringify(evidence)) > 16_384)
117
+ throw new QaPreparationError("prepare", ids, "execution_metadata_limit_exceeded");
118
+ return result;
119
+ };
120
+ try {
121
+ clean();
122
+ if (environment.prepare) {
123
+ const result = await execute(environment.prepare, "prepare");
124
+ if (result.exit_code !== 0 || result.timed_out || result.output_limited)
125
+ throw new QaPreparationError("prepare", ids, result.timed_out ? "timeout" : result.output_limited ? "output_limit" : "nonzero_exit");
126
+ }
127
+ for (const item of group) {
128
+ const started = Date.now();
129
+ const progress = (phase: QaVerificationProgressInput["phase"]) => options.onProgress?.({
130
+ index: item.index + 1, total: snapshot.acceptance.length, acceptance_id: item.id, phase, elapsed_ms: Date.now() - started,
131
+ });
132
+ progress("running");
133
+ const result = await execute(item.descriptor.command, "check");
134
+ const failed = result.exit_code !== 0 || result.timed_out || result.output_limited;
135
+ progress(failed ? "failed" : "passed");
136
+ if (failed) findings.push({ id: qaFindingId(item.id, snapshotDigest(snapshot)), kind: "blocking", acceptance_id: item.id,
137
+ summary: `verification failed (exit ${result.exit_code}${result.timed_out ? ", timed out" : ""}${result.output_limited ? ", output limit" : ""}) stdout=${Buffer.byteLength(result.stdout)}B stderr=${Buffer.byteLength(result.stderr)}B`, findings_digest: "" });
138
+ }
139
+ // Recheck while project entries still exist; cleanup removes them.
140
+ for (const frozen of groupCommands) {
141
+ if (frozen?.entry) assertCommandIdentity(frozen);
142
+ }
143
+ } finally { try { delivery.cleanup(); } finally { removeTaskOwnedTree(home); } }
144
+ }
145
+ live();
146
+ // Host entries survive materialization cleanup and must still match before approval.
147
+ for (const frozen of allCommands) {
148
+ if (frozen?.entry) assertCommandIdentity(frozen);
71
149
  }
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
- };
150
+ const base = { contract: "assurance_kernel/assurance_verdict/v2" as const, role: "qa" as const,
151
+ task_id: snapshot.task_id, snapshot_digest: snapshotDigest(snapshot) };
152
+ if (findings.length) return { ...base, decision: "rework", findings };
153
+ const executionDigest = createHash("sha256").update(JSON.stringify({
154
+ tree, platform: process.platform, arch: process.arch, observations: evidence,
155
+ })).digest("hex");
156
+ return { ...base, decision: "pass", approval: { kind: "qa", authority_role: "qa",
157
+ summary: `all ${snapshot.acceptance.length} project verification check(s) passed; execution=sha256:${executionDigest}` } };
84
158
  }
85
159
 
@@ -21,6 +21,7 @@ import {
21
21
  type GitTaskIndexEntry,
22
22
  type GitTaskRevisionSnapshot,
23
23
  } from "../workspace_scope";
24
+ import { currentRunId } from "../kernel/storage";
24
25
 
25
26
  const MAX_REVIEW_BUNDLE_BYTES = 2 * 1024 * 1024;
26
27
 
@@ -346,10 +347,16 @@ function taskIdFromReviewRefSegment(segment: string): string {
346
347
  return taskId;
347
348
  }
348
349
 
349
- function reviewRef(taskId: string, reviewCommit: string): string {
350
+ function workspaceRefSegment(root: string): string {
351
+ return createHash("sha256").update(realpathSync(root)).digest("hex").slice(0, 16);
352
+ }
353
+
354
+ function reviewRef(root: string, taskId: string, reviewCommit: string): string {
350
355
  const taskSegment = reviewRefTaskSegment(taskId);
351
356
  if (!GIT_COMMIT_ID.test(reviewCommit)) throw new Error("review commit has invalid identity");
352
- return `${REVIEW_REF_NAMESPACE}/${taskSegment}/${reviewCommit}`;
357
+ const runId = currentRunId(root, taskId) ?? "none";
358
+ if (!REVIEW_TASK_ID.test(runId) && runId !== "none") throw new Error("review run id has invalid identity");
359
+ return `${REVIEW_REF_NAMESPACE}/${workspaceRefSegment(root)}/${runId}/${taskSegment}/${reviewCommit}`;
353
360
  }
354
361
 
355
362
  /** The scoped delta of one captured revision, as base->tree paths. */
@@ -406,7 +413,7 @@ export function publishReviewRevision(
406
413
  // Determinism proof: the synthetic commit's tree-to-tree delta against the
407
414
  // Enrollment base must be exactly the captured scoped delta, nothing more.
408
415
  if (!GIT_COMMIT_ID.test(reviewCommit)) throw new Error("review synthetic commit write failed");
409
- const ref = reviewRef(taskId, reviewCommit);
416
+ const ref = reviewRef(root, taskId, reviewCommit);
410
417
  const expected = revisionDelta(snapshot);
411
418
  const actual = decodeNullPaths(
412
419
  gitEvidenceBytes(root, ["diff", "--no-renames", "--name-only", "-z", snapshot.base_head, reviewCommit]),
@@ -452,7 +459,7 @@ function publishInput(
452
459
  throw new Error("review requires a TaskRecord v4 git_base_head");
453
460
  if (!REVISION_DIFF_HASH.test(input.expectedDiffHash))
454
461
  throw new Error("review task revision hash has invalid identity");
455
- const snapshot = captureGitTaskRevisionSnapshot(root, input.scopeHint, input.baseHead);
462
+ const snapshot = captureGitTaskRevisionSnapshot(root, input.scopeHint, input.baseHead, input.taskId);
456
463
  const recomputed = `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
457
464
  if (recomputed !== input.expectedDiffHash)
458
465
  throw new Error("review task revision does not match assurance snapshot");
@@ -505,14 +512,15 @@ export function captureReviewManifest(
505
512
  }
506
513
 
507
514
  export function listReviewRefs(root: string): Array<{ ref: string; commit: string; taskId: string }> {
508
- const output = gitEvidence(root, ["for-each-ref", "--format=%(refname) %(objectname)", `${REVIEW_REF_NAMESPACE}/`]);
515
+ const output = gitEvidence(root, ["for-each-ref", "--format=%(refname) %(objectname)", `${REVIEW_REF_NAMESPACE}/${workspaceRefSegment(root)}/`]);
509
516
  const refs: Array<{ ref: string; commit: string; taskId: string }> = [];
510
517
  for (const line of output.split("\n").filter(Boolean)) {
511
518
  const [ref, commit] = line.split(" ");
512
- const segment = ref?.slice(REVIEW_REF_NAMESPACE.length + 1).split("/")[0] ?? "";
519
+ const parts = ref?.slice(REVIEW_REF_NAMESPACE.length + 1).split("/") ?? [];
520
+ const segment = parts.length >= 4 ? parts[parts.length - 2] : parts[0] ?? "";
513
521
  let taskId: string;
514
522
  try {
515
- taskId = taskIdFromReviewRefSegment(segment);
523
+ taskId = taskIdFromReviewRefSegment(segment ?? "");
516
524
  } catch {
517
525
  continue;
518
526
  }