immune-brain 5.0.0 → 5.1.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 (49) hide show
  1. package/README.md +223 -12
  2. package/README.zh-CN.md +228 -11
  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-work.ts +18 -5
  6. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +189 -3
  7. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +66 -3
  8. package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -1
  9. package/plugins/immune-brain/dist/claude/mcp-server.mjs +1885 -149
  10. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +27 -0
  11. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +8 -6
  12. package/plugins/immune-brain/dist/imm-planner.md +4 -1
  13. package/plugins/immune-brain/dist/imm-run.md +198 -3
  14. package/plugins/immune-brain/dist/role-prompts/code-review.md +10 -0
  15. package/plugins/immune-brain/dist/role-prompts/lane-steward.md +44 -0
  16. package/plugins/immune-brain/runtime/assurance/coordinator.ts +22 -4
  17. package/plugins/immune-brain/runtime/assurance/review_mediation.ts +107 -0
  18. package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +3 -0
  19. package/plugins/immune-brain/runtime/claude/interaction.ts +16 -0
  20. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +105 -58
  21. package/plugins/immune-brain/runtime/claude/mcp_server.ts +89 -16
  22. package/plugins/immune-brain/runtime/claude/role_agents.ts +4 -3
  23. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
  24. package/plugins/immune-brain/runtime/kernel/canary_application.ts +9 -2
  25. package/plugins/immune-brain/runtime/kernel/completion.ts +24 -1
  26. package/plugins/immune-brain/runtime/kernel/reducer.ts +15 -2
  27. package/plugins/immune-brain/runtime/kernel/spec_binding.ts +6 -0
  28. package/plugins/immune-brain/runtime/kernel/storage.ts +20 -4
  29. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +33 -3
  30. package/plugins/immune-brain/runtime/kernel/types.ts +7 -0
  31. package/plugins/immune-brain/runtime/kernel/validation.ts +13 -2
  32. package/plugins/immune-brain/runtime/local_initiative.ts +94 -0
  33. package/plugins/immune-brain/runtime/loop_contract.ts +55 -2
  34. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  35. package/plugins/immune-brain/runtime/prompts/code-review.md +10 -0
  36. package/plugins/immune-brain/runtime/prompts/lane-steward.md +44 -0
  37. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +16 -3
  38. package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +222 -0
  39. package/plugins/immune-brain/runtime/unattended/batch_integration.ts +387 -0
  40. package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +2 -2
  41. package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +946 -0
  42. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +36 -11
  43. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +48 -33
  44. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +13 -1
  45. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +22 -1
  46. package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +162 -0
  47. package/plugins/immune-brain/runtime/unattended/batch_state.ts +398 -12
  48. package/plugins/immune-brain/runtime/unattended/types.ts +21 -2
  49. package/plugins/immune-brain/skills/imm-run/SKILL.md +4 -0
@@ -14,7 +14,14 @@ export type BatchRunState =
14
14
  | "completed"
15
15
  | "budget_stopped"
16
16
  | "failed"
17
- | "rejected";
17
+ | "rejected"
18
+ /**
19
+ * Retired by an explicit literal-user disposition after the batch plan moved
20
+ * past it. Terminal, and not a failure of the batch's own: the record, its
21
+ * children, its commits and its branch stay as evidence, and the retired
22
+ * authorization is never reused for the revised plan.
23
+ */
24
+ | "superseded";
18
25
 
19
26
  export type BatchChildRunState =
20
27
  | "pending"
@@ -64,6 +71,78 @@ export interface BatchRunStateRecord {
64
71
  updated_at: string;
65
72
  }
66
73
 
