immune-brain 5.2.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 (77) 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 +17 -1
  12. package/plugins/immune-brain/dist/claude/mcp-server.mjs +16145 -12745
  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-brainstorm.md +1 -1
  17. package/plugins/immune-brain/dist/imm-planner.md +63 -19
  18. package/plugins/immune-brain/dist/imm-run-batch.md +398 -0
  19. package/plugins/immune-brain/dist/imm-run.md +89 -344
  20. package/plugins/immune-brain/dist/role-prompts/code-review.md +26 -2
  21. package/plugins/immune-brain/dist/role-prompts/executor.md +35 -10
  22. package/plugins/immune-brain/hooks/hooks.json +22 -2
  23. package/plugins/immune-brain/runtime/assurance/coordinator.ts +203 -13
  24. package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +2 -1
  25. package/plugins/immune-brain/runtime/assurance/enrollment_git_base.ts +99 -14
  26. package/plugins/immune-brain/runtime/assurance/phase_entry.ts +10 -26
  27. package/plugins/immune-brain/runtime/assurance/qa.ts +126 -43
  28. package/plugins/immune-brain/runtime/assurance/qa_findings.ts +8 -0
  29. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +48 -0
  30. package/plugins/immune-brain/runtime/assurance/run_continuation.ts +284 -0
  31. package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +46 -4
  32. package/plugins/immune-brain/runtime/assurance/verification.ts +83 -35
  33. package/plugins/immune-brain/runtime/assurance/workflow_autocommit.ts +222 -0
  34. package/plugins/immune-brain/runtime/claude/interaction.ts +11 -2
  35. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +199 -48
  36. package/plugins/immune-brain/runtime/claude/lane_guard.ts +30 -20
  37. package/plugins/immune-brain/runtime/claude/mcp_server.ts +105 -9
  38. package/plugins/immune-brain/runtime/claude/run_continuation.ts +274 -0
  39. package/plugins/immune-brain/runtime/commands/kernel.ts +41 -103
  40. package/plugins/immune-brain/runtime/github_issue_tracker.ts +60 -2
  41. package/plugins/immune-brain/runtime/kernel/application.ts +4 -1
  42. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +103 -20
  43. package/plugins/immune-brain/runtime/kernel/canary_application.ts +57 -7
  44. package/plugins/immune-brain/runtime/kernel/completion.ts +28 -4
  45. package/plugins/immune-brain/runtime/kernel/enrollment.ts +48 -8
  46. package/plugins/immune-brain/runtime/kernel/intent.ts +169 -104
  47. package/plugins/immune-brain/runtime/kernel/reducer.ts +136 -6
  48. package/plugins/immune-brain/runtime/kernel/risk_floor.ts +286 -0
  49. package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +83 -1
  50. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +1 -1
  51. package/plugins/immune-brain/runtime/kernel/types.ts +49 -3
  52. package/plugins/immune-brain/runtime/kernel/validation.ts +147 -7
  53. package/plugins/immune-brain/runtime/loop_contract.ts +5 -0
  54. package/plugins/immune-brain/runtime/plan_core.ts +15 -8
  55. package/plugins/immune-brain/runtime/plugin_source_skew.ts +58 -0
  56. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  57. package/plugins/immune-brain/runtime/prompts/code-review.md +26 -2
  58. package/plugins/immune-brain/runtime/prompts/executor.md +35 -10
  59. package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +5 -3
  60. package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +40 -10
  61. package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +57 -3
  62. package/plugins/immune-brain/runtime/unattended/batch_git.ts +424 -66
  63. package/plugins/immune-brain/runtime/unattended/batch_integration.ts +2 -1
  64. package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +13 -0
  65. package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +43 -24
  66. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +6 -2
  67. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +102 -41
  68. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +9 -3
  69. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +9 -9
  70. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +90 -32
  71. package/plugins/immune-brain/runtime/unattended/batch_state.ts +102 -4
  72. package/plugins/immune-brain/runtime/unattended/parent_delivery_consolidation.ts +248 -0
  73. package/plugins/immune-brain/runtime/workspace_scope.ts +126 -18
  74. package/plugins/immune-brain/skills/BASELINE.md +17 -1
  75. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +4 -3
  76. package/plugins/immune-brain/skills/imm-planner/SKILL.md +8 -2
  77. package/plugins/immune-brain/skills/imm-run/SKILL.md +12 -8
