immune-brain 5.3.0 → 5.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/README.zh-CN.md +4 -4
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +54 -70
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +163 -28
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +71 -10
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +1 -1
- package/plugins/immune-brain/.pi-extension/pi-run-continuation.ts +161 -0
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -2
- package/plugins/immune-brain/dist/BASELINE.md +13 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +16130 -12734
- package/plugins/immune-brain/dist/docs/reference/code-quality-guard.md +3 -1
- package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +41 -1
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/imm-planner.md +57 -16
- package/plugins/immune-brain/dist/imm-run-batch.md +398 -0
- package/plugins/immune-brain/dist/imm-run.md +89 -344
- package/plugins/immune-brain/dist/role-prompts/code-review.md +26 -2
- package/plugins/immune-brain/dist/role-prompts/executor.md +35 -10
- package/plugins/immune-brain/hooks/hooks.json +21 -1
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +203 -13
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +2 -1
- package/plugins/immune-brain/runtime/assurance/enrollment_git_base.ts +99 -14
- package/plugins/immune-brain/runtime/assurance/phase_entry.ts +1 -7
- package/plugins/immune-brain/runtime/assurance/qa.ts +126 -43
- package/plugins/immune-brain/runtime/assurance/qa_findings.ts +8 -0
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +48 -0
- package/plugins/immune-brain/runtime/assurance/run_continuation.ts +284 -0
- package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +46 -4
- package/plugins/immune-brain/runtime/assurance/verification.ts +83 -35
- package/plugins/immune-brain/runtime/assurance/workflow_autocommit.ts +222 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +11 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +199 -48
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +8 -15
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +105 -9
- package/plugins/immune-brain/runtime/claude/run_continuation.ts +274 -0
- package/plugins/immune-brain/runtime/commands/kernel.ts +41 -103
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +60 -2
- package/plugins/immune-brain/runtime/kernel/application.ts +4 -1
- package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +103 -20
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +57 -7
- package/plugins/immune-brain/runtime/kernel/completion.ts +28 -4
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +48 -8
- package/plugins/immune-brain/runtime/kernel/intent.ts +169 -104
- package/plugins/immune-brain/runtime/kernel/reducer.ts +136 -6
- package/plugins/immune-brain/runtime/kernel/risk_floor.ts +286 -0
- package/plugins/immune-brain/runtime/kernel/storage_layout_migration.ts +83 -1
- package/plugins/immune-brain/runtime/kernel/storage_paths.ts +1 -1
- package/plugins/immune-brain/runtime/kernel/types.ts +49 -3
- package/plugins/immune-brain/runtime/kernel/validation.ts +147 -7
- package/plugins/immune-brain/runtime/loop_contract.ts +5 -0
- package/plugins/immune-brain/runtime/plan_core.ts +15 -8
- package/plugins/immune-brain/runtime/plugin_source_skew.ts +58 -0
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +26 -2
- package/plugins/immune-brain/runtime/prompts/executor.md +35 -10
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +5 -3
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +40 -10
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +57 -3
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +424 -66
- package/plugins/immune-brain/runtime/unattended/batch_integration.ts +2 -1
- package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +13 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +43 -24
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +6 -2
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +102 -41
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +9 -3
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +9 -9
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +90 -32
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +102 -4
- package/plugins/immune-brain/runtime/unattended/parent_delivery_consolidation.ts +248 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +126 -18
- package/plugins/immune-brain/skills/BASELINE.md +13 -1
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +5 -0
- 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
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
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
|
-
|
|
209
|
-
record.children.
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, () => {
|