74
+ /**
75
+ * Lane-mode child states. `integrated` plays the role `committed` plays in the
76
+ * serial record: it is what unblocks dependents. `released` is recorded by the
77
+ * lane steward once an integrated Lane is gone.
78
+ */
79
+ export type BatchLaneChildRunState =
80
+ | "pending"
81
+ | "lane_admitted"
82
+ | "enrolled"
83
+ | "settled"
84
+ | "lane_committed"
85
+ | "integrated"
86
+ | "released"
87
+ | "needs_human"
88
+ | "skipped_blocked";
89
+
90
+ export interface BatchLaneBinding {
91
+ /** Absolute, real path of the admitted Lane worktree. */
92
+ path: string;
93
+ branch: string;
94
+ /** The batch head the Lane was provisioned at and the child was enrolled on. */
95
+ base_head: string;
96
+ /** Commit created in the Lane after the Lane Kernel settled the child. */
97
+ lane_commit: string | null;
98
+ /** Kernel run observed at enrollment; absent historical bindings cannot resume a parked child. */
99
+ run_id?: string | null;
100
+ }
101
+
102
+ export interface BatchLaneChildRun {
103
+ task_id: string;
104
+ slice_id: string;
105
+ blocked_by: string[];
106
+ state: BatchLaneChildRunState;
107
+ reason: string | null;
108
+ /** Commit on the batch branch once this child is integrated. */
109
+ commit: string | null;
110
+ lane: BatchLaneBinding | null;
111
+ qa_failures: number;
112
+ }
113
+
114
+ /**
115
+ * Lane-mode batch state. A serial batch never writes this contract; a v1
116
+ * record is never upgraded. Every v1-gated reader skips it by contract.
117
+ */
118
+ export interface BatchLaneRunStateRecord {
119
+ contract: "assurance_kernel/batch_run_state/v2";
120
+ batch_id: string;
121
+ initiative_slug: string;
122
+ plan_digest: string;
123
+ base_head: string;
124
+ branch?: string;
125
+ confirmation_time: string;
126
+ budget: {
127
+ max_children: number;
128
+ qa_failure_limit: number;
129
+ };
130
+ max_parallel: number;
131
+ batch_state: BatchRunState;
132
+ children: BatchLaneChildRun[];
133
+ /** Batch-branch commits produced by integration, in integration order. */
134
+ commits: string[];
135
+ adopted_heads?: Array<{ from: string; to: string }>;
136
+ created_at: string;
137
+ updated_at: string;
138
+ }
139
+
140
+ export type AnyBatchRunStateRecord = BatchRunStateRecord | BatchLaneRunStateRecord;
141
+
142
+ export function isLaneBatchRecord(record: AnyBatchRunStateRecord): record is BatchLaneRunStateRecord {
143
+ return record.contract === "assurance_kernel/batch_run_state/v2";
144
+ }
145
+
67
146
  const BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
68
147
 