@@ -21,13 +21,14 @@ export function batchQaFailureFacts(record: Pick<TaskRecord, "history"> & { find
21
21
  return { qa_failure_count: failed.length, last_qa_failure_at: failed.at(-1)?.at ?? null,
22
22
  recovery_findings: record.findings.filter(f => f.status === "open").map(f => ({ id: f.id, kind: f.kind, status: f.status, acceptance_id: f.acceptance_id })) };
23
23
  }
24
+ import { runFinalVerification } from "./batch_final_verification";
24
25
  import type { BatchPlanChild } from "./types";
25
26
  import { ownUnpersistedBatchHead, takeReconfirmation } from "./batch_reconfirmation";
26
27
  import {
27
28
  createDefaultBatchGitPort,
28
29
  type BatchRunnerGitPort,
29
30
  } from "./batch_git";
30
- import { classifyBatchLineage, expectedBatchHead } from "./batch_preflight";
31
+ import { classifyBatchLineage, expectedBatchHead, findSettledBatchRecord } from "./batch_preflight";
31
32
  import { laneRejectionReport, runLaneBatch, type LaneOffer } from "./batch_lanes";
32
33
  import {
33
34
  type BatchRunStateRecord,
@@ -46,7 +47,44 @@ import {
46
47
  /** Kernel-facing ports the driver needs. Host adapters supply these. Git
47
48
  * operations are not Kernel-port members: the runner drives them through
48
49
  * BatchRunnerGitPort alone (D5 of docs/specs/deepen-authority-seams.spec.md). */
50
+ import { readTaskTombstone } from "../kernel/backend_claim";
51
+ import { observeInitiative } from "./batch_plan";
52
+ import { observeParentCompletion } from "./batch_git";
53
+ import { createBatchTrackerPort } from "./batch_lanes";
54
+ import type { InitiativeObservationReader } from "./types";
55
+ export async function completedParentReport(root: string, initiativeSlug: string, readInitiative?: InitiativeObservationReader,
56
+ tracker = createBatchTrackerPort(root, initiativeSlug)): Promise<BatchRunReport | null> {
57
+ const candidate = findSettledBatchRecord(root, initiativeSlug);
58
+ if (!candidate || candidate.batch_state !== "completed") return null;
59
+ const batch = readAnyBatchRunState(root, candidate.batch_id);
60
+ if (!batch || batch.final_verification_result?.passed !== true) return null;
61
+ const parent = await (readInitiative ?? observeInitiative)(root, initiativeSlug);
62
+ const proof = await observeParentCompletion(root, initiativeSlug, parent.tasks.map(child => child.task_id));
63
+ if (!proof.complete) return null;
64
+ const last = parent.tasks.at(-1);
65
+ const tombstone = last ? readTaskTombstone(root, last.task_id) : null;
66
+ let sync: Awaited<ReturnType<BatchTrackerPort["markTerminal"]>> | null = null;
67
+ if (last && tracker) {
68
+ if (!tombstone || tombstone.terminal_lifecycle !== "done") throw new Error("Parent tracker synchronization requires exact terminal evidence");
69
+ try { sync = await tracker.markTerminal(root, { task_id: last.task_id, phase: "done", terminal_event_id: tombstone.terminal_event_id }); }
70
+ catch { sync = { status: "failed", message: "Local Parent completion proved; retry tracker synchronization on this Host" }; }
71
+ }
72
+ const report = {
73
+ contract: "assurance_kernel/batch_run_report/v1" as const,
74
+ batch_id: batch.batch_id, initiative_slug: initiativeSlug, batch_state: batch.batch_state,
75
+ children: batch.children, commits: batch.commits, final_verification: batch.final_verification_result,
76
+ ...(sync ? { tracker: sync } : {}),
77
+ reason: sync?.status === "failed" ? "Local Parent completion proved; tracker synchronization failed" : null,
78
+ next_action: sync?.status === "failed" ? sync.message : "Parent delivery and final verification passed",
79
+ created_at: new Date().toISOString(), ...(isLaneBatchRecord(batch) ? { max_parallel: batch.max_parallel, handoffs: [] } : {}),
80
+ };
81
+ // The first terminal report is historical; return this HEAD's freshly proved observation.
82
+ writeBatchRunReport(root, report);
83
+ return report as unknown as BatchRunReport;
84
+ }
85
+
49
86
  export interface BatchRunnerKernelPort {
87
+
50
88
  /** Enroll one child through the batch-derived capability. */
51
89
  enrollTask(input: {
52
90
  root: string;
@@ -68,6 +106,8 @@ export interface BatchRunnerKernelPort {
68
106
  /** Authoritative batch ownership check: true when the task's Kernel claim
69
107
  * is held under this batch's derived capability (consumed child slot). */
70
108
  ownsTaskClaim(taskId: string): boolean;
109
+ /** Read-only Parent prerequisites outside the ordinary authorization manifest. */
110
+ canStartTask?(taskId: string): Promise<boolean>;
71
111
  /** review-8(4th rework): Kernel-side proof that a replacement parked-batch
72
112
  * authorization is genuine and bound to this exact batch_id,
73
113
  * plan_digest, and base_head, validated against the Kernel's authoritative
@@ -122,9 +162,9 @@ export interface StartBatchInput {
122
162
  */
123
163
  executor_runtime?: string;
124
164
  /**
125
- * Lane mode only: the project's full verification commands, recorded when a
126
- * new batch starts and run on the batch branch once every child is
127
- * integrated. Ignored on a resume; the recorded list stands.
165
+ * The project's full verification commands, recorded when a new batch starts
166
+ * and run on its branch after every delivery is integrated. A resume uses
167
+ * the recorded list in either execution mode.
128
168
  */
129
169
  final_verification?: string[];
130
170
  /**
@@ -204,16 +244,13 @@ function requireFreshProjection<T extends AssuranceProjectionResult>(
204
244
  }
205
245
 
206
246
  /** Next pending child whose direct dependents are all committed. */
207
- function nextEnrollableChild(record: BatchRunStateRecord): BatchChildRun | null {
208
- return (
209
- record.children.find(
210
- (child) =>
211
- child.state === "pending" &&
212
- record.children.every(
213
- (other) => !child.blocked_by.includes(other.task_id) || other.state === "committed",
214
- ),
215
- ) ?? null
216
- );
247
+ async function nextEnrollableChild(record: BatchRunStateRecord, kernel: BatchRunnerKernelPort): Promise<BatchChildRun | null> {
248
+ for (const child of record.children) {
249
+ if (child.state !== "pending" || record.children.some(other => child.blocked_by.includes(other.task_id) && other.state !== "committed")) continue;
250
+ if (kernel.canStartTask && !await kernel.canStartTask(child.task_id)) continue;
251
+ return child;
252
+ }
253
+ return null;
217
254
  }
218
255
 
219
256
  const TERMINAL_NEXT_ACTIONS: Record<string, string> = {
@@ -239,6 +276,7 @@ function reportFor(
239
276
  batch_state: record.batch_state,
240
277
  children: record.children,
241
278
  commits: record.commits,
279
+ ...(record.final_verification_result ? { final_verification: record.final_verification_result } : {}),
242
280
  reason,
243
281
  next_action: nextAction || (TERMINAL_NEXT_ACTIONS[record.batch_state] ?? "Inspect the batch run state."),
244
282
  created_at: record.updated_at,
@@ -400,17 +438,22 @@ async function validatePersistedRun(input: StartBatchInput, record: BatchRunStat
400
438
  throw new Error("persisted batch commit list does not match child evidence");
401
439
  }
402
440
 
403
- /** review-6: restate a terminal record and ensure its report exists. */
404
- function replayTerminal(
405
- root: string,
406
- existing: BatchRunStateRecord,
407
- ): BatchRunReport {
408
- return finalize(
409
- root,
410
- existing,
411
- `terminal state already reached: ${existing.batch_state}`,
412
- "",
413
- );
441
+ async function finishSerial(input: StartBatchInput, record: BatchRunStateRecord): Promise<BatchRunReport> {
442
+ if (record.batch_state !== "completed" || !record.final_verification?.length)
443
+ return finalize(input.root, record, stopReasonFor(record), "");
444
+ if (!record.final_verification_result) {
445
+ const result = await runFinalVerification(input.root, record.final_verification);
446
+ record = writeBatchRunState(input.root, { ...record, final_verification_result: result });
447
+ }
448
+ const live = spawnSync("git", ["-C", input.root, "rev-parse", "HEAD"], { encoding: "utf8" });
449
+ const passed = record.final_verification_result!.passed && live.status === 0 && record.final_verification_result!.head === live.stdout.trim();
450
+ return finalize(input.root, record, passed ? "Every serial delivery is integrated and final verification passed" : "Final verification failed or its Git HEAD changed; Parent is not complete",
451
+ passed ? "Check every required Parent Slice before reporting overall success" : "Diagnose the failure, prepare the smallest repair TaskIntent for the original goal, validate it and request fresh native Enrollment");
452
+ }
453
+
454
+ /** A terminal replay retains its result and never silently reruns recorded commands. */
455
+ function replayTerminal(input: StartBatchInput, existing: BatchRunStateRecord): Promise<BatchRunReport> {
456
+ return finishSerial(input, existing);
414
457
  }
415
458
 
416
459
  function validateRunAuthorization(input: StartBatchInput, existing: BatchRunStateRecord | null): ValidatedBatchAuthorization {
@@ -535,7 +578,7 @@ async function startBatchLocked(input: StartBatchInput): Promise<BatchRunReport>
535
578
  } catch (error) {
536
579
  const message = error instanceof Error ? error.message : String(error);
537
580
  if (isLineageBreakError(message)) {
538
- if (isTerminalBatchState(existing.batch_state)) return replayTerminal(input.root, existing);
581
+ if (isTerminalBatchState(existing.batch_state)) return replayTerminal(input, existing);
539
582
  const failed = failPersistedLineage(input.root, existing, message);
540
583
  return finalize(input.root, failed, message, "");
541
584
  }
@@ -544,7 +587,7 @@ async function startBatchLocked(input: StartBatchInput): Promise<BatchRunReport>
544
587
  }
545
588
  if (existing && isTerminalBatchState(existing.batch_state)) {
546
589
  // Idempotent terminal replay: no state mutation, ensure the report.
547
- return replayTerminal(input.root, existing);
590
+ return replayTerminal(input, existing);
548
591
  }
549
592
 
550
593
  if (existing?.batch_state === "needs_human") {
@@ -683,6 +726,7 @@ async function startBatchLocked(input: StartBatchInput): Promise<BatchRunReport>
683
726
  confirmation_time: input.confirmation_time,
684
727
  budget: input.budget,
685
728
  now: input.now,
729
+ ...(input.final_verification ? { final_verification: input.final_verification } : {}),
686
730
  });
687
731
 
688
732
  const persist = (): void => {
@@ -705,8 +749,10 @@ async function startBatchLocked(input: StartBatchInput): Promise<BatchRunReport>
705
749
  let head = expectedBatchHead(record);
706
750
 
707
751
  while (record.batch_state === "running") {
708
- const child = nextEnrollableChild(record);
752
+ const child = await nextEnrollableChild(record, input.kernel);
709
753
  if (!child) {
754
+ if (record.children.some(candidate => candidate.state === "pending"))
755
+ return reportFor(record, "Remaining children await Parent prerequisites", "Execute dependency-ready critical Slices through native single-task Enrollment, then continue this intact batch authorization");
710
756
  record.batch_state = record.children.some(
711
757
  (c) => c.state === "needs_human" || c.state === "skipped_blocked",
712
758
  )
@@ -835,7 +881,7 @@ async function startBatchLocked(input: StartBatchInput): Promise<BatchRunReport>
835
881
 
836
882
  }
837
883
 
838
- return finalize(input.root, record, stopReasonFor(record), "");
884
+ return finishSerial(input, record);
839
885
  }
840
886
 
841
887
  function stopReasonFor(record: BatchRunStateRecord): string | null {
@@ -877,14 +923,14 @@ export async function resumeBatch(
877
923
  } catch (error) {
878
924
  const message = error instanceof Error ? error.message : String(error);
879
925
  if (isLineageBreakError(message)) {
880
- if (isTerminalBatchState(existing.batch_state)) return replayTerminal(input.root, existing);
926
+ if (isTerminalBatchState(existing.batch_state)) return replayTerminal(input, existing);
881
927
  const failed = failPersistedLineage(input.root, existing, message);
882
928
  return finalize(input.root, failed, message, "");
883
929
  }
884
930
  throw error;
885
931
  }
886
932
  if (isTerminalBatchState(existing.batch_state))
887
- return replayTerminal(input.root, existing);
933
+ return replayTerminal(input, existing);
888
934
 
889
935
  // An enrolled-but-unsettled child must reach its own Kernel terminal
890
936
  // settlement before anything else: its claim is already held and the
@@ -976,6 +1022,10 @@ export async function resumeBatch(
976
1022
  : c,
977
1023
  ),
978
1024
  });
1025
+ } else if (fresh.projection.lifecycle === "stopped" && fresh.claim === null) {
1026
+ skipDependents(existing, driven.task_id, `dependency ${driven.task_id} stopped`);
1027
+ existing = writeBatchRunState(input.root, { ...existing,
1028
+ children: existing.children.map(c => c.task_id === driven.task_id ? { ...c, state: "needs_human", reason: "Kernel child stopped" } : c) });
979
1029
  } else {
980
1030
  // No Kernel claim and no terminal settlement: the enrollment
981
1031
  // never completed. Re-mark the child pending so the normal
@@ -1013,7 +1063,7 @@ export async function resumeBatch(
1013
1063
  // the persisted child states, not the stale pre-recovery snapshot.
1014
1064
  existing = readBatchRunState(input.root, input.batch_id)!;
1015
1065
  if (isTerminalBatchState(existing.batch_state))
1016
- return replayTerminal(input.root, existing);
1066
+ return replayTerminal(input, existing);
1017
1067
  }
1018
1068
 
1019
1069
  // No interrupted child remains: continue with the normal serial loop.
@@ -1175,6 +1225,14 @@ async function driveInterruptedChild(
1175
1225
  record.children = record.children.map((c) =>
1176
1226
  c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c,
1177
1227
  );
1228
+ if (terminal.state === "stopped") {
1229
+ const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
1230
+ if (fresh.claim === null && fresh.projection.lifecycle === "stopped") {
1231
+ skipDependents(record, child.task_id, `dependency ${child.task_id} stopped`);
1232
+ persist();
1233
+ return null;
1234
+ }
1235
+ }
1178
1236
  record.batch_state = "needs_human";
1179
1237
  skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
1180
1238
  persist();
@@ -5,7 +5,9 @@ import { existsSync, mkdirSync, openSync, closeSync, writeFileSync, renameSync,
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { dirname, join } from "node:path";
7
7
  import { readSecureProjectFile, readSecureProjectBytes, withKernelStoreLock } from "../kernel/storage";
8
+ import { parseFinalVerification, parseFinalVerificationReport } from "./batch_final_verification";
8
9
  import type { BatchPlanChild } from "./types";
10
+ import { parseConsolidationReceipt } from "./parent_delivery_consolidation";
9
11
 
10
12
  export type BatchRunState =
11
13
  | "prepared"
@@ -62,6 +64,11 @@ export interface BatchRunStateRecord {
62
64
  consecutive_qa_failures: number;
63
65
  /** Commit heads produced by this batch, in child order. */
64
66
  commits: string[];
67
+ /** Recorded at confirmation; a resume never replaces these commands. */
68
+ final_verification?: string[];
69
+ final_verification_result?: import("./batch_final_verification").FinalVerificationReport;
70
+ /** Host observation of separately enrolled repairs; grants no task authority. */
71
+ repair_tasks?: Array<{ task_id: string; run_id: string }>;
65
72
  /**
66
73
  * Fast-forward commits the user made on the batch branch, adopted as the new
67
74
  * expected head. Never attributed to a child and never rewritten.
@@ -168,6 +175,8 @@ export interface BatchLaneRunStateRecord {
168
175
  interventions?: LaneIntervention[];
169
176
  /** Commands run on the batch branch once every child is integrated; fixed when the batch starts. */
170
177
  final_verification?: string[];
178
+ final_verification_result?: import("./batch_final_verification").FinalVerificationReport;
179
+ repair_tasks?: Array<{ task_id: string; run_id: string }>;
171
180
  /**
172
181
  * ADR 0018: the literal user's grant, in this batch's confirmation, to let the
173
182
  * coordinator approve in-envelope Lane Intent revisions. Absent: not granted.
@@ -244,12 +253,27 @@ function canonicalBytes(record: AnyBatchRunStateRecord): string {
244
253
  return `${JSON.stringify(withoutRetiredClock(record), null, 2)}\n`;
245
254
  }
246
255
 
256
+ function validateRepairTasks(record: Record<string, unknown>): void {
257
+ if (record.repair_tasks === undefined) return;
258
+ if (!Array.isArray(record.repair_tasks) || record.repair_tasks.some(task => !task ||
259
+ typeof task.task_id !== "string" || !BATCH_ID_PATTERN.test(task.task_id) ||
260
+ typeof task.run_id !== "string" || !BATCH_ID_PATTERN.test(task.run_id)) ||
261
+ new Set(record.repair_tasks.map(task => task.task_id)).size !== record.repair_tasks.length)
262
+ throw new Error("invalid Parent repair task identities");
263
+ }
264
+
247
265
  function validateRecordShape(value: unknown, batchId: string): asserts value is BatchRunStateRecord {
248
266
  if (typeof value !== "object" || value === null)
249
267
  throw new Error(`batch run state ${batchId} is not an object`);
250
268
  const record = value as Record<string, unknown>;
251
269
  if (record.contract !== "assurance_kernel/batch_run_state/v1")
252
270
  throw new Error(`batch run state ${batchId} has an unknown contract`);
271
+ parseFinalVerification(record.final_verification);
272
+ validateRepairTasks(record);
273
+ if (record.final_verification_result !== undefined) {
274
+ parseFinalVerificationReport(record.final_verification_result, record.final_verification as string[] | undefined);
275
+ if (record.batch_state !== "completed") throw new Error("final verification requires a completed serial batch");
276
+ }
253
277
  if (record.batch_id !== batchId)
254
278
  throw new Error(`batch run state ${batchId} carries batch_id ${String(record.batch_id)}`);
255
279
  if (typeof record.plan_digest !== "string" || !record.plan_digest)
@@ -339,11 +363,13 @@ function validateRecordShape(value: unknown, batchId: string): asserts value is
339
363
  if (child.state === "committed" && child.commit !== null && !record.commits.includes(child.commit))
340
364
  throw new Error(`batch run state ${batchId} child ${child.task_id} commit is missing from commits`);
341
365
  }
366
+ // `superseded` is exempt: a retired record carries its children untouched, and
367
+ // the disposition owner admits a mid-flight state only when the Kernel store
368
+ // proves that run already ended outside the batch (ADR 0016).
342
369
  if (
343
370
  (record.batch_state === "budget_stopped" ||
344
371
  record.batch_state === "failed" ||
345
- record.batch_state === "rejected" ||
346
- record.batch_state === "superseded") &&
372
+ record.batch_state === "rejected") &&
347
373
  record.children.some((child) => child.state === "enrolled" || child.state === "settled")
348
374
  )
349
375
  throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
@@ -364,10 +390,12 @@ export function prepareBatchRunState(input: {
364
390
  confirmation_time: string;
365
391
  budget: { max_children: number; qa_failure_limit: number };
366
392
  now: string;
393
+ final_verification?: string[];
367
394
  }): BatchRunStateRecord {
368
395
  validateBatchId(input.batch_id);
369
396
  const prepared: BatchRunStateRecord = {
370
397
  contract: "assurance_kernel/batch_run_state/v1",
398
+ ...(input.final_verification ? { final_verification: parseFinalVerification(input.final_verification)! } : {}),
371
399
  batch_id: input.batch_id,
372
400
  initiative_slug: input.initiative_slug,
373
401
  plan_digest: input.plan_digest,
@@ -587,6 +615,11 @@ function validateLaneRecordShape(value: unknown, batchId: string): asserts value
587
615
  record.final_verification.some((c: unknown) => typeof c !== "string" || !c))
588
616
  )
589
617
  throw new Error(`batch run state ${batchId} has an invalid final_verification list`);
618
+ validateRepairTasks(record);
619
+ if (record.final_verification_result !== undefined) {
620
+ parseFinalVerificationReport(record.final_verification_result, record.final_verification as string[] | undefined);
621
+ if (record.batch_state !== "completed") throw new Error("final verification requires a completed lane batch");
622
+ }
590
623
  if (
591
624
  record.interventions !== undefined &&
592
625
  (!Array.isArray(record.interventions) ||
@@ -702,9 +735,9 @@ function validateLaneRecordShape(value: unknown, batchId: string): asserts value
702
735
  child.state === "enrolled" ||
703
736
  child.state === "settled" ||
704
737
  child.state === "lane_committed";
738
+ // `superseded` is exempt for the same reason as the serial record (ADR 0016).
705
739
  if (
706
- (record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected" ||
707
- record.batch_state === "superseded") &&
740
+ (record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected") &&
708
741
  record.children.some(inFlight)
709
742
  )
710
743
  throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
@@ -826,6 +859,10 @@ export interface BatchRunReport {
826
859
  /** Safe observation only; never an execution-readiness certificate. */
827
860
  recovery?: import("../assurance/coordinator").AssuranceRecovery;
828
861
  diagnostics?: import("../assurance/qa").QaCheckDiagnostic[];
862
+ /** Final commands passed on this exact integrated head; absent means unverified. */
863
+ final_verification?: import("./batch_final_verification").FinalVerificationReport;
864
+ /** Latest Parent synchronization observation; does not replace delivery evidence. */
865
+ tracker?: { status: string; message: string };
829
866
  /** The single next action for the operator. */
830
867
  next_action: string;
831
868
  created_at: string;
@@ -915,6 +952,67 @@ function reportPath(batchId: string): string {
915
952
  /** Persist one current stop report per batch. Terminal reports are immutable;
916
953
  * a resumable needs_human report may be replaced by the later stop reached
917
954
  * after a fresh literal-user confirmation. */
955
+ export interface NativeParentFinalReport {
956
+ contract: "assurance_kernel/parent_final_report/v1";
957
+ consolidation_digest?: string;
958
+ initiative_slug: string;
959
+ task_ids: string[];
960
+ final_verification: import("./batch_final_verification").FinalVerificationReport;
961
+ repair_tasks?: Array<{ task_id: string; run_id: string }>;
962
+ }
963
+
964
+ function nativeParentReportPath(slug: string) {
965
+ validateBatchId(slug);
966
+ return join(".imm", "state", "batches", `${slug}.native.report.json`);
967
+ }
968
+
969
+ function validateNativeParentReport(report: NativeParentFinalReport) {
970
+ if (report.contract !== "assurance_kernel/parent_final_report/v1" || !Array.isArray(report.task_ids) || !report.task_ids.length ||
971
+ new Set(report.task_ids).size !== report.task_ids.length || report.task_ids.some(id => typeof id !== "string" || !BATCH_ID_PATTERN.test(id)))
972
+ throw new Error("invalid Parent final observation");
973
+ validateBatchId(report.initiative_slug);
974
+ if (report.consolidation_digest !== undefined && !/^sha256:[a-f0-9]{64}$/.test(report.consolidation_digest))
975
+ throw new Error("invalid final consolidation binding");
976
+ validateRepairTasks({ repair_tasks: report.repair_tasks });
977
+ parseFinalVerificationReport(report.final_verification);
978
+ }
979
+
980
+ /** Final observations for Parents needing no ordinary batch; never a BatchAuthorization or readiness store. */
981
+ export function readNativeParentFinalReport(root: string, slug: string): NativeParentFinalReport | null {
982
+ const path = nativeParentReportPath(slug);
983
+ if (!existsSync(join(root, path))) return null;
984
+ const report = JSON.parse(readSecureProjectFile(root, path));
985
+ validateNativeParentReport(report);
986
+ if (report.initiative_slug !== slug) throw new Error("Parent final observation identity changed");
987
+ return report;
988
+ }
989
+
990
+ export function readParentConsolidationReceipt(root: string, slug: string): import("./parent_delivery_consolidation").ConsolidationReceipt | null {
991
+ validateBatchId(slug);
992
+ const path = `.imm/state/batches/${slug}.consolidation.json`;
993
+ if (!existsSync(join(root, path))) return null;
994
+ const receipt = parseConsolidationReceipt(JSON.parse(readSecureProjectFile(root, path)));
995
+ if (receipt.envelope.initiative_slug !== slug) throw new Error("consolidation receipt Parent mismatch");
996
+ return receipt;
997
+ }
998
+
999
+ export function writeParentConsolidationReceipt(root: string, receipt: import("./parent_delivery_consolidation").ConsolidationReceipt): void {
1000
+ parseConsolidationReceipt(receipt);
1001
+ validateBatchId(receipt.envelope.initiative_slug);
1002
+ withKernelStoreLock(root, () => {
1003
+ ensureSecureDirectory(root, join(".imm", "state", "batches"));
1004
+ writeFileAtomically(root, `.imm/state/batches/${receipt.envelope.initiative_slug}.consolidation.json`, JSON.stringify(receipt, null, 2) + "\n");
1005
+ });
1006
+ }
1007
+
1008
+ export function writeNativeParentFinalReport(root: string, report: NativeParentFinalReport): void {
1009
+ validateNativeParentReport(report);
1010
+ withKernelStoreLock(root, () => {
1011
+ ensureSecureDirectory(root, join(".imm", "state", "batches"));
1012
+ writeFileAtomically(root, nativeParentReportPath(report.initiative_slug), JSON.stringify(report, null, 2) + "\n");
1013
+ });
1014
+ }
1015
+
918
1016
  export function writeBatchRunReport<T extends PersistableReport>(root: string, report: T): T {
919
1017
  const relative = reportPath(report.batch_id);
920
1018
  return withKernelStoreLock(root, () => {