immune-brain 5.3.0 → 5.5.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 (75) hide show
  1. package/README.md +4 -4
  2. package/README.zh-CN.md +4 -4
  3. package/package.json +2 -1
  4. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +54 -70
  6. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +163 -28
  7. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +71 -10
  8. package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +1 -1
  9. package/plugins/immune-brain/.pi-extension/pi-run-continuation.ts +161 -0
  10. package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -2
  11. package/plugins/immune-brain/dist/BASELINE.md +13 -1
  12. package/plugins/immune-brain/dist/claude/mcp-server.mjs +16130 -12734
  13. package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +3 -1
  14. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +41 -1
  15. package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
  16. package/plugins/immune-brain/dist/imm-planner.md +57 -16
  17. package/plugins/immune-brain/dist/imm-run-batch.md +398 -0
  18. package/plugins/immune-brain/dist/imm-run.md +89 -344
  19. package/plugins/immune-brain/dist/role-prompts/code-review.md +26 -2
  20. package/plugins/immune-brain/dist/role-prompts/executor.md +35 -10
  21. package/plugins/immune-brain/hooks/hooks.json +21 -1
  22. package/plugins/immune-brain/runtime/assurance/coordinator.ts +203 -13
  23. package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +2 -1
  24. package/plugins/immune-brain/runtime/assurance/enrollment_git_base.ts +99 -14
  25. package/plugins/immune-brain/runtime/assurance/phase_entry.ts +1 -7
  26. package/plugins/immune-brain/runtime/assurance/qa.ts +126 -43
  27. package/plugins/immune-brain/runtime/assurance/qa_findings.ts +8 -0
  28. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +48 -0
  29. package/plugins/immune-brain/runtime/assurance/run_continuation.ts +284 -0
  30. package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +46 -4
  31. package/plugins/immune-brain/runtime/assurance/verification.ts +83 -35
  32. package/plugins/immune-brain/runtime/assurance/workflow_autocommit.ts +222 -0
  33. package/plugins/immune-brain/runtime/claude/interaction.ts +11 -2
  34. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +199 -48
  35. package/plugins/immune-brain/runtime/claude/lane_guard.ts +8 -15
  36. package/plugins/immune-brain/runtime/claude/mcp_server.ts +105 -9
  37. package/plugins/immune-brain/runtime/claude/run_continuation.ts +274 -0
  38. package/plugins/immune-brain/runtime/commands/kernel.ts +41 -103
  39. package/plugins/immune-brain/runtime/github_issue_tracker.ts +60 -2
  40. package/plugins/immune-brain/runtime/kernel/application.ts +4 -1
  41. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +103 -20
  42. package/plugins/immune-brain/runtime/kernel/canary_application.ts +57 -7
  43. package/plugins/immune-brain/runtime/kernel/completion.ts +28 -4
  44. package/plugins/immune-brain/runtime/kernel/enrollment.ts +48 -8
  45. package/plugins/immune-brain/runtime/kernel/intent.ts +169 -104
  46. package/plugins/immune-brain/runtime/kernel/reducer.ts +136 -6
  47. package/plugins/immune-brain/runtime/kernel/risk_floor.ts +286 -0
  48. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +83 -1
  49. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +1 -1
  50. package/plugins/immune-brain/runtime/kernel/types.ts +49 -3
  51. package/plugins/immune-brain/runtime/kernel/validation.ts +147 -7
  52. package/plugins/immune-brain/runtime/loop_contract.ts +5 -0
  53. package/plugins/immune-brain/runtime/plan_core.ts +15 -8
  54. package/plugins/immune-brain/runtime/plugin_source_skew.ts +58 -0
  55. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  56. package/plugins/immune-brain/runtime/prompts/code-review.md +26 -2
  57. package/plugins/immune-brain/runtime/prompts/executor.md +35 -10
  58. package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +5 -3
  59. package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +40 -10
  60. package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +57 -3
  61. package/plugins/immune-brain/runtime/unattended/batch_git.ts +424 -66
  62. package/plugins/immune-brain/runtime/unattended/batch_integration.ts +2 -1
  63. package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +13 -0
  64. package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +43 -24
  65. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +6 -2
  66. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +102 -41
  67. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +9 -3
  68. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +9 -9
  69. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +90 -32
  70. package/plugins/immune-brain/runtime/unattended/batch_state.ts +102 -4
  71. package/plugins/immune-brain/runtime/unattended/parent_delivery_consolidation.ts +248 -0
  72. package/plugins/immune-brain/runtime/workspace_scope.ts +126 -18
  73. package/plugins/immune-brain/skills/BASELINE.md +13 -1
  74. package/plugins/immune-brain/skills/imm-planner/SKILL.md +5 -0
  75. package/plugins/immune-brain/skills/imm-run/SKILL.md +12 -8