69
148
  const CHILD_RUN_STATES: ReadonlySet<string> = new Set([
@@ -75,7 +154,7 @@ const CHILD_RUN_STATES: ReadonlySet<string> = new Set([
75
154
  "skipped_blocked",
76
155
  ]);
77
156
 
78
- const BATCH_RUN_STATES: ReadonlySet<string> = new Set([
157
+ export const BATCH_RUN_STATES: ReadonlySet<string> = new Set([
79
158
  "prepared",
80
159
  "running",
81
160
  "needs_human",
@@ -83,6 +162,7 @@ const BATCH_RUN_STATES: ReadonlySet<string> = new Set([
83
162
  "budget_stopped",
84
163
  "failed",
85
164
  "rejected",
165
+ "superseded",
86
166
  ]);
87
167
 
88
168
  const ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
@@ -108,13 +188,13 @@ function statePath(batchId: string): string {
108
188
  * `authorization_expires_at` and `budget.deadline_at`. They are ignored on read
109
189
  * and never written back.
110
190
  */
111
- function withoutRetiredClock(record: BatchRunStateRecord): BatchRunStateRecord {
112
- const { authorization_expires_at: _expiry, ...rest } = record as BatchRunStateRecord & { authorization_expires_at?: unknown };
113
- const { deadline_at: _deadline, ...budget } = rest.budget as BatchRunStateRecord["budget"] & { deadline_at?: unknown };
114
- return { ...rest, budget };
191
+ function withoutRetiredClock<T extends AnyBatchRunStateRecord>(record: T): T {
192
+ const { authorization_expires_at: _expiry, ...rest } = record as T & { authorization_expires_at?: unknown };
193
+ const { deadline_at: _deadline, ...budget } = rest.budget as T["budget"] & { deadline_at?: unknown };
194
+ return { ...rest, budget } as unknown as T;
115
195
  }
116
196
 
117
- function canonicalBytes(record: BatchRunStateRecord): string {
197
+ function canonicalBytes(record: AnyBatchRunStateRecord): string {
118
198
  return `${JSON.stringify(withoutRetiredClock(record), null, 2)}\n`;
119
199
  }
120
200
 
@@ -216,7 +296,8 @@ function validateRecordShape(value: unknown, batchId: string): asserts value is
216
296
  if (
217
297
  (record.batch_state === "budget_stopped" ||
218
298
  record.batch_state === "failed" ||
219
- record.batch_state === "rejected") &&
299
+ record.batch_state === "rejected" ||
300
+ record.batch_state === "superseded") &&
220
301
  record.children.some((child) => child.state === "enrolled" || child.state === "settled")
221
302
  )
222
303
  throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
@@ -328,6 +409,25 @@ export function replaceBatchRunState(root: string, expected: Buffer, next: Batch
328
409
  });
329
410
  }
330
411
 
412
+ /** Replace a record of either version only after a locked expected-byte check. */
413
+ export function replaceAnyBatchRunState(
414
+ root: string,
415
+ expected: Buffer,
416
+ next: AnyBatchRunStateRecord,
417
+ validate: () => void,
418
+ ): AnyBatchRunStateRecord {
419
+ const path = statePath(next.batch_id);
420
+ if (isLaneBatchRecord(next)) validateLaneRecordShape(next, next.batch_id);
421
+ else validateRecordShape(next, next.batch_id);
422
+ return withKernelStoreLock(root, () => {
423
+ if (!readSecureProjectBytes(root, path).equals(expected)) throw new Error("batch state CAS mismatch");
424
+ validate();
425
+ const stored = withoutRetiredClock({ ...next, updated_at: new Date().toISOString() });
426
+ writeFileAtomically(root, path, canonicalBytes(stored));
427
+ return stored;
428
+ });
429
+ }
430
+
331
431
  export function writeBatchRunState(
332
432
  root: string,
333
433
  record: BatchRunStateRecord,
@@ -348,6 +448,242 @@ export function writeBatchRunState(
348
448
  });
349
449
  }
350
450
 
451
+
452
+ const LANE_CHILD_STATES: ReadonlySet<string> = new Set([
453
+ "pending",
454
+ "lane_admitted",
455
+ "enrolled",
456
+ "settled",
457
+ "lane_committed",
458
+ "integrated",
459
+ "released",
460
+ "needs_human",
461
+ "skipped_blocked",
462
+ ]);
463
+
464
+ /** Lane-mode states whose child must carry a Lane binding. */
465
+ const LANE_BOUND_STATES: ReadonlySet<string> = new Set([
466
+ "lane_admitted",
467
+ "enrolled",
468
+ "settled",
469
+ "lane_committed",
470
+ "integrated",
471
+ "released",
472
+ ]);
473
+
474
+ function validateLaneRecordShape(value: unknown, batchId: string): asserts value is BatchLaneRunStateRecord {
475
+ if (typeof value !== "object" || value === null)
476
+ throw new Error(`batch run state ${batchId} is not an object`);
477
+ const record = value as Record<string, unknown>;
478
+ if (record.contract !== "assurance_kernel/batch_run_state/v2")
479
+ throw new Error(`batch run state ${batchId} has an unknown contract`);
480
+ if (record.batch_id !== batchId)
481
+ throw new Error(`batch run state ${batchId} carries batch_id ${String(record.batch_id)}`);
482
+ if (typeof record.plan_digest !== "string" || !record.plan_digest)
483
+ throw new Error(`batch run state ${batchId} has an invalid plan_digest`);
484
+ if (typeof record.base_head !== "string" || !record.base_head)
485
+ throw new Error(`batch run state ${batchId} has an invalid base_head`);
486
+ if (record.branch !== undefined && (typeof record.branch !== "string" || !record.branch))
487
+ throw new Error(`batch run state ${batchId} has an invalid branch`);
488
+ if (!isCanonicalTimestamp(record.confirmation_time))
489
+ throw new Error(`batch run state ${batchId} has an invalid confirmation_time`);
490
+ if (!isCanonicalTimestamp(record.created_at) || !isCanonicalTimestamp(record.updated_at))
491
+ throw new Error(`batch run state ${batchId} has invalid state timestamps`);
492
+ if (!Array.isArray(record.children) || record.children.length === 0)
493
+ throw new Error(`batch run state ${batchId} has no children`);
494
+ if (!BATCH_RUN_STATES.has(String(record.batch_state)))
495
+ throw new Error(`batch run state ${batchId} has an invalid batch_state`);
496
+ if (typeof record.max_parallel !== "number" || !Number.isSafeInteger(record.max_parallel) || record.max_parallel <= 0)
497
+ throw new Error(`batch run state ${batchId} has an invalid max_parallel`);
498
+ const budget = record.budget as Record<string, unknown>;
499
+ if (
500
+ typeof record.budget !== "object" ||
501
+ record.budget === null ||
502
+ typeof budget.max_children !== "number" ||
503
+ !Number.isInteger(budget.max_children) ||
504
+ budget.max_children <= 0 ||
505
+ typeof budget.qa_failure_limit !== "number" ||
506
+ !Number.isInteger(budget.qa_failure_limit) ||
507
+ budget.qa_failure_limit <= 0
508
+ )
509
+ throw new Error(`batch run state ${batchId} has an invalid budget`);
510
+ if (!Array.isArray(record.commits) || record.commits.some((c) => typeof c !== "string"))
511
+ throw new Error(`batch run state ${batchId} has an invalid commits list`);
512
+ if (
513
+ record.adopted_heads !== undefined &&
514
+ (!Array.isArray(record.adopted_heads) ||
515
+ record.adopted_heads.some(
516
+ (a: unknown) =>
517
+ typeof a !== "object" ||
518
+ a === null ||
519
+ typeof (a as { from?: unknown }).from !== "string" ||
520
+ !(a as { from: string }).from ||
521
+ typeof (a as { to?: unknown }).to !== "string" ||
522
+ !(a as { to: string }).to,
523
+ ))
524
+ )
525
+ throw new Error(`batch run state ${batchId} has an invalid adopted_heads list`);
526
+ const seenTaskIds = new Set<string>();
527
+ for (const child of record.children) {
528
+ if (
529
+ typeof child !== "object" ||
530
+ child === null ||
531
+ typeof child.task_id !== "string" ||
532
+ !child.task_id ||
533
+ typeof child.slice_id !== "string" ||
534
+ !child.slice_id
535
+ )
536
+ throw new Error(`batch run state ${batchId} has an invalid child entry`);
537
+ if (!LANE_CHILD_STATES.has(String(child.state)))
538
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid state`);
539
+ if (!Array.isArray(child.blocked_by) || child.blocked_by.some((b: unknown) => typeof b !== "string"))
540
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid blocked_by`);
541
+ if (
542
+ (child.reason !== null && typeof child.reason !== "string") ||
543
+ (child.commit !== null && typeof child.commit !== "string")
544
+ )
545
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has invalid terminal fields`);
546
+ if (typeof child.qa_failures !== "number" || !Number.isInteger(child.qa_failures) || child.qa_failures < 0)
547
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid qa_failures`);
548
+ const lane = child.lane as BatchLaneBinding | null | undefined;
549
+ if (lane !== null) {
550
+ if (
551
+ typeof lane !== "object" ||
552
+ typeof lane.path !== "string" ||
553
+ !lane.path ||
554
+ typeof lane.branch !== "string" ||
555
+ !lane.branch ||
556
+ typeof lane.base_head !== "string" ||
557
+ !lane.base_head ||
558
+ (lane.lane_commit !== null && typeof lane.lane_commit !== "string") ||
559
+ (lane.run_id !== undefined && lane.run_id !== null && (typeof lane.run_id !== "string" || !lane.run_id))
560
+ )
561
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid lane`);
562
+ }
563
+ if (LANE_BOUND_STATES.has(child.state) && lane === null)
564
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a lane`);
565
+ if ((child.state === "pending" || child.state === "skipped_blocked") && lane !== null)
566
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} but holds a lane`);
567
+ const integrated = child.state === "integrated" || child.state === "released";
568
+ if (integrated && child.commit === null)
569
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a commit`);
570
+ if (!integrated && child.commit !== null)
571
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has a commit but is not integrated`);
572
+ if (integrated && lane?.lane_commit === null)
573
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a lane commit`);
574
+ if (child.state === "lane_committed" && lane?.lane_commit === null)
575
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is lane_committed without a lane commit`);
576
+ if ((child.state === "needs_human" || child.state === "skipped_blocked") && child.reason === null)
577
+ throw new Error(`batch run state ${batchId} child ${child.task_id} needs a terminal reason`);
578
+ if (seenTaskIds.has(child.task_id))
579
+ throw new Error(`batch run state ${batchId} has a duplicate child ${child.task_id}`);
580
+ seenTaskIds.add(child.task_id);
581
+ }
582
+ for (const child of record.children) {
583
+ if (child.commit !== null && !record.commits.includes(child.commit))
584
+ throw new Error(`batch run state ${batchId} child ${child.task_id} commit is missing from commits`);
585
+ }
586
+ const inFlight = (child: { state: string }) =>
587
+ child.state === "lane_admitted" ||
588
+ child.state === "enrolled" ||
589
+ child.state === "settled" ||
590
+ child.state === "lane_committed";
591
+ if (
592
+ (record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected" ||
593
+ record.batch_state === "superseded") &&
594
+ record.children.some(inFlight)
595
+ )
596
+ throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
597
+ if (
598
+ record.batch_state === "completed" &&
599
+ record.children.some((child) => child.state !== "integrated" && child.state !== "released")
600
+ )
601
+ throw new Error(`batch run state ${batchId} is completed but a child is not integrated`);
602
+ }
603
+
604
+ export function prepareBatchLaneRunState(input: {
605
+ batch_id: string;
606
+ initiative_slug: string;
607
+ children: BatchPlanChild[];
608
+ plan_digest: string;
609
+ base_head: string;
610
+ branch?: string;
611
+ confirmation_time: string;
612
+ budget: { max_children: number; qa_failure_limit: number };
613
+ max_parallel: number;
614
+ now: string;
615
+ }): BatchLaneRunStateRecord {
616
+ validateBatchId(input.batch_id);
617
+ return {
618
+ contract: "assurance_kernel/batch_run_state/v2",
619
+ batch_id: input.batch_id,
620
+ initiative_slug: input.initiative_slug,
621
+ plan_digest: input.plan_digest,
622
+ base_head: input.base_head,
623
+ branch: input.branch ?? `imm/${input.initiative_slug}`,
624
+ confirmation_time: input.confirmation_time,
625
+ budget: input.budget,
626
+ max_parallel: input.max_parallel,
627
+ batch_state: "prepared",
628
+ children: input.children.map((child) => ({
629
+ task_id: child.task_id,
630
+ slice_id: child.slice_id,
631
+ blocked_by: [...child.blocked_by],
632
+ state: "pending",
633
+ reason: null,
634
+ commit: null,
635
+ lane: null,
636
+ qa_failures: 0,
637
+ })),
638
+ commits: [],
639
+ created_at: input.now,
640
+ updated_at: input.now,
641
+ };
642
+ }
643
+
644
+ /** Validate a captured v2 snapshot without reading or writing filesystem state. */
645
+ export function parseBatchLaneRunState(raw: string, batchId: string): BatchLaneRunStateRecord {
646
+ validateBatchId(batchId);
647
+ const parsed: unknown = JSON.parse(raw);
648
+ validateLaneRecordShape(parsed, batchId);
649
+ return withoutRetiredClock(parsed);
650
+ }
651
+
652
+ /** Parse either record version by its contract; an unknown contract is refused. */
653
+ export function parseAnyBatchRunState(raw: string, batchId: string): AnyBatchRunStateRecord {
654
+ validateBatchId(batchId);
655
+ const parsed: unknown = JSON.parse(raw);
656
+ if (typeof parsed === "object" && parsed !== null && (parsed as { contract?: unknown }).contract === "assurance_kernel/batch_run_state/v2")
657
+ return parseBatchLaneRunState(raw, batchId);
658
+ return parseBatchRunState(raw, batchId);
659
+ }
660
+
661
+ /** Read the persisted record of either version; null when the batch has none. */
662
+ export function readAnyBatchRunState(root: string, batchId: string): AnyBatchRunStateRecord | null {
663
+ const path = statePath(batchId);
664
+ if (!existsSync(join(root, path))) return null;
665
+ return parseAnyBatchRunState(readSecureProjectFile(root, path), batchId);
666
+ }
667
+
668
+ export function readBatchLaneRunState(root: string, batchId: string): BatchLaneRunStateRecord | null {
669
+ const path = statePath(batchId);
670
+ if (!existsSync(join(root, path))) return null;
671
+ return parseBatchLaneRunState(readSecureProjectFile(root, path), batchId);
672
+ }
673
+
674
+ export function writeBatchLaneRunState(root: string, record: BatchLaneRunStateRecord): BatchLaneRunStateRecord {
675
+ const path = statePath(record.batch_id);
676
+ validateLaneRecordShape(record, record.batch_id);
677
+ return withKernelStoreLock(root, () => {
678
+ const existing = existsSync(join(root, path)) ? readSecureProjectFile(root, path) : null;
679
+ if (existing !== null && existing === canonicalBytes(record)) return record;
680
+ const stored = withoutRetiredClock({ ...record, updated_at: new Date().toISOString() });
681
+ ensureSecureDirectory(root, join(".imm", "state", "batches"));
682
+ writeFileAtomically(root, path, canonicalBytes(stored));
683
+ return stored;
684
+ });
685
+ }
686
+
351
687
  export interface BatchRunReport {
352
688
  contract: "assurance_kernel/batch_run_report/v1";
353
689
  batch_id: string;
@@ -372,6 +708,50 @@ export interface BatchRunReport {
372
708
  created_at: string;
373
709
  }
374
710
 
711
+ /**
712
+ * The lane-mode report: the serial report's identity fields with lane children
713
+ * and `handoffs[]` in place of the single `handoff`. Handoffs are observations,
714
+ * never readiness.
715
+ */
716
+ export interface BatchLaneRunReport {
717
+ contract: "assurance_kernel/batch_run_report/v1";
718
+ batch_id: string;
719
+ initiative_slug: string;
720
+ batch_state: BatchRunState;
721
+ max_parallel: number;
722
+ children: BatchLaneChildRun[];
723
+ commits: string[];
724
+ reason: string | null;
725
+ handoffs: BatchLaneHandoff[];
726
+ /** Offers refused at admission; a refused offer wrote nothing. */
727
+ lane_refusals?: Array<{ task_id: string; path: string; reason: string }>;
728
+ next_action: string;
729
+ created_at: string;
730
+ }
731
+
732
+ export type BatchLaneHandoff =
733
+ | {
734
+ role: "lane-steward";
735
+ action: "provision";
736
+ task_id: string;
737
+ lane_branch: string;
738
+ base_head: string;
739
+ executor_hosts: readonly string[];
740
+ }
741
+ | { role: "lane-steward"; action: "release"; task_id: string; lane_branch: string }
742
+ | {
743
+ role: "executor";
744
+ task_id: string;
745
+ run_id: string | null;
746
+ record_revision: string;
747
+ next_obligation: import("../kernel/types").AssuranceObligation;
748
+ lane_branch: string;
749
+ /** Where the Executor Host must be rooted; an observation, never authority. */
750
+ lane_path: string;
751
+ };
752
+
753
+ type PersistableReport = Pick<BatchRunReport | BatchLaneRunReport, "contract" | "batch_id" | "batch_state">;
754
+
375
755
  function reportPath(batchId: string): string {
376
756
  validateBatchId(batchId);
377
757
  return join(".imm", "state", "batches", `${batchId}.report.json`);
@@ -380,7 +760,7 @@ function reportPath(batchId: string): string {
380
760
  /** Persist one current stop report per batch. Terminal reports are immutable;
381
761
  * a resumable needs_human report may be replaced by the later stop reached
382
762
  * after a fresh literal-user confirmation. */
383
- export function writeBatchRunReport(root: string, report: BatchRunReport): BatchRunReport {
763
+ export function writeBatchRunReport<T extends PersistableReport>(root: string, report: T): T {
384
764
  const relative = reportPath(report.batch_id);
385
765
  return withKernelStoreLock(root, () => {
386
766
  const path = join(root, relative);
@@ -392,7 +772,7 @@ export function writeBatchRunReport(root: string, report: BatchRunReport): Batch
392
772
  (original as BatchRunReport).contract !== "assurance_kernel/batch_run_report/v1"
393
773
  )
394
774
  throw new Error(`batch run report ${report.batch_id} has an unknown contract`);
395
- const prior = original as BatchRunReport;
775
+ const prior = original as T;
396
776
  if (canonicalReportBytes(prior) === canonicalReportBytes(report)) return prior;
397
777
  if (prior.batch_state !== "needs_human") return prior;
398
778
  }
@@ -402,11 +782,17 @@ export function writeBatchRunReport(root: string, report: BatchRunReport): Batch
402
782
  });
403
783
  }
404
784
 
405
- function canonicalReportBytes(report: BatchRunReport): string {
785
+ function canonicalReportBytes(report: PersistableReport): string {
406
786
  return `${JSON.stringify(report, null, 2)}\n`;
407
787
  }
408
788
 
409
789
  /** All terminal batch states; needs_human is a park, not terminal. */
410
790
  export function isTerminalBatchState(state: BatchRunState): boolean {
411
- return state === "completed" || state === "budget_stopped" || state === "failed" || state === "rejected";
791
+ return (
792
+ state === "completed" ||
793
+ state === "budget_stopped" ||
794
+ state === "failed" ||
795
+ state === "rejected" ||
796
+ state === "superseded"
797
+ );
412
798
  }
@@ -1,4 +1,5 @@
1
1
  import type { GithubInitiativeObservation } from "../github_issue_tracker";
2
+ import type { LocalInitiativeObservation } from "../local_initiative";
2
3
 
3
4
  export interface BatchPlanBudget {
4
5
  max_children: number;
@@ -49,15 +50,30 @@ export interface BatchPlanChild {
49
50
  intent_content_hash: string | null;
50
51
  }
51
52
 
53
+ /** Start waves of enrollable children: each is ready and pairwise scope-disjoint. */
54
+ export type BatchPlanParallelGroups = string[][];
55
+
56
+ /**
57
+ * A ready child held out of a start wave because its scope cannot be proved
58
+ * disjoint from the listed children, so it serializes after them.
59
+ */
60
+ export interface BatchPlanScopeConflict {
61
+ task_id: string;
62
+ overlaps_with: string[];
63
+ }
64
+
52
65
  export interface BatchPlan {
53
66
  contract: "assurance_kernel/batch_plan/v1";
54
67
  initiative_slug: string;
55
68
  confirmation_time: string;
56
- tracker_observation: GithubInitiativeObservation;
69
+ tracker_observation: InitiativeObservation;
57
70
  children: BatchPlanChild[];
58
71
  enrollable: BatchPlanDigestChild[];
59
72
  plan_digest: string;
60
73
  budget: BatchPlanBudget;
74
+ /** Additive projection; not part of `plan_digest`. */
75
+ parallel_groups: BatchPlanParallelGroups;
76
+ scope_conflicts: BatchPlanScopeConflict[];
61
77
  }
62
78
 
63
79
  export interface ProjectBatchPlanInput {
@@ -65,7 +81,10 @@ export interface ProjectBatchPlanInput {
65
81
  budget?: BatchPlanBudgetInput;
66
82
  }
67
83
 
84
+ /** What either Initiative carrier yields: the Tasks, their Slices, and their dependencies. */
85
+ export type InitiativeObservation = GithubInitiativeObservation | LocalInitiativeObservation;
86
+
68
87
  export type InitiativeObservationReader = (
69
88
  root: string,
70
89
  initiativeSlug: string,
71
- ) => Promise<GithubInitiativeObservation>;
90
+ ) => Promise<InitiativeObservation>;
@@ -25,6 +25,10 @@ or all references as an entry prerequisite.
25
25
 
26
26
  - common: [Shared Guards](../../dist/BASELINE.md#shared-guards), [Workflow Activation](../../dist/BASELINE.md#workflow-activation), [Host Confirmation Boundary](../../dist/BASELINE.md#host-confirmation-boundary), [Kernel Canary Routing and Authority](../../dist/imm-run.md#kernel-canary-routing-and-authority)
27
27
  - unattended batch run, or any question about whether `imm-run` starts one: [Unattended Batch Opt-In](../../dist/imm-run.md#unattended-batch-opt-in)
28
+ - parallel batch (`max_parallel`, `lane_offers`, Lane admission or integration): [Parallel Batch Opt-In](../../dist/imm-run.md#parallel-batch-opt-in)
29
+ - a lane-mode report carries an `executor` handoff, or a Lane's Executor Host session has ended: [Lane Executor Supervision](../../dist/imm-run.md#lane-executor-supervision)
30
+ - that Parent runs inside Herdr (`HERDR_ENV=1`): [Herdr Lane Tabs](../../dist/imm-run.md#herdr-lane-tabs)
31
+ - a lane-mode batch is about to start, to resolve the repository's parallelism, Executor Host, model and effort: [Lane Preferences](../../dist/imm-run.md#lane-preferences)
28
32
  - steady execution: [Verification and Local Recovery](../../dist/BASELINE.md#verification-and-local-recovery), [Execution Loop](../../dist/imm-run.md#execution-loop), [Observable Output](../../dist/imm-run.md#observable-output)
29
33
  - rework, scope expansion, breaking revision, user decision, stop, interruption or unknown state before any action: [Decisions and Recovery](../../dist/imm-run.md#decisions-and-recovery), [Failure Output](../../dist/imm-run.md#failure-output); re-read `status`, then the pending obligation. Rework submits the verdict before editing, and an uncertain interruption resumes from exact task/run authority facts rather than a summary.
30
34
  - review or post-settlement learning: [Review and Learning](../../dist/imm-run.md#review-and-learning)