immune-brain 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +223 -12
- package/README.zh-CN.md +228 -11
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +1 -0
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +24 -3
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +1339 -66
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +27 -0
- package/plugins/immune-brain/dist/imm-run.md +198 -3
- package/plugins/immune-brain/dist/role-prompts/lane-steward.md +44 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +6 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +16 -4
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +24 -2
- package/plugins/immune-brain/runtime/claude/role_agents.ts +4 -3
- package/plugins/immune-brain/runtime/local_initiative.ts +94 -0
- package/plugins/immune-brain/runtime/loop_contract.ts +55 -2
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/lane-steward.md +44 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +15 -2
- package/plugins/immune-brain/runtime/unattended/batch_integration.ts +387 -0
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +2 -2
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +909 -0
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +36 -11
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +41 -22
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +20 -1
- package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +162 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +356 -8
- package/plugins/immune-brain/runtime/unattended/types.ts +21 -2
- package/plugins/immune-brain/skills/imm-run/SKILL.md +4 -0
|
@@ -64,6 +64,76 @@ export interface BatchRunStateRecord {
|
|
|
64
64
|
updated_at: string;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Lane-mode child states. `integrated` plays the role `committed` plays in the
|
|
69
|
+
* serial record: it is what unblocks dependents. `released` is recorded by the
|
|
70
|
+
* lane steward once an integrated Lane is gone.
|
|
71
|
+
*/
|
|
72
|
+
export type BatchLaneChildRunState =
|
|
73
|
+
| "pending"
|
|
74
|
+
| "lane_admitted"
|
|
75
|
+
| "enrolled"
|
|
76
|
+
| "settled"
|
|
77
|
+
| "lane_committed"
|
|
78
|
+
| "integrated"
|
|
79
|
+
| "released"
|
|
80
|
+
| "needs_human"
|
|
81
|
+
| "skipped_blocked";
|
|
82
|
+
|
|
83
|
+
export interface BatchLaneBinding {
|
|
84
|
+
/** Absolute, real path of the admitted Lane worktree. */
|
|
85
|
+
path: string;
|
|
86
|
+
branch: string;
|
|
87
|
+
/** The batch head the Lane was provisioned at and the child was enrolled on. */
|
|
88
|
+
base_head: string;
|
|
89
|
+
/** Commit created in the Lane after the Lane Kernel settled the child. */
|
|
90
|
+
lane_commit: string | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface BatchLaneChildRun {
|
|
94
|
+
task_id: string;
|
|
95
|
+
slice_id: string;
|
|
96
|
+
blocked_by: string[];
|
|
97
|
+
state: BatchLaneChildRunState;
|
|
98
|
+
reason: string | null;
|
|
99
|
+
/** Commit on the batch branch once this child is integrated. */
|
|
100
|
+
commit: string | null;
|
|
101
|
+
lane: BatchLaneBinding | null;
|
|
102
|
+
qa_failures: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Lane-mode batch state. A serial batch never writes this contract; a v1
|
|
107
|
+
* record is never upgraded. Every v1-gated reader skips it by contract.
|
|
108
|
+
*/
|
|
109
|
+
export interface BatchLaneRunStateRecord {
|
|
110
|
+
contract: "assurance_kernel/batch_run_state/v2";
|
|
111
|
+
batch_id: string;
|
|
112
|
+
initiative_slug: string;
|
|
113
|
+
plan_digest: string;
|
|
114
|
+
base_head: string;
|
|
115
|
+
branch?: string;
|
|
116
|
+
confirmation_time: string;
|
|
117
|
+
budget: {
|
|
118
|
+
max_children: number;
|
|
119
|
+
qa_failure_limit: number;
|
|
120
|
+
};
|
|
121
|
+
max_parallel: number;
|
|
122
|
+
batch_state: BatchRunState;
|
|
123
|
+
children: BatchLaneChildRun[];
|
|
124
|
+
/** Batch-branch commits produced by integration, in integration order. */
|
|
125
|
+
commits: string[];
|
|
126
|
+
adopted_heads?: Array<{ from: string; to: string }>;
|
|
127
|
+
created_at: string;
|
|
128
|
+
updated_at: string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type AnyBatchRunStateRecord = BatchRunStateRecord | BatchLaneRunStateRecord;
|
|
132
|
+
|
|
133
|
+
export function isLaneBatchRecord(record: AnyBatchRunStateRecord): record is BatchLaneRunStateRecord {
|
|
134
|
+
return record.contract === "assurance_kernel/batch_run_state/v2";
|
|
135
|
+
}
|
|
136
|
+
|
|
67
137
|
const BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
68
138
|
|
|
69
139
|
const CHILD_RUN_STATES: ReadonlySet<string> = new Set([
|
|
@@ -108,13 +178,13 @@ function statePath(batchId: string): string {
|
|
|
108
178
|
* `authorization_expires_at` and `budget.deadline_at`. They are ignored on read
|
|
109
179
|
* and never written back.
|
|
110
180
|
*/
|
|
111
|
-
function withoutRetiredClock(record:
|
|
112
|
-
const { authorization_expires_at: _expiry, ...rest } = record as
|
|
113
|
-
const { deadline_at: _deadline, ...budget } = rest.budget as
|
|
114
|
-
return { ...rest, budget };
|
|
181
|
+
function withoutRetiredClock<T extends AnyBatchRunStateRecord>(record: T): T {
|
|
182
|
+
const { authorization_expires_at: _expiry, ...rest } = record as T & { authorization_expires_at?: unknown };
|
|
183
|
+
const { deadline_at: _deadline, ...budget } = rest.budget as T["budget"] & { deadline_at?: unknown };
|
|
184
|
+
return { ...rest, budget } as unknown as T;
|
|
115
185
|
}
|
|
116
186
|
|
|
117
|
-
function canonicalBytes(record:
|
|
187
|
+
function canonicalBytes(record: AnyBatchRunStateRecord): string {
|
|
118
188
|
return `${JSON.stringify(withoutRetiredClock(record), null, 2)}\n`;
|
|
119
189
|
}
|
|
120
190
|
|
|
@@ -348,6 +418,240 @@ export function writeBatchRunState(
|
|
|
348
418
|
});
|
|
349
419
|
}
|
|
350
420
|
|
|
421
|
+
|
|
422
|
+
const LANE_CHILD_STATES: ReadonlySet<string> = new Set([
|
|
423
|
+
"pending",
|
|
424
|
+
"lane_admitted",
|
|
425
|
+
"enrolled",
|
|
426
|
+
"settled",
|
|
427
|
+
"lane_committed",
|
|
428
|
+
"integrated",
|
|
429
|
+
"released",
|
|
430
|
+
"needs_human",
|
|
431
|
+
"skipped_blocked",
|
|
432
|
+
]);
|
|
433
|
+
|
|
434
|
+
/** Lane-mode states whose child must carry a Lane binding. */
|
|
435
|
+
const LANE_BOUND_STATES: ReadonlySet<string> = new Set([
|
|
436
|
+
"lane_admitted",
|
|
437
|
+
"enrolled",
|
|
438
|
+
"settled",
|
|
439
|
+
"lane_committed",
|
|
440
|
+
"integrated",
|
|
441
|
+
"released",
|
|
442
|
+
]);
|
|
443
|
+
|
|
444
|
+
function validateLaneRecordShape(value: unknown, batchId: string): asserts value is BatchLaneRunStateRecord {
|
|
445
|
+
if (typeof value !== "object" || value === null)
|
|
446
|
+
throw new Error(`batch run state ${batchId} is not an object`);
|
|
447
|
+
const record = value as Record<string, unknown>;
|
|
448
|
+
if (record.contract !== "assurance_kernel/batch_run_state/v2")
|
|
449
|
+
throw new Error(`batch run state ${batchId} has an unknown contract`);
|
|
450
|
+
if (record.batch_id !== batchId)
|
|
451
|
+
throw new Error(`batch run state ${batchId} carries batch_id ${String(record.batch_id)}`);
|
|
452
|
+
if (typeof record.plan_digest !== "string" || !record.plan_digest)
|
|
453
|
+
throw new Error(`batch run state ${batchId} has an invalid plan_digest`);
|
|
454
|
+
if (typeof record.base_head !== "string" || !record.base_head)
|
|
455
|
+
throw new Error(`batch run state ${batchId} has an invalid base_head`);
|
|
456
|
+
if (record.branch !== undefined && (typeof record.branch !== "string" || !record.branch))
|
|
457
|
+
throw new Error(`batch run state ${batchId} has an invalid branch`);
|
|
458
|
+
if (!isCanonicalTimestamp(record.confirmation_time))
|
|
459
|
+
throw new Error(`batch run state ${batchId} has an invalid confirmation_time`);
|
|
460
|
+
if (!isCanonicalTimestamp(record.created_at) || !isCanonicalTimestamp(record.updated_at))
|
|
461
|
+
throw new Error(`batch run state ${batchId} has invalid state timestamps`);
|
|
462
|
+
if (!Array.isArray(record.children) || record.children.length === 0)
|
|
463
|
+
throw new Error(`batch run state ${batchId} has no children`);
|
|
464
|
+
if (!BATCH_RUN_STATES.has(String(record.batch_state)))
|
|
465
|
+
throw new Error(`batch run state ${batchId} has an invalid batch_state`);
|
|
466
|
+
if (typeof record.max_parallel !== "number" || !Number.isSafeInteger(record.max_parallel) || record.max_parallel <= 0)
|
|
467
|
+
throw new Error(`batch run state ${batchId} has an invalid max_parallel`);
|
|
468
|
+
const budget = record.budget as Record<string, unknown>;
|
|
469
|
+
if (
|
|
470
|
+
typeof record.budget !== "object" ||
|
|
471
|
+
record.budget === null ||
|
|
472
|
+
typeof budget.max_children !== "number" ||
|
|
473
|
+
!Number.isInteger(budget.max_children) ||
|
|
474
|
+
budget.max_children <= 0 ||
|
|
475
|
+
typeof budget.qa_failure_limit !== "number" ||
|
|
476
|
+
!Number.isInteger(budget.qa_failure_limit) ||
|
|
477
|
+
budget.qa_failure_limit <= 0
|
|
478
|
+
)
|
|
479
|
+
throw new Error(`batch run state ${batchId} has an invalid budget`);
|
|
480
|
+
if (!Array.isArray(record.commits) || record.commits.some((c) => typeof c !== "string"))
|
|
481
|
+
throw new Error(`batch run state ${batchId} has an invalid commits list`);
|
|
482
|
+
if (
|
|
483
|
+
record.adopted_heads !== undefined &&
|
|
484
|
+
(!Array.isArray(record.adopted_heads) ||
|
|
485
|
+
record.adopted_heads.some(
|
|
486
|
+
(a: unknown) =>
|
|
487
|
+
typeof a !== "object" ||
|
|
488
|
+
a === null ||
|
|
489
|
+
typeof (a as { from?: unknown }).from !== "string" ||
|
|
490
|
+
!(a as { from: string }).from ||
|
|
491
|
+
typeof (a as { to?: unknown }).to !== "string" ||
|
|
492
|
+
!(a as { to: string }).to,
|
|
493
|
+
))
|
|
494
|
+
)
|
|
495
|
+
throw new Error(`batch run state ${batchId} has an invalid adopted_heads list`);
|
|
496
|
+
const seenTaskIds = new Set<string>();
|
|
497
|
+
for (const child of record.children) {
|
|
498
|
+
if (
|
|
499
|
+
typeof child !== "object" ||
|
|
500
|
+
child === null ||
|
|
501
|
+
typeof child.task_id !== "string" ||
|
|
502
|
+
!child.task_id ||
|
|
503
|
+
typeof child.slice_id !== "string" ||
|
|
504
|
+
!child.slice_id
|
|
505
|
+
)
|
|
506
|
+
throw new Error(`batch run state ${batchId} has an invalid child entry`);
|
|
507
|
+
if (!LANE_CHILD_STATES.has(String(child.state)))
|
|
508
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid state`);
|
|
509
|
+
if (!Array.isArray(child.blocked_by) || child.blocked_by.some((b: unknown) => typeof b !== "string"))
|
|
510
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid blocked_by`);
|
|
511
|
+
if (
|
|
512
|
+
(child.reason !== null && typeof child.reason !== "string") ||
|
|
513
|
+
(child.commit !== null && typeof child.commit !== "string")
|
|
514
|
+
)
|
|
515
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has invalid terminal fields`);
|
|
516
|
+
if (typeof child.qa_failures !== "number" || !Number.isInteger(child.qa_failures) || child.qa_failures < 0)
|
|
517
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid qa_failures`);
|
|
518
|
+
const lane = child.lane as BatchLaneBinding | null | undefined;
|
|
519
|
+
if (lane !== null) {
|
|
520
|
+
if (
|
|
521
|
+
typeof lane !== "object" ||
|
|
522
|
+
typeof lane.path !== "string" ||
|
|
523
|
+
!lane.path ||
|
|
524
|
+
typeof lane.branch !== "string" ||
|
|
525
|
+
!lane.branch ||
|
|
526
|
+
typeof lane.base_head !== "string" ||
|
|
527
|
+
!lane.base_head ||
|
|
528
|
+
(lane.lane_commit !== null && typeof lane.lane_commit !== "string")
|
|
529
|
+
)
|
|
530
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid lane`);
|
|
531
|
+
}
|
|
532
|
+
if (LANE_BOUND_STATES.has(child.state) && lane === null)
|
|
533
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a lane`);
|
|
534
|
+
if ((child.state === "pending" || child.state === "skipped_blocked") && lane !== null)
|
|
535
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} but holds a lane`);
|
|
536
|
+
const integrated = child.state === "integrated" || child.state === "released";
|
|
537
|
+
if (integrated && child.commit === null)
|
|
538
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a commit`);
|
|
539
|
+
if (!integrated && child.commit !== null)
|
|
540
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has a commit but is not integrated`);
|
|
541
|
+
if (integrated && lane?.lane_commit === null)
|
|
542
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a lane commit`);
|
|
543
|
+
if (child.state === "lane_committed" && lane?.lane_commit === null)
|
|
544
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} is lane_committed without a lane commit`);
|
|
545
|
+
if ((child.state === "needs_human" || child.state === "skipped_blocked") && child.reason === null)
|
|
546
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} needs a terminal reason`);
|
|
547
|
+
if (seenTaskIds.has(child.task_id))
|
|
548
|
+
throw new Error(`batch run state ${batchId} has a duplicate child ${child.task_id}`);
|
|
549
|
+
seenTaskIds.add(child.task_id);
|
|
550
|
+
}
|
|
551
|
+
for (const child of record.children) {
|
|
552
|
+
if (child.commit !== null && !record.commits.includes(child.commit))
|
|
553
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} commit is missing from commits`);
|
|
554
|
+
}
|
|
555
|
+
const inFlight = (child: { state: string }) =>
|
|
556
|
+
child.state === "lane_admitted" ||
|
|
557
|
+
child.state === "enrolled" ||
|
|
558
|
+
child.state === "settled" ||
|
|
559
|
+
child.state === "lane_committed";
|
|
560
|
+
if (
|
|
561
|
+
(record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected") &&
|
|
562
|
+
record.children.some(inFlight)
|
|
563
|
+
)
|
|
564
|
+
throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
|
|
565
|
+
if (
|
|
566
|
+
record.batch_state === "completed" &&
|
|
567
|
+
record.children.some((child) => child.state !== "integrated" && child.state !== "released")
|
|
568
|
+
)
|
|
569
|
+
throw new Error(`batch run state ${batchId} is completed but a child is not integrated`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
export function prepareBatchLaneRunState(input: {
|
|
573
|
+
batch_id: string;
|
|
574
|
+
initiative_slug: string;
|
|
575
|
+
children: BatchPlanChild[];
|
|
576
|
+
plan_digest: string;
|
|
577
|
+
base_head: string;
|
|
578
|
+
branch?: string;
|
|
579
|
+
confirmation_time: string;
|
|
580
|
+
budget: { max_children: number; qa_failure_limit: number };
|
|
581
|
+
max_parallel: number;
|
|
582
|
+
now: string;
|
|
583
|
+
}): BatchLaneRunStateRecord {
|
|
584
|
+
validateBatchId(input.batch_id);
|
|
585
|
+
return {
|
|
586
|
+
contract: "assurance_kernel/batch_run_state/v2",
|
|
587
|
+
batch_id: input.batch_id,
|
|
588
|
+
initiative_slug: input.initiative_slug,
|
|
589
|
+
plan_digest: input.plan_digest,
|
|
590
|
+
base_head: input.base_head,
|
|
591
|
+
branch: input.branch ?? `imm/${input.initiative_slug}`,
|
|
592
|
+
confirmation_time: input.confirmation_time,
|
|
593
|
+
budget: input.budget,
|
|
594
|
+
max_parallel: input.max_parallel,
|
|
595
|
+
batch_state: "prepared",
|
|
596
|
+
children: input.children.map((child) => ({
|
|
597
|
+
task_id: child.task_id,
|
|
598
|
+
slice_id: child.slice_id,
|
|
599
|
+
blocked_by: [...child.blocked_by],
|
|
600
|
+
state: "pending",
|
|
601
|
+
reason: null,
|
|
602
|
+
commit: null,
|
|
603
|
+
lane: null,
|
|
604
|
+
qa_failures: 0,
|
|
605
|
+
})),
|
|
606
|
+
commits: [],
|
|
607
|
+
created_at: input.now,
|
|
608
|
+
updated_at: input.now,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
/** Validate a captured v2 snapshot without reading or writing filesystem state. */
|
|
613
|
+
export function parseBatchLaneRunState(raw: string, batchId: string): BatchLaneRunStateRecord {
|
|
614
|
+
validateBatchId(batchId);
|
|
615
|
+
const parsed: unknown = JSON.parse(raw);
|
|
616
|
+
validateLaneRecordShape(parsed, batchId);
|
|
617
|
+
return withoutRetiredClock(parsed);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/** Parse either record version by its contract; an unknown contract is refused. */
|
|
621
|
+
export function parseAnyBatchRunState(raw: string, batchId: string): AnyBatchRunStateRecord {
|
|
622
|
+
validateBatchId(batchId);
|
|
623
|
+
const parsed: unknown = JSON.parse(raw);
|
|
624
|
+
if (typeof parsed === "object" && parsed !== null && (parsed as { contract?: unknown }).contract === "assurance_kernel/batch_run_state/v2")
|
|
625
|
+
return parseBatchLaneRunState(raw, batchId);
|
|
626
|
+
return parseBatchRunState(raw, batchId);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Read the persisted record of either version; null when the batch has none. */
|
|
630
|
+
export function readAnyBatchRunState(root: string, batchId: string): AnyBatchRunStateRecord | null {
|
|
631
|
+
const path = statePath(batchId);
|
|
632
|
+
if (!existsSync(join(root, path))) return null;
|
|
633
|
+
return parseAnyBatchRunState(readSecureProjectFile(root, path), batchId);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export function readBatchLaneRunState(root: string, batchId: string): BatchLaneRunStateRecord | null {
|
|
637
|
+
const path = statePath(batchId);
|
|
638
|
+
if (!existsSync(join(root, path))) return null;
|
|
639
|
+
return parseBatchLaneRunState(readSecureProjectFile(root, path), batchId);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
export function writeBatchLaneRunState(root: string, record: BatchLaneRunStateRecord): BatchLaneRunStateRecord {
|
|
643
|
+
const path = statePath(record.batch_id);
|
|
644
|
+
validateLaneRecordShape(record, record.batch_id);
|
|
645
|
+
return withKernelStoreLock(root, () => {
|
|
646
|
+
const existing = existsSync(join(root, path)) ? readSecureProjectFile(root, path) : null;
|
|
647
|
+
if (existing !== null && existing === canonicalBytes(record)) return record;
|
|
648
|
+
const stored = withoutRetiredClock({ ...record, updated_at: new Date().toISOString() });
|
|
649
|
+
ensureSecureDirectory(root, join(".imm", "state", "batches"));
|
|
650
|
+
writeFileAtomically(root, path, canonicalBytes(stored));
|
|
651
|
+
return stored;
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
|
|
351
655
|
export interface BatchRunReport {
|
|
352
656
|
contract: "assurance_kernel/batch_run_report/v1";
|
|
353
657
|
batch_id: string;
|
|
@@ -372,6 +676,50 @@ export interface BatchRunReport {
|
|
|
372
676
|
created_at: string;
|
|
373
677
|
}
|
|
374
678
|
|
|
679
|
+
/**
|
|
680
|
+
* The lane-mode report: the serial report's identity fields with lane children
|
|
681
|
+
* and `handoffs[]` in place of the single `handoff`. Handoffs are observations,
|
|
682
|
+
* never readiness.
|
|
683
|
+
*/
|
|
684
|
+
export interface BatchLaneRunReport {
|
|
685
|
+
contract: "assurance_kernel/batch_run_report/v1";
|
|
686
|
+
batch_id: string;
|
|
687
|
+
initiative_slug: string;
|
|
688
|
+
batch_state: BatchRunState;
|
|
689
|
+
max_parallel: number;
|
|
690
|
+
children: BatchLaneChildRun[];
|
|
691
|
+
commits: string[];
|
|
692
|
+
reason: string | null;
|
|
693
|
+
handoffs: BatchLaneHandoff[];
|
|
694
|
+
/** Offers refused at admission; a refused offer wrote nothing. */
|
|
695
|
+
lane_refusals?: Array<{ task_id: string; path: string; reason: string }>;
|
|
696
|
+
next_action: string;
|
|
697
|
+
created_at: string;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
export type BatchLaneHandoff =
|
|
701
|
+
| {
|
|
702
|
+
role: "lane-steward";
|
|
703
|
+
action: "provision";
|
|
704
|
+
task_id: string;
|
|
705
|
+
lane_branch: string;
|
|
706
|
+
base_head: string;
|
|
707
|
+
executor_hosts: readonly string[];
|
|
708
|
+
}
|
|
709
|
+
| { role: "lane-steward"; action: "release"; task_id: string; lane_branch: string }
|
|
710
|
+
| {
|
|
711
|
+
role: "executor";
|
|
712
|
+
task_id: string;
|
|
713
|
+
run_id: string | null;
|
|
714
|
+
record_revision: string;
|
|
715
|
+
next_obligation: import("../kernel/types").AssuranceObligation;
|
|
716
|
+
lane_branch: string;
|
|
717
|
+
/** Where the Executor Host must be rooted; an observation, never authority. */
|
|
718
|
+
lane_path: string;
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
type PersistableReport = Pick<BatchRunReport | BatchLaneRunReport, "contract" | "batch_id" | "batch_state">;
|
|
722
|
+
|
|
375
723
|
function reportPath(batchId: string): string {
|
|
376
724
|
validateBatchId(batchId);
|
|
377
725
|
return join(".imm", "state", "batches", `${batchId}.report.json`);
|
|
@@ -380,7 +728,7 @@ function reportPath(batchId: string): string {
|
|
|
380
728
|
/** Persist one current stop report per batch. Terminal reports are immutable;
|
|
381
729
|
* a resumable needs_human report may be replaced by the later stop reached
|
|
382
730
|
* after a fresh literal-user confirmation. */
|
|
383
|
-
export function writeBatchRunReport(root: string, report:
|
|
731
|
+
export function writeBatchRunReport<T extends PersistableReport>(root: string, report: T): T {
|
|
384
732
|
const relative = reportPath(report.batch_id);
|
|
385
733
|
return withKernelStoreLock(root, () => {
|
|
386
734
|
const path = join(root, relative);
|
|
@@ -392,7 +740,7 @@ export function writeBatchRunReport(root: string, report: BatchRunReport): Batch
|
|
|
392
740
|
(original as BatchRunReport).contract !== "assurance_kernel/batch_run_report/v1"
|
|
393
741
|
)
|
|
394
742
|
throw new Error(`batch run report ${report.batch_id} has an unknown contract`);
|
|
395
|
-
const prior = original as
|
|
743
|
+
const prior = original as T;
|
|
396
744
|
if (canonicalReportBytes(prior) === canonicalReportBytes(report)) return prior;
|
|
397
745
|
if (prior.batch_state !== "needs_human") return prior;
|
|
398
746
|
}
|
|
@@ -402,7 +750,7 @@ export function writeBatchRunReport(root: string, report: BatchRunReport): Batch
|
|
|
402
750
|
});
|
|
403
751
|
}
|
|
404
752
|
|
|
405
|
-
function canonicalReportBytes(report:
|
|
753
|
+
function canonicalReportBytes(report: PersistableReport): string {
|
|
406
754
|
return `${JSON.stringify(report, null, 2)}\n`;
|
|
407
755
|
}
|
|
408
756
|
|
|
@@ -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:
|
|
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<
|
|
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)
|