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.
- 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 +18 -5
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +189 -3
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +66 -3
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +1885 -149
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +27 -0
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +8 -6
- package/plugins/immune-brain/dist/imm-planner.md +4 -1
- package/plugins/immune-brain/dist/imm-run.md +198 -3
- package/plugins/immune-brain/dist/role-prompts/code-review.md +10 -0
- package/plugins/immune-brain/dist/role-prompts/lane-steward.md +44 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +22 -4
- package/plugins/immune-brain/runtime/assurance/review_mediation.ts +107 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +3 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +16 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +105 -58
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +89 -16
- package/plugins/immune-brain/runtime/claude/role_agents.ts +4 -3
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +9 -2
- package/plugins/immune-brain/runtime/kernel/completion.ts +24 -1
- package/plugins/immune-brain/runtime/kernel/reducer.ts +15 -2
- package/plugins/immune-brain/runtime/kernel/spec_binding.ts +6 -0
- package/plugins/immune-brain/runtime/kernel/storage.ts +20 -4
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +33 -3
- package/plugins/immune-brain/runtime/kernel/types.ts +7 -0
- package/plugins/immune-brain/runtime/kernel/validation.ts +13 -2
- 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/code-review.md +10 -0
- package/plugins/immune-brain/runtime/prompts/lane-steward.md +44 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +16 -3
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +222 -0
- 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 +946 -0
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +36 -11
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +48 -33
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +13 -1
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +22 -1
- package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +162 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +398 -12
- package/plugins/immune-brain/runtime/unattended/types.ts +21 -2
- 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:
|
|
112
|
-
const { authorization_expires_at: _expiry, ...rest } = record as
|
|
113
|
-
const { deadline_at: _deadline, ...budget } = rest.budget as
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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)
|