@@ -20,10 +20,10 @@
20
20
  // open. No Batch Authorization capability is minted, because retiring a record
21
21
  // consumes no child and enrolls nothing — the capability exists to authorize
22
22
  // child consumption, and minting one here would be ceremony without a property.
23
- import { readSecureProjectBytes } from "../kernel/storage";
23
+ import { readSecureProjectBytes, readSettledRunEvidence } from "../kernel/storage";
24
24
  import { batchStatePath } from "../kernel/storage_paths";
25
25
  import { batchReason, type BatchReason } from "./batch_reasons";
26
- import { findExistingActiveBatch, findSettledBatchRecord, type BatchGateDecision } from "./batch_preflight";
26
+ import { findExistingActiveBatch, findSettledBatchRecord, readActiveClaimTaskId, type BatchGateDecision } from "./batch_preflight";
27
27
  import {
28
28
  isTerminalBatchState,
29
29
  replaceAnyBatchRunState,
@@ -34,7 +34,12 @@ import {
34
34
  type BatchRunState,
35
35
  } from "./batch_state";
36
36
 
37
- /** Child states whose Kernel run is still live; a live run settles on its own. */
37
+ /**
38
+ * Child states the batch runner has not finished absorbing. Such a child is
39
+ * mid-flight unless the Kernel store proves its run already ended outside the
40
+ * batch (see `kernelLifecycleOf`): the record alone cannot tell a live run from
41
+ * one a manual imm-run settled while the drifted batch could no longer resume.
42
+ */
38
43
  const SERIAL_IN_FLIGHT: ReadonlySet<string> = new Set(["enrolled", "settled"]);
39
44
  const LANE_IN_FLIGHT: ReadonlySet<string> = new Set(["lane_admitted", "enrolled", "settled", "lane_committed"]);
40
45
 
@@ -45,6 +50,8 @@ export interface StaleBatchChildEvidence {
45
50
  state: string;
46
51
  commit: string | null;
47
52
  lane_branch: string | null;
53
+ /** The Kernel store's terminal lifecycle for this task; null while live or unknown. */
54
+ kernel_lifecycle: "done" | "stopped" | null;
48
55
  }
49
56
 
50
57
  /** The exact record identity the literal user is asked to retire. */
@@ -74,10 +81,27 @@ const RETIRE_REASON =
74
81
  "Recorded child commits, child states and the batch branch are preserved as evidence. " +
75
82
  "Children this batch never executed carry no commit, no batch trailer and no approval; nothing infers their delivery from this record.";
76
83
 
84
+ const RETIRE_REASON_ENDED_OUTSIDE =
85
+ " A child this record still holds mid-flight had already ended in the Kernel store when the record was retired; its own TaskRecord, not this record, is the evidence of that run.";
86
+
77
87
  const RETIRE_NEXT_ACTION =
78
88
  "The Initiative has no active batch. Run each remaining child with imm-run under its own authorization, or start a new batch for this Initiative after checking out its batch branch.";
79
89
 
80
- function childEvidenceOf(record: AnyBatchRunStateRecord): StaleBatchChildEvidence[] {
90
+ /**
91
+ * The terminal lifecycle the coordinator's Kernel store holds for a task, or
92
+ * null. Fail closed: no run, a live run, a task that still owns the workspace
93
+ * claim, or an unreadable store all read as null, so the child stays mid-flight.
94
+ */
95
+ function kernelLifecycleOf(root: string, taskId: string): "done" | "stopped" | null {
96
+ try {
97
+ if (readActiveClaimTaskId(root) === taskId) return null;
98
+ return readSettledRunEvidence(root, taskId)?.lifecycle ?? null;
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ function childEvidenceOf(root: string, record: AnyBatchRunStateRecord): StaleBatchChildEvidence[] {
81
105
  if (record.contract === "assurance_kernel/batch_run_state/v2")
82
106
  return record.children.map((child) => ({
83
107
  task_id: child.task_id,
@@ -85,6 +109,7 @@ function childEvidenceOf(record: AnyBatchRunStateRecord): StaleBatchChildEvidenc
85
109
  state: child.state,
86
110
  commit: child.commit,
87
111
  lane_branch: child.lane?.branch ?? null,
112
+ kernel_lifecycle: kernelLifecycleOf(root, child.task_id),
88
113
  }));
89
114
  return record.children.map((child) => ({
90
115
  task_id: child.task_id,
@@ -92,12 +117,13 @@ function childEvidenceOf(record: AnyBatchRunStateRecord): StaleBatchChildEvidenc
92
117
  state: child.state,
93
118
  commit: child.commit,
94
119
  lane_branch: null,
120
+ kernel_lifecycle: kernelLifecycleOf(root, child.task_id),
95
121
  }));
96
122
  }
97
123
 
98
- function inFlightChildOf(record: AnyBatchRunStateRecord): { task_id: string } | null {
124
+ function inFlightChildOf(root: string, record: AnyBatchRunStateRecord): { task_id: string } | null {
99
125
  const inFlight = record.contract === "assurance_kernel/batch_run_state/v2" ? LANE_IN_FLIGHT : SERIAL_IN_FLIGHT;
100
- return record.children.find((child) => inFlight.has(child.state)) ?? null;
126
+ return record.children.find((child) => inFlight.has(child.state) && kernelLifecycleOf(root, child.task_id) === null) ?? null;
101
127
  }
102
128
 
103
129
  /**
@@ -114,7 +140,7 @@ export function readStaleBatchLookup(root: string, initiativeSlug: string): Stal
114
140
  if (found.corrupt) return { kind: "corrupt", path: found.path };
115
141
  const record = found.record;
116
142
  if (isTerminalBatchState(record.batch_state)) return { kind: "terminal", batch_id: record.batch_id };
117
- const inFlight = inFlightChildOf(record);
143
+ const inFlight = inFlightChildOf(root, record);
118
144
  if (inFlight) return { kind: "in_flight", batch_id: record.batch_id, task_id: inFlight.task_id };
119
145
  return {
120
146
  kind: "retirable",
@@ -128,7 +154,7 @@ export function readStaleBatchLookup(root: string, initiativeSlug: string): Stal
128
154
  base_head: record.base_head,
129
155
  confirmation_time: record.confirmation_time,
130
156
  recorded_commits: [...record.commits],
131
- children: childEvidenceOf(record),
157
+ children: childEvidenceOf(root, record),
132
158
  },
133
159
  };
134
160
  }
@@ -188,6 +214,10 @@ export async function retireStaleBatch<HostRejection>(options: {
188
214
  // Children, commits and branch are carried over untouched: the disposition
189
215
  // records what the batch did and did not do, and invents neither.
190
216
  const retired: AnyBatchRunStateRecord = { ...record, batch_state: "superseded" };
217
+ const inFlightStates = record.contract === "assurance_kernel/batch_run_state/v2" ? LANE_IN_FLIGHT : SERIAL_IN_FLIGHT;
218
+ const reason = record.children.some((child) => inFlightStates.has(child.state))
219
+ ? RETIRE_REASON + RETIRE_REASON_ENDED_OUTSIDE
220
+ : RETIRE_REASON;
191
221
  const stored = replaceAnyBatchRunState(root, expected, retired, () => {
192
222
  if (!readSecureProjectBytes(root, batchStatePath(record.batch_id)).equals(expected))
193
223
  throw new Error("batch state changed before the disposition was applied");
@@ -202,7 +232,7 @@ export async function retireStaleBatch<HostRejection>(options: {
202
232
  max_parallel: stored.max_parallel,
203
233
  children: stored.children,
204
234
  commits: stored.commits,
205
- reason: RETIRE_REASON,
235
+ reason,
206
236
  handoffs: [],
207
237
  next_action: RETIRE_NEXT_ACTION,
208
238
  created_at: now,
@@ -214,7 +244,7 @@ export async function retireStaleBatch<HostRejection>(options: {
214
244
  batch_state: stored.batch_state,
215
245
  children: stored.children,
216
246
  commits: stored.commits,
217
- reason: RETIRE_REASON,
247
+ reason,
218
248
  next_action: RETIRE_NEXT_ACTION,
219
249
  created_at: now,
220
250
  } satisfies BatchRunReport);
@@ -4,6 +4,8 @@
4
4
  // the completing tick runs exactly what the literal user saw at confirmation.
5
5
  // A failure marks the completion report as not passed; integrated commits are
6
6
  // never rolled back. Only exit status and timing are recorded, never output.
7
+ import { createHash } from "node:crypto";
8
+ import { parseTaskIntentV1 } from "../kernel/intent";
7
9
  import { spawn, spawnSync } from "node:child_process";
8
10
 
9
11
  export const MAX_FINAL_VERIFICATION_COMMANDS = 8;
@@ -38,6 +40,24 @@ export function parseFinalVerification(value: unknown): string[] | undefined {
38
40
  });
39
41
  }
40
42
 
43
+ export function parseFinalVerificationReport(value: unknown, commands?: readonly string[]): FinalVerificationReport {
44
+ if (!value || typeof value !== "object") throw new Error("invalid final verification result");
45
+ const report = value as FinalVerificationReport;
46
+ if (typeof report.passed !== "boolean" || !(report.head === null || typeof report.head === "string") ||
47
+ !Array.isArray(report.results) || !report.results.length || report.results.some(result =>
48
+ !result || typeof result.command !== "string" || typeof result.passed !== "boolean" ||
49
+ !(result.exit_code === null || Number.isInteger(result.exit_code)) ||
50
+ !(result.signal === null || typeof result.signal === "string") ||
51
+ !Number.isFinite(result.duration_ms) || result.duration_ms < 0 ||
52
+ result.passed !== (result.exit_code === 0 && result.signal === null)))
53
+ throw new Error("invalid final verification result");
54
+ parseFinalVerification(report.results.map(result => result.command));
55
+ if (report.passed && report.results.some(result => !result.passed)) throw new Error("contradictory final verification result");
56
+ if (commands && JSON.stringify(report.results.map(result => result.command)) !== JSON.stringify(commands))
57
+ throw new Error("final verification commands changed");
58
+ return report;
59
+ }
60
+
41
61
  /** One command, asynchronously, so a long suite never blocks the Host's event loop. */
42
62
  function runOne(root: string, command: string, timeoutMs: number): Promise<FinalVerificationResult> {
43
63
  const [executable, ...argv] = command.split(" ");
@@ -48,7 +68,7 @@ function runOne(root: string, command: string, timeoutMs: number): Promise<Final
48
68
  if (settled) return;
49
69
  settled = true;
50
70
  clearTimeout(timer);
51
- resolve({ command, exit_code: exitCode, signal, passed: exitCode === 0, duration_ms: Date.now() - started });
71
+ resolve({ command, exit_code: exitCode, signal, passed: exitCode === 0 && signal === null, duration_ms: Date.now() - started });
52
72
  };
53
73
  const child = spawn(executable!, argv, { cwd: root, stdio: "ignore", env: process.env });
54
74
  const timer = setTimeout(() => {
@@ -60,13 +80,47 @@ function runOne(root: string, command: string, timeoutMs: number): Promise<Final
60
80
  });
61
81
  }
62
82
 
83
+ export function prepareFinalRepairCandidate(input: {
84
+ initiative_slug: string; original_goal: string; scope_hint: string[];
85
+ failure: FinalVerificationReport; prior_repairs: number;
86
+ }) {
87
+ const failure = parseFinalVerificationReport(input.failure);
88
+ if (failure.passed || !failure.head) throw new Error("repair candidate requires a failed final verification bound to a delivery HEAD");
89
+ if (!failure.results.some(result => !result.passed)) throw new Error("reconcile the verification environment or delivery HEAD before preparing a code repair");
90
+ if (!Number.isInteger(input.prior_repairs) || input.prior_repairs < 0 || input.prior_repairs >= 2)
91
+ throw new Error("Parent coordinator correction budget exhausted");
92
+ const identity = createHash("sha256").update(JSON.stringify({ head: failure.head,
93
+ results: failure.results.map(({ command, exit_code, signal }) => ({ command, exit_code, signal })) })).digest("hex");
94
+ const taskId = `${input.initiative_slug.slice(0, 96)}-repair-${identity.slice(0, 12)}`;
95
+ const candidate = parseTaskIntentV1({ contract: "assurance_kernel/task_intent/v1", task_id: taskId,
96
+ goal: input.original_goal, owner: "user", revision: 1, risk: "material", scope_hint: [...new Set([...input.scope_hint, `docs/plans/${taskId}.intent.json`])],
97
+ acceptance: failure.results.map((result, index) => {
98
+ const [executable, ...argv] = result.command.split(" ");
99
+ return { id: `final-${index + 1}`, assertion: `Restore the original goal and pass final command ${index + 1}`,
100
+ verification: JSON.stringify({ contract: "assurance_kernel/verification_descriptor/v2",
101
+ command: { executable, argv, cwd: ".", timeout_ms: 120000, max_output_bytes: 131072 },
102
+ environment: { prepare: null, writable_paths: [] } }, null, 2) + "\n" };
103
+ }) });
104
+ return { failure_id: `sha256:${identity}`, delivery_head: failure.head, candidate };
105
+ }
106
+
63
107
  export async function runFinalVerification(
64
108
  root: string,
65
109
  commands: readonly string[],
66
110
  options: { timeoutMs?: number } = {},
67
111
  ): Promise<FinalVerificationReport> {
112
+ const verifiedCommands = parseFinalVerification([...commands])!;
68
113
  const head = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" });
114
+ const clean = () => {
115
+ const tracked = spawnSync("git", ["-C", root, "diff", "--quiet", "HEAD"]);
116
+ const untracked = spawnSync("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z"], { encoding: "utf8" });
117
+ return tracked.status === 0 && untracked.status === 0 && untracked.stdout.length === 0;
118
+ };
119
+ const cleanBefore = clean();
69
120
  const results: FinalVerificationResult[] = [];
70
- for (const command of commands) results.push(await runOne(root, command, options.timeoutMs ?? DEFAULT_TIMEOUT_MS));
71
- return { passed: results.every((result) => result.passed), head: head.status === 0 ? head.stdout.trim() : null, results };
121
+ for (const command of verifiedCommands) results.push(await runOne(root, command, options.timeoutMs ?? DEFAULT_TIMEOUT_MS));
122
+ const after = spawnSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" });
123
+ const unchangedHead = head.status === 0 && after.status === 0 && head.stdout.trim() === after.stdout.trim();
124
+ const cleanAfter = clean();
125
+ return { passed: results.every(result => result.passed) && unchangedHead && cleanBefore && cleanAfter, head: head.status === 0 ? head.stdout.trim() : null, results };
72
126
  }