immune-brain 5.1.0 → 5.1.2
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/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +37 -5
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +230 -10
- package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +66 -3
- package/plugins/immune-brain/agents/immune-brain-reviewer.md +26 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +8712 -7410
- package/plugins/immune-brain/dist/docs/reference/planning-quality-gate.md +1 -1
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +8 -6
- package/plugins/immune-brain/dist/imm-brainstorm.md +4 -4
- package/plugins/immune-brain/dist/imm-planner.md +41 -5
- package/plugins/immune-brain/dist/imm-run.md +123 -9
- package/plugins/immune-brain/dist/role-prompts/code-review.md +25 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +7 -0
- package/plugins/immune-brain/hooks/hooks.json +22 -1
- package/plugins/immune-brain/runtime/assurance/active_task_notice.ts +42 -0
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +31 -4
- package/plugins/immune-brain/runtime/assurance/delivery_workspace.ts +4 -1
- 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 +15 -0
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +120 -59
- package/plugins/immune-brain/runtime/claude/lane_guard.ts +33 -0
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +142 -31
- package/plugins/immune-brain/runtime/commands/kernel.ts +54 -1
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +235 -7
- 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/runtime_contracts.json +7 -0
- package/plugins/immune-brain/runtime/kernel/runtime_contracts.ts +75 -0
- 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/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +25 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +7 -0
- package/plugins/immune-brain/runtime/role_prompt_bridge.ts +1 -1
- package/plugins/immune-brain/runtime/unattended/batch_delegation.ts +160 -0
- package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +222 -0
- package/plugins/immune-brain/runtime/unattended/batch_final_verification.ts +72 -0
- package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +447 -37
- package/plugins/immune-brain/runtime/unattended/batch_leak_restore.ts +131 -0
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +33 -16
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +16 -3
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +44 -0
- package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +40 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +199 -6
- package/plugins/immune-brain/runtime/unattended/lane_workspace.ts +58 -0
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +1 -1
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +3 -1
|
@@ -6,18 +6,33 @@
|
|
|
6
6
|
// deletes a Git worktree and never names a workspace tool.
|
|
7
7
|
// Defined by docs/specs/parallel-batch-lanes.spec.md and ADR 0013.
|
|
8
8
|
import { spawnSync } from "node:child_process";
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
9
10
|
import { existsSync, realpathSync } from "node:fs";
|
|
10
11
|
import { isAbsolute, join } from "node:path";
|
|
11
12
|
import { computeBatchPlanDigest } from "../kernel/batch_authority";
|
|
13
|
+
import { readTaskRecordRaw } from "../kernel/storage";
|
|
12
14
|
import { readTaskIntent } from "../kernel/intent";
|
|
13
|
-
import
|
|
15
|
+
import { readTaskTombstone } from "../kernel/backend_claim";
|
|
16
|
+
import {
|
|
17
|
+
readRuntimeContracts,
|
|
18
|
+
runningRuntimeSource,
|
|
19
|
+
RUNTIME_CONTRACTS,
|
|
20
|
+
runtimeContractDifferences,
|
|
21
|
+
} from "../kernel/runtime_contracts";
|
|
22
|
+
import { readFileSync } from "node:fs";
|
|
23
|
+
import { runFinalVerification, type FinalVerificationReport } from "./batch_final_verification";
|
|
24
|
+
import { applyDelegatedIntentRevision, delegatedConfirmationRef, delegatedRevisionRefusal } from "./batch_delegation";
|
|
25
|
+
import { runGithubTrackerOperation } from "../github_issue_tracker";
|
|
26
|
+
import { hasLocalInitiative } from "../local_initiative";
|
|
27
|
+
import type { BatchTrackerPort, StartBatchInput } from "./batch_runner";
|
|
14
28
|
import {
|
|
15
29
|
BatchIntegrationError,
|
|
16
30
|
findIntegratedCandidate,
|
|
17
31
|
integrateGuardedLaneCommit,
|
|
18
32
|
type IntegrationCheckChild,
|
|
19
33
|
} from "./batch_integration";
|
|
20
|
-
import { classifyBatchLineage, expectedBatchHead, findExistingActiveBatch, readActiveClaimTaskId } from "./batch_preflight";
|
|
34
|
+
import { classifyBatchLineage, expectedBatchHead, findExistingActiveBatch, porcelainEntries, readActiveClaimTaskId } from "./batch_preflight";
|
|
35
|
+
import { restoreProvableLaneLeaks } from "./batch_leak_restore";
|
|
21
36
|
import { startableChildren } from "./batch_schedule";
|
|
22
37
|
import {
|
|
23
38
|
type AnyBatchRunStateRecord,
|
|
@@ -67,11 +82,12 @@ export function resolveLaneParallel(
|
|
|
67
82
|
initiativeSlug: string,
|
|
68
83
|
maxParallel: number | undefined,
|
|
69
84
|
laneOffers: unknown,
|
|
85
|
+
laneInstruction?: unknown,
|
|
70
86
|
): number | undefined {
|
|
71
|
-
if (maxParallel !== undefined || laneOffers === undefined) return maxParallel;
|
|
87
|
+
if (maxParallel !== undefined || (laneOffers === undefined && laneInstruction === undefined)) return maxParallel;
|
|
72
88
|
const lookup = findExistingActiveBatch(root, initiativeSlug);
|
|
73
89
|
if (lookup === null || lookup.corrupt || !isLaneBatchRecord(lookup.record))
|
|
74
|
-
throw new Error("lane_offers requires max_parallel unless a recorded lane batch is being resumed
|
|
90
|
+
throw new Error(`${laneOffers !== undefined ? "lane_offers" : "lane_instruction"} requires max_parallel unless a recorded lane batch is being resumed`);
|
|
75
91
|
return lookup.record.max_parallel;
|
|
76
92
|
}
|
|
77
93
|
|
|
@@ -109,6 +125,119 @@ export function parseLaneOffers(value: unknown): LaneOffer[] | undefined {
|
|
|
109
125
|
});
|
|
110
126
|
}
|
|
111
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The production tracker port both Hosts hand a lane batch: the same
|
|
130
|
+
* `mark-terminal` projection a single task runs after settlement. A Local
|
|
131
|
+
* Initiative has no tracker, so it gets none.
|
|
132
|
+
*/
|
|
133
|
+
export function createBatchTrackerPort(root: string, initiativeSlug: string): BatchTrackerPort | undefined {
|
|
134
|
+
if (hasLocalInitiative(root, initiativeSlug)) return undefined;
|
|
135
|
+
return { markTerminal: (trackerRoot, input) => runGithubTrackerOperation(trackerRoot, { op: "mark-terminal", ...input }) };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface LaneInstructionRequest {
|
|
139
|
+
task_id: string;
|
|
140
|
+
kind: "instruction" | "correction";
|
|
141
|
+
text: string;
|
|
142
|
+
/** The Lane session's state as the Parent just observed it from its supervised session. */
|
|
143
|
+
session_state: "idle" | "working" | "blocked" | "done";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const MAX_INSTRUCTION_LENGTH = 4000;
|
|
147
|
+
|
|
148
|
+
/** Untrusted tool input, shape-checked here and decided against the record below. */
|
|
149
|
+
export function parseLaneInstruction(value: unknown): LaneInstructionRequest | undefined {
|
|
150
|
+
if (value === undefined || value === null) return undefined;
|
|
151
|
+
if (typeof value !== "object" || Array.isArray(value)) throw new Error("invalid lane_instruction: expected an object");
|
|
152
|
+
const { task_id: taskId, kind = "instruction", text, session_state: sessionState, ...rest } = value as Record<string, unknown>;
|
|
153
|
+
if (Object.keys(rest).length > 0) throw new Error("invalid lane_instruction: unknown field");
|
|
154
|
+
if (typeof taskId !== "string" || !TASK_ID_PATTERN.test(taskId)) throw new Error("invalid lane_instruction: task_id is not a valid task id");
|
|
155
|
+
if (kind !== "instruction" && kind !== "correction") throw new Error("invalid lane_instruction: kind must be instruction or correction");
|
|
156
|
+
if (typeof text !== "string" || !text.trim() || text.length > MAX_INSTRUCTION_LENGTH)
|
|
157
|
+
throw new Error(`invalid lane_instruction: text must be 1 to ${MAX_INSTRUCTION_LENGTH} characters`);
|
|
158
|
+
if (sessionState !== "idle" && sessionState !== "working" && sessionState !== "blocked" && sessionState !== "done")
|
|
159
|
+
throw new Error("invalid lane_instruction: session_state must be idle, working, blocked or done");
|
|
160
|
+
return { task_id: taskId, kind, text, session_state: sessionState };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Whether the coordinator may send this text to the Lane session now. Text goes
|
|
165
|
+
* only to an idle session of a child that is still running in its Lane. A
|
|
166
|
+
* blocked session shows a dialog only the user may answer (trust, sign-in,
|
|
167
|
+
* permission, a question, a native confirmation), so it never receives input;
|
|
168
|
+
* a working session is not interrupted. Null means allowed.
|
|
169
|
+
*/
|
|
170
|
+
export function decideLaneInstruction(record: BatchLaneRunStateRecord, request: LaneInstructionRequest): string | null {
|
|
171
|
+
const child = record.children.find((c) => c.task_id === request.task_id);
|
|
172
|
+
if (!child || !child.lane) return `${request.task_id} has no Lane in this batch`;
|
|
173
|
+
if (child.state !== "enrolled") return `${request.task_id} is ${child.state}, not running in its Lane`;
|
|
174
|
+
if (request.kind === "correction" && !child.correction_due)
|
|
175
|
+
return `${request.task_id} has not exhausted its rework budget; send an ordinary instruction instead`;
|
|
176
|
+
if (request.kind === "instruction" && child.correction_due)
|
|
177
|
+
return `${request.task_id} waits for a design-level correction; send it with kind correction`;
|
|
178
|
+
if (request.session_state === "blocked")
|
|
179
|
+
return `${request.task_id}'s session is blocked on a dialog only the user may answer; send nothing and tell the user`;
|
|
180
|
+
if (request.session_state !== "idle") return `${request.task_id}'s session is ${request.session_state}; text goes only to an idle session`;
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** How many design-level coordinator corrections one child may receive. */
|
|
185
|
+
export const MAX_COORDINATOR_CORRECTIONS = 2;
|
|
186
|
+
/** The park reason once a child exhausts its budget after its last correction. */
|
|
187
|
+
export const CORRECTION_LIMIT_REACHED = "batch_correction_limit_reached";
|
|
188
|
+
|
|
189
|
+
export const LANE_CONTRACT_MISMATCH = "batch_lane_contract_mismatch";
|
|
190
|
+
export const RUNTIME_CONTRACT_MISMATCH = "batch_runtime_contract_mismatch";
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The runtime a Lane Executor will load, when the coordinator can know it: an
|
|
194
|
+
* explicit path, else the repository's own plugin source when the repository
|
|
195
|
+
* is the Immune-Brain package itself (a Host loading the package from the
|
|
196
|
+
* repository runs that source in every Lane, which is cut from this head).
|
|
197
|
+
* Null when nothing can be compared.
|
|
198
|
+
*/
|
|
199
|
+
function laneRuntimeDir(root: string, explicit: string | undefined): string | null {
|
|
200
|
+
if (explicit) return explicit;
|
|
201
|
+
try {
|
|
202
|
+
const manifest = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { name?: unknown };
|
|
203
|
+
return manifest.name === "immune-brain" ? root : null;
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Batch-start preflight for lane mode: the coordinator reads every Lane's
|
|
211
|
+
* Kernel state with its own runtime, so the runtime a Lane Executor loads must
|
|
212
|
+
* use the same contract identifiers. The comparison is on those identifiers,
|
|
213
|
+
* never on a plugin version number. A refusal names both sources; a match
|
|
214
|
+
* returns null and adds nothing, no confirmation included.
|
|
215
|
+
*/
|
|
216
|
+
export function laneRuntimeContractRefusal(root: string, executorRuntime?: string): string | null {
|
|
217
|
+
const dir = laneRuntimeDir(root, executorRuntime);
|
|
218
|
+
if (!dir) return null;
|
|
219
|
+
let lane: ReturnType<typeof readRuntimeContracts>;
|
|
220
|
+
try {
|
|
221
|
+
lane = readRuntimeContracts(dir);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
return `${RUNTIME_CONTRACT_MISMATCH}: ${error instanceof Error ? error.message : String(error)}`;
|
|
224
|
+
}
|
|
225
|
+
if (!lane) return executorRuntime ? `${RUNTIME_CONTRACT_MISMATCH}: no runtime contract manifest under ${executorRuntime}` : null;
|
|
226
|
+
const differences = runtimeContractDifferences(RUNTIME_CONTRACTS, lane.contracts);
|
|
227
|
+
if (differences.length === 0) return null;
|
|
228
|
+
return `${RUNTIME_CONTRACT_MISMATCH}: coordinator runtime ${runningRuntimeSource()} and Lane runtime ${lane.path} disagree (${differences.join("; ")}); load the same runtime on both sides before starting the batch`;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* A Lane whose Kernel state exists but that this runtime refuses to read is a
|
|
233
|
+
* contract mismatch, not a lost Lane: one side was upgraded under the batch.
|
|
234
|
+
*/
|
|
235
|
+
function laneUnreadableReason(error: string): string {
|
|
236
|
+
return /contract must equal|unknown field|schema version .* incompatible|unknown contract/i.test(error)
|
|
237
|
+
? `${LANE_CONTRACT_MISMATCH}: ${error}`
|
|
238
|
+
: "batch_lane_lost";
|
|
239
|
+
}
|
|
240
|
+
|
|
112
241
|
/** The branch the Lane for a child must sit on. */
|
|
113
242
|
export function laneBranchName(initiativeSlug: string, taskId: string): string {
|
|
114
243
|
return `imm-lane/${initiativeSlug}/${taskId}`;
|
|
@@ -279,12 +408,15 @@ function releaseHandoffs(
|
|
|
279
408
|
record: BatchLaneRunStateRecord,
|
|
280
409
|
lanes: BatchLaneGitPort,
|
|
281
410
|
): BatchLaneHandoff[] {
|
|
411
|
+
const lineage = classifyBatchLineage({ root: input.root, branch: record.branch ?? "",
|
|
412
|
+
expectedHead: expectedBatchHead(record), childCommits: record.commits, batchId: record.batch_id });
|
|
413
|
+
if (lineage.kind === "broken") return [];
|
|
282
414
|
const handoffs: BatchLaneHandoff[] = [];
|
|
283
415
|
for (const child of record.children) {
|
|
284
416
|
if (child.state !== "integrated" || !child.lane) continue;
|
|
285
417
|
const facts = lanes.inspectLane(input.root, child.lane.path);
|
|
286
|
-
if (!facts.exists || !facts.same_repository || !facts.clean || facts.branch !== child.lane.branch) continue;
|
|
287
|
-
if (!lanes.auditReachable({ root: input.root, head:
|
|
418
|
+
if (!facts.exists || !facts.same_repository || !facts.is_worktree_root || !facts.clean || facts.branch !== child.lane.branch || facts.active_claim_task_id !== null) continue;
|
|
419
|
+
if (!lanes.auditReachable({ root: input.root, head: lineage.head, task_id: child.task_id })) continue;
|
|
288
420
|
handoffs.push({ role: "lane-steward", action: "release", task_id: child.task_id, lane_branch: child.lane.branch });
|
|
289
421
|
}
|
|
290
422
|
return handoffs;
|
|
@@ -342,7 +474,7 @@ function scheduleView(input: StartBatchInput, record: BatchLaneRunStateRecord) {
|
|
|
342
474
|
}));
|
|
343
475
|
}
|
|
344
476
|
|
|
345
|
-
/**
|
|
477
|
+
/** Recovery preserves the parked run and requires fresh literal-user authorization. */
|
|
346
478
|
const LANE_NEEDS_HUMAN_NEXT_ACTION =
|
|
347
479
|
"The lane batch has stopped on a parked child and its Lane is kept. Resolve the child by hand; continuing needs a new Batch Authorization.";
|
|
348
480
|
|
|
@@ -351,16 +483,110 @@ const TERMINAL_NEXT_ACTIONS: Record<string, string> = {
|
|
|
351
483
|
budget_stopped: "The child budget stopped new enrollments; re-confirm to continue under a new authorization.",
|
|
352
484
|
failed: "A lineage failure stopped the batch; inspect the failing child and the branch state.",
|
|
353
485
|
rejected: "The batch was rejected before any enrollment; correct the stated reason and re-confirm.",
|
|
486
|
+
superseded: "The plan moved past this record and an explicit disposition retired it; it grants no handoff and no child delivery.",
|
|
354
487
|
needs_human: LANE_NEEDS_HUMAN_NEXT_ACTION,
|
|
355
488
|
running: "The batch is still running; no terminal report is due yet.",
|
|
356
489
|
prepared: "The batch is prepared but not started.",
|
|
357
490
|
};
|
|
358
491
|
|
|
492
|
+
/**
|
|
493
|
+
* ADR 0018 / #198: once a child is integrated, its TaskIntent on the batch
|
|
494
|
+
* branch may differ from the identity the batch authorized. When it equals the
|
|
495
|
+
* last revision applied under this batch's own delegation, the recorded
|
|
496
|
+
* identity is replaced and `plan_digest` resealed, with both digests recorded.
|
|
497
|
+
* Any other difference is left alone, so it still fails closed as `plan_changed`.
|
|
498
|
+
* Commits and their evidence are never touched.
|
|
499
|
+
*/
|
|
500
|
+
function resealDelegatedRevision(input: StartBatchInput, record: BatchLaneRunStateRecord, taskId: string): boolean {
|
|
501
|
+
const identities = record.intent_identities;
|
|
502
|
+
const recorded = identities?.[taskId];
|
|
503
|
+
if (!identities || !recorded) return false;
|
|
504
|
+
let integrated: { revision: number; content_hash: string };
|
|
505
|
+
try {
|
|
506
|
+
const read = readTaskIntent(input.root, taskId, recorded.intent_path);
|
|
507
|
+
integrated = { revision: read.intent.revision, content_hash: read.content_hash };
|
|
508
|
+
} catch {
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
if (integrated.revision === recorded.intent_revision && integrated.content_hash === recorded.intent_content_hash) return false;
|
|
512
|
+
const delegated = [...(record.delegated_revisions ?? [])].reverse().find((r) => r.task_id === taskId);
|
|
513
|
+
if (!delegated || delegated.to_revision !== integrated.revision || delegated.intent_content_hash !== integrated.content_hash) return false;
|
|
514
|
+
const next = { ...identities, [taskId]: { ...recorded, intent_revision: integrated.revision, intent_content_hash: integrated.content_hash } };
|
|
515
|
+
// Every child needs a recorded identity, or no digest can be recomputed.
|
|
516
|
+
if (record.children.some((child) => !next[child.task_id])) return false;
|
|
517
|
+
let toDigest: string;
|
|
518
|
+
try {
|
|
519
|
+
toDigest = computeBatchPlanDigest(record.children.map((child) => {
|
|
520
|
+
const identity = next[child.task_id]!;
|
|
521
|
+
return { task_id: child.task_id, intent_path: identity.intent_path, intent_revision: identity.intent_revision, intent_content_hash: identity.intent_content_hash, blocked_by: child.blocked_by };
|
|
522
|
+
}));
|
|
523
|
+
} catch {
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
record.reseals = [...(record.reseals ?? []), {
|
|
527
|
+
at: input.now,
|
|
528
|
+
task_id: taskId,
|
|
529
|
+
from_digest: record.plan_digest,
|
|
530
|
+
to_digest: toDigest,
|
|
531
|
+
intent_revision: integrated.revision,
|
|
532
|
+
intent_content_hash: integrated.content_hash,
|
|
533
|
+
}];
|
|
534
|
+
record.intent_identities = next;
|
|
535
|
+
record.plan_digest = toDigest;
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
type TrackerObservations = NonNullable<BatchLaneRunReport["tracker_observations"]>;
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Close the Child Issue of every integrated child not closed yet. Only the
|
|
544
|
+
* coordinator does this, and only after integration: a Lane settlement writes
|
|
545
|
+
* nothing to the tracker, and a parked or lost child is never integrated, so
|
|
546
|
+
* its Issue stays open. The terminal event comes from the child's audit pair,
|
|
547
|
+
* which integration brought onto the batch branch. A tracker failure never
|
|
548
|
+
* changes batch or Kernel state; the next tick retries it, and a confirmed
|
|
549
|
+
* close is recorded so it is never repeated.
|
|
550
|
+
*/
|
|
551
|
+
async function closeIntegratedIssues(
|
|
552
|
+
input: StartBatchInput,
|
|
553
|
+
record: BatchLaneRunStateRecord,
|
|
554
|
+
persist: () => void,
|
|
555
|
+
): Promise<TrackerObservations> {
|
|
556
|
+
const observations: TrackerObservations = [];
|
|
557
|
+
if (!input.tracker) return observations;
|
|
558
|
+
for (const child of record.children) {
|
|
559
|
+
if ((child.state !== "integrated" && child.state !== "released") || child.tracker_closed) continue;
|
|
560
|
+
let status: string;
|
|
561
|
+
let message: string;
|
|
562
|
+
try {
|
|
563
|
+
const tombstone = readTaskTombstone(input.root, child.task_id);
|
|
564
|
+
if (!tombstone || tombstone.lifecycle_status !== "terminal" || tombstone.terminal_lifecycle !== "done") {
|
|
565
|
+
observations.push({ task_id: child.task_id, status: "retryable_failure", message: "integrated child has no done terminal proof on the batch branch" });
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
({ status, message } = await input.tracker.markTerminal(input.root, {
|
|
569
|
+
task_id: child.task_id,
|
|
570
|
+
phase: "done",
|
|
571
|
+
terminal_event_id: tombstone.terminal_event_id,
|
|
572
|
+
}));
|
|
573
|
+
} catch (error) {
|
|
574
|
+
status = "retryable_failure";
|
|
575
|
+
message = error instanceof Error ? error.message : String(error);
|
|
576
|
+
}
|
|
577
|
+
observations.push({ task_id: child.task_id, status, message });
|
|
578
|
+
if (status !== "updated" && status !== "already_current") continue;
|
|
579
|
+
record.children = record.children.map((c) => (c.task_id === child.task_id ? { ...c, tracker_closed: true } : c));
|
|
580
|
+
persist();
|
|
581
|
+
}
|
|
582
|
+
return observations;
|
|
583
|
+
}
|
|
584
|
+
|
|
359
585
|
function laneReport(
|
|
360
586
|
record: BatchLaneRunStateRecord,
|
|
361
587
|
reason: string | null,
|
|
362
588
|
nextAction: string,
|
|
363
|
-
extra: { handoffs?: BatchLaneHandoff[]; refusals?: BatchLaneRunReport["lane_refusals"] } = {},
|
|
589
|
+
extra: { handoffs?: BatchLaneHandoff[]; refusals?: BatchLaneRunReport["lane_refusals"]; tracker?: TrackerObservations; final?: FinalVerificationReport; instruction?: BatchLaneRunReport["lane_instruction"]; revision?: BatchLaneRunReport["lane_revision"] } = {},
|
|
364
590
|
): BatchLaneRunReport {
|
|
365
591
|
return {
|
|
366
592
|
contract: "assurance_kernel/batch_run_report/v1",
|
|
@@ -373,14 +599,27 @@ function laneReport(
|
|
|
373
599
|
reason,
|
|
374
600
|
handoffs: extra.handoffs ?? [],
|
|
375
601
|
...(extra.refusals?.length ? { lane_refusals: extra.refusals } : {}),
|
|
602
|
+
...(extra.tracker?.length ? { tracker_observations: extra.tracker } : {}),
|
|
603
|
+
...(record.restores?.length ? { restores: record.restores } : {}),
|
|
604
|
+
...(extra.final ? { final_verification: extra.final } : {}),
|
|
605
|
+
...(record.interventions?.length ? { interventions: record.interventions } : {}),
|
|
606
|
+
...(extra.instruction ? { lane_instruction: extra.instruction } : {}),
|
|
607
|
+
...(extra.revision ? { lane_revision: extra.revision } : {}),
|
|
608
|
+
...(record.delegated_revisions?.length ? { delegated_revisions: record.delegated_revisions } : {}),
|
|
609
|
+
...(record.reseals?.length ? { reseals: record.reseals } : {}),
|
|
376
610
|
next_action: nextAction || (TERMINAL_NEXT_ACTIONS[record.batch_state] ?? "Inspect the batch run state."),
|
|
377
611
|
created_at: record.updated_at,
|
|
378
612
|
};
|
|
379
613
|
}
|
|
380
614
|
|
|
381
615
|
/** A store-condition rejection of a persisted lane batch: its persisted lane children, commits and parallelism, never an empty serial plan. */
|
|
382
|
-
export function laneRejectionReport(record: BatchLaneRunStateRecord, reason: string, nextAction: string): BatchLaneRunReport {
|
|
383
|
-
|
|
616
|
+
export function laneRejectionReport(input: StartBatchInput, record: BatchLaneRunStateRecord, reason: string, nextAction: string): BatchLaneRunReport {
|
|
617
|
+
// A rejected tick grants no execution or admission authority. Only already
|
|
618
|
+
// integrated, unoccupied Lanes with surviving audit evidence may be released.
|
|
619
|
+
const handoffs = releaseHandoffs(input, record, laneGitOf(input));
|
|
620
|
+
return laneReport({ ...record, batch_state: "rejected" }, reason,
|
|
621
|
+
handoffs.length ? `${nextAction}. Check each release handoff with the Lane Steward.` : nextAction,
|
|
622
|
+
{ handoffs });
|
|
384
623
|
}
|
|
385
624
|
|
|
386
625
|
function finalizeLane(
|
|
@@ -531,20 +770,50 @@ export async function runLaneBatch(
|
|
|
531
770
|
if (persisted) {
|
|
532
771
|
record = persisted;
|
|
533
772
|
assertPlanMatches(input, record);
|
|
534
|
-
if (isTerminalBatchState(record.batch_state))
|
|
773
|
+
if (isTerminalBatchState(record.batch_state)) {
|
|
774
|
+
// A close that failed on the completing tick is retried here; a terminal
|
|
775
|
+
// record otherwise stays as it was.
|
|
776
|
+
const tracker = record.batch_state === "completed"
|
|
777
|
+
? await closeIntegratedIssues(input, record, () => { record = writeBatchLaneRunState(input.root, record); })
|
|
778
|
+
: [];
|
|
535
779
|
return finalizeLane(input.root, record, `terminal state already reached: ${record.batch_state}`, "", {
|
|
536
780
|
handoffs: record.batch_state === "completed" ? releaseHandoffs(input, record, lanes) : [],
|
|
781
|
+
tracker,
|
|
537
782
|
});
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
"
|
|
543
|
-
|
|
544
|
-
)
|
|
783
|
+
}
|
|
784
|
+
if (record.batch_state === "needs_human") {
|
|
785
|
+
const invalid = validateNewAuthorization(input);
|
|
786
|
+
if (invalid || Date.parse(input.confirmation_time) <= Date.parse(record.confirmation_time))
|
|
787
|
+
return laneReport(record, invalid ?? "the parked batch requires a fresh literal-user confirmation", LANE_NEEDS_HUMAN_NEXT_ACTION);
|
|
788
|
+
const resumed: BatchLaneChildRun[] = [];
|
|
789
|
+
for (const child of record.children) {
|
|
790
|
+
if (child.state !== "needs_human") { resumed.push(child); continue; }
|
|
791
|
+
if (!child.lane?.run_id)
|
|
792
|
+
return laneReport(record, "parked Lane has no recorded run identity; inspect it without re-enrolling", LANE_NEEDS_HUMAN_NEXT_ACTION);
|
|
793
|
+
const facts = lanes.inspectLane(input.root, child.lane.path);
|
|
794
|
+
const fresh = await input.kernel.projectTask(child.lane.path, child.task_id);
|
|
795
|
+
if (!facts.exists || !facts.same_repository || !facts.is_worktree_root || facts.branch !== child.lane.branch ||
|
|
796
|
+
fresh.error !== null || fresh.projection.run_id !== child.lane.run_id ||
|
|
797
|
+
fresh.projection.open_user_decision_count > 0 || fresh.projection.replan_required_ids.length > 0)
|
|
798
|
+
return laneReport(record, "parked Lane identity or decision is unresolved", LANE_NEEDS_HUMAN_NEXT_ACTION);
|
|
799
|
+
const state = fresh.projection.lifecycle === "done" && fresh.projection.completion_ready ?
|
|
800
|
+
(child.lane.lane_commit ? "lane_committed" : "settled") :
|
|
801
|
+
fresh.projection.lifecycle === "active" && fresh.claim?.task_id === child.task_id &&
|
|
802
|
+
(facts.active_claim_task_id === null || facts.active_claim_task_id === child.task_id) ? "enrolled" : null;
|
|
803
|
+
if (!state) return laneReport(record, "parked child has no recoverable owned run", LANE_NEEDS_HUMAN_NEXT_ACTION);
|
|
804
|
+
resumed.push({ ...child, state, reason: null, qa_failures: 0 });
|
|
805
|
+
}
|
|
806
|
+
record = { ...record, batch_state: "running", confirmation_time: input.confirmation_time,
|
|
807
|
+
children: resumed.map(child => child.state === "skipped_blocked" ? { ...child, state: "pending", reason: null } : child) };
|
|
808
|
+
}
|
|
545
809
|
} else {
|
|
546
810
|
const invalid = validateNewAuthorization(input);
|
|
547
811
|
if (invalid) return refuse(input, null, "batch_authorization_invalid", invalid);
|
|
812
|
+
const contractRefusal = laneRuntimeContractRefusal(input.root, input.executor_runtime);
|
|
813
|
+
if (contractRefusal) {
|
|
814
|
+
const [code, ...detail] = contractRefusal.split(": ");
|
|
815
|
+
return refuse(input, null, code!, detail.join(": "));
|
|
816
|
+
}
|
|
548
817
|
const preflight = await git.preflight({
|
|
549
818
|
root: input.root,
|
|
550
819
|
initiative_slug: input.initiative_slug,
|
|
@@ -561,12 +830,35 @@ export async function runLaneBatch(
|
|
|
561
830
|
budget: input.budget,
|
|
562
831
|
max_parallel: limit,
|
|
563
832
|
now: input.now,
|
|
833
|
+
...(input.final_verification?.length ? { final_verification: input.final_verification } : {}),
|
|
834
|
+
...(input.revision_delegation ? { revision_delegation: { confirmation_time: input.confirmation_time } } : {}),
|
|
564
835
|
});
|
|
565
836
|
}
|
|
837
|
+
// A fresh confirmation of a persisted batch replaces its grant with that answer.
|
|
838
|
+
if (persisted && input.revision_delegation !== undefined) {
|
|
839
|
+
const { revision_delegation: _prior, ...rest } = record;
|
|
840
|
+
record = input.revision_delegation ? { ...rest, revision_delegation: { confirmation_time: input.confirmation_time } } : rest;
|
|
841
|
+
// Durable at once: a withdrawn grant must never survive to a later reused tick.
|
|
842
|
+
if (record.batch_state !== "prepared") record = writeBatchLaneRunState(input.root, record);
|
|
843
|
+
}
|
|
566
844
|
|
|
567
845
|
const persist = (): void => {
|
|
568
846
|
record = writeBatchLaneRunState(input.root, record);
|
|
569
847
|
};
|
|
848
|
+
// Lane writes that leaked into the coordinator checkout, restored only now,
|
|
849
|
+
// after authorization, and only when every dirty path is provably a Lane's.
|
|
850
|
+
if (persisted) {
|
|
851
|
+
const entries = porcelainEntries(input.root);
|
|
852
|
+
if (entries && entries.length > 0) {
|
|
853
|
+
const outcome = restoreProvableLaneLeaks(input.root, record, entries, input.now);
|
|
854
|
+
if (outcome.kind === "restored") {
|
|
855
|
+
record.restores = [...(record.restores ?? []), outcome.restore];
|
|
856
|
+
persist();
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// A resumed park is persisted only after the existing lineage validation below.
|
|
861
|
+
const resumedPark = persisted?.batch_state === "needs_human" && record.batch_state === "running";
|
|
570
862
|
if (record.batch_state === "prepared") {
|
|
571
863
|
record.batch_state = "running";
|
|
572
864
|
persist();
|
|
@@ -622,7 +914,72 @@ export async function runLaneBatch(
|
|
|
622
914
|
return failLineage(input.root, record, error instanceof Error ? error.message : String(error));
|
|
623
915
|
}
|
|
624
916
|
|
|
917
|
+
if (resumedPark) persist();
|
|
918
|
+
// A coordinator instruction is recorded before the Parent sends it, and only
|
|
919
|
+
// when it may be sent; the report tells the Parent which.
|
|
920
|
+
let instruction: BatchLaneRunReport["lane_instruction"];
|
|
921
|
+
if (input.lane_instruction) {
|
|
922
|
+
const request = input.lane_instruction;
|
|
923
|
+
const refusal = decideLaneInstruction(record, request);
|
|
924
|
+
if (refusal) instruction = { task_id: request.task_id, kind: request.kind, accepted: false, reason: `batch_lane_instruction_refused: ${refusal}` };
|
|
925
|
+
else {
|
|
926
|
+
if (request.kind === "correction")
|
|
927
|
+
record.children = record.children.map((c) => {
|
|
928
|
+
if (c.task_id !== request.task_id) return c;
|
|
929
|
+
const { correction_due: _due, ...rest } = c;
|
|
930
|
+
return { ...rest, corrections: (c.corrections ?? 0) + 1, qa_failures: 0 };
|
|
931
|
+
});
|
|
932
|
+
record.interventions = [...(record.interventions ?? []), {
|
|
933
|
+
at: input.now,
|
|
934
|
+
task_id: request.task_id,
|
|
935
|
+
kind: request.kind,
|
|
936
|
+
text: request.text,
|
|
937
|
+
text_digest: `sha256:${createHash("sha256").update(request.text).digest("hex")}`,
|
|
938
|
+
}];
|
|
939
|
+
persist();
|
|
940
|
+
instruction = { task_id: request.task_id, kind: request.kind, accepted: true, reason: null };
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
// ADR 0018: a breaking revision the coordinator approves under the batch's
|
|
944
|
+
// grant. Out of bounds, or without a grant, it is refused and the Executor's
|
|
945
|
+
// own native gate stays the only path.
|
|
946
|
+
let revision: BatchLaneRunReport["lane_revision"];
|
|
947
|
+
if (input.lane_revision) {
|
|
948
|
+
const request = input.lane_revision;
|
|
949
|
+
const fallback = "the Executor requests approve_breaking_intent_revision through its own native gate for the user to answer";
|
|
950
|
+
const refuseRevision = (reason: string) => { revision = { task_id: request.task_id, accepted: false, reason: `batch_delegation_refused: ${reason}`, fallback }; };
|
|
951
|
+
const child = record.children.find((c) => c.task_id === request.task_id);
|
|
952
|
+
const planChild = input.children.find((c) => c.task_id === request.task_id);
|
|
953
|
+
const grant = record.revision_delegation;
|
|
954
|
+
if (!grant) refuseRevision("this batch's confirmation did not delegate Intent revisions");
|
|
955
|
+
else if (!child?.lane || child.state !== "enrolled") refuseRevision(`${request.task_id} is not running in its Lane`);
|
|
956
|
+
else {
|
|
957
|
+
try {
|
|
958
|
+
const authorized = readTaskIntent(input.root, request.task_id, planChild?.intent_path ?? undefined).intent;
|
|
959
|
+
const current = readTaskRecordRaw(child.lane.path, request.task_id).record?.intent_snapshot;
|
|
960
|
+
if (!current) throw new Error(`the Lane has no TaskRecord for ${request.task_id}`);
|
|
961
|
+
const outOfBounds = delegatedRevisionRefusal(authorized, current, request.next_intent);
|
|
962
|
+
if (outOfBounds) refuseRevision(outOfBounds);
|
|
963
|
+
else {
|
|
964
|
+
const confirmationRef = delegatedConfirmationRef(record.batch_id, grant);
|
|
965
|
+
const applied = await applyDelegatedIntentRevision({
|
|
966
|
+
lane_root: child.lane.path,
|
|
967
|
+
task_id: request.task_id,
|
|
968
|
+
next_intent: request.next_intent,
|
|
969
|
+
confirmation_ref: confirmationRef,
|
|
970
|
+
now: input.now,
|
|
971
|
+
});
|
|
972
|
+
record.delegated_revisions = [...(record.delegated_revisions ?? []), { at: input.now, task_id: request.task_id, ...applied, confirmation_ref: confirmationRef }];
|
|
973
|
+
persist();
|
|
974
|
+
revision = { task_id: request.task_id, accepted: true, reason: null };
|
|
975
|
+
}
|
|
976
|
+
} catch (error) {
|
|
977
|
+
refuseRevision(error instanceof Error ? error.message : String(error));
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
625
981
|
const refusals: NonNullable<BatchLaneRunReport["lane_refusals"]> = [];
|
|
982
|
+
|
|
626
983
|
// A parked child ends only itself and its dependents; the batch keeps moving
|
|
627
984
|
// for every scope-disjoint sibling and settles needs_human at the end of the
|
|
628
985
|
// tick, when nothing is in flight and nothing can start.
|
|
@@ -641,7 +998,10 @@ export async function runLaneBatch(
|
|
|
641
998
|
task_id: taskId,
|
|
642
999
|
batch: { registry: input.registry, capability: input.capability, binding: { batch_id: record.batch_id, expected_head: lane.base_head } },
|
|
643
1000
|
});
|
|
644
|
-
|
|
1001
|
+
const fresh = await input.kernel.projectTask(lane.path, taskId);
|
|
1002
|
+
if (fresh.error !== null || !fresh.projection.run_id || fresh.claim?.task_id !== taskId)
|
|
1003
|
+
throw new Error("enrolled Lane has no observable owned run");
|
|
1004
|
+
record.children = record.children.map((c) => (c.task_id === taskId ? { ...c, state: "enrolled", reason: null, lane: { ...lane, run_id: fresh.projection.run_id } } : c));
|
|
645
1005
|
persist();
|
|
646
1006
|
return null;
|
|
647
1007
|
} catch (error) {
|
|
@@ -662,11 +1022,15 @@ export async function runLaneBatch(
|
|
|
662
1022
|
let fresh: Awaited<ReturnType<StartBatchInput["kernel"]["projectTask"]>>;
|
|
663
1023
|
try {
|
|
664
1024
|
fresh = await input.kernel.projectTask(child.lane.path, child.task_id);
|
|
665
|
-
} catch {
|
|
666
|
-
fresh = { error:
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
fresh = { error: error instanceof Error ? error.message : String(error) } as unknown as typeof fresh;
|
|
667
1027
|
}
|
|
668
1028
|
if (fresh.error !== null) {
|
|
669
|
-
parkChild(child.task_id,
|
|
1029
|
+
parkChild(child.task_id, laneUnreadableReason(String(fresh.error)));
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
if (child.lane.run_id && fresh.projection.run_id !== child.lane.run_id) {
|
|
1033
|
+
parkChild(child.task_id, "batch_lane_run_mismatch");
|
|
670
1034
|
continue;
|
|
671
1035
|
}
|
|
672
1036
|
const holdsClaim = fresh.claim !== null && fresh.claim.task_id === child.task_id;
|
|
@@ -674,7 +1038,7 @@ export async function runLaneBatch(
|
|
|
674
1038
|
// A crash between admission and enrollment adopts an existing claim or enrolls now.
|
|
675
1039
|
if (holdsClaim) {
|
|
676
1040
|
record.children = record.children.map((c) =>
|
|
677
|
-
c.task_id === child.task_id ? { ...c, state: "enrolled", reason: "adopted existing lane claim after interruption" } : c,
|
|
1041
|
+
c.task_id === child.task_id ? { ...c, state: "enrolled", reason: "adopted existing lane claim after interruption", lane: { ...child.lane!, run_id: fresh.projection.run_id } } : c,
|
|
678
1042
|
);
|
|
679
1043
|
persist();
|
|
680
1044
|
} else {
|
|
@@ -696,6 +1060,8 @@ export async function runLaneBatch(
|
|
|
696
1060
|
parkChild(child.task_id, "claim held by another batch");
|
|
697
1061
|
continue;
|
|
698
1062
|
}
|
|
1063
|
+
// A child waiting for its coordinator correction gets no further attempt.
|
|
1064
|
+
if (child.correction_due) continue;
|
|
699
1065
|
// An unfrozen child, or one with QA pending the Executor's own attempt,
|
|
700
1066
|
// is the Executor's to move; the batch never reruns that attempt.
|
|
701
1067
|
if (fresh.projection.next_obligation === "run_qa" || fresh.projection.artifact_state !== "frozen") continue;
|
|
@@ -717,11 +1083,24 @@ export async function runLaneBatch(
|
|
|
717
1083
|
const failures = child.qa_failures + 1;
|
|
718
1084
|
if (failures >= record.budget.qa_failure_limit) {
|
|
719
1085
|
const reason = "QA failure limit reached";
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
1086
|
+
const used = child.corrections ?? 0;
|
|
1087
|
+
if (used < MAX_COORDINATOR_CORRECTIONS) {
|
|
1088
|
+
// Hand the design judgment to the coordinator instead of parking: one
|
|
1089
|
+
// correction, sent to a new Lane session, before any further attempt.
|
|
1090
|
+
record.children = record.children.map((c) =>
|
|
1091
|
+
c.task_id === child.task_id ? { ...c, qa_failures: failures, correction_due: reason } : c,
|
|
1092
|
+
);
|
|
1093
|
+
persist();
|
|
1094
|
+
continue;
|
|
1095
|
+
}
|
|
1096
|
+
const parked = `${CORRECTION_LIMIT_REACHED}: ${reason} after ${used} coordinator corrections`;
|
|
1097
|
+
record.children = record.children.map((c) => {
|
|
1098
|
+
if (c.task_id !== child.task_id) return c;
|
|
1099
|
+
const { correction_due: _due, ...rest } = c;
|
|
1100
|
+
return { ...rest, state: "needs_human", reason: parked, qa_failures: failures };
|
|
1101
|
+
});
|
|
723
1102
|
skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
724
|
-
parkedMessage ??=
|
|
1103
|
+
parkedMessage ??= parked;
|
|
725
1104
|
persist();
|
|
726
1105
|
continue;
|
|
727
1106
|
}
|
|
@@ -791,6 +1170,9 @@ export async function runLaneBatch(
|
|
|
791
1170
|
record.children = record.children.map((c) => (c.task_id === child.task_id ? { ...c, state: "integrated", commit, reason: null } : c));
|
|
792
1171
|
record.commits = [...record.commits, commit];
|
|
793
1172
|
persist();
|
|
1173
|
+
// The integration is durable before any reseal; a reseal that cannot be
|
|
1174
|
+
// computed leaves plan_digest as it was, so the change fails closed later.
|
|
1175
|
+
if (resealDelegatedRevision(input, record, child.task_id)) persist();
|
|
794
1176
|
} catch (error) {
|
|
795
1177
|
const message = error instanceof Error ? error.message : String(error);
|
|
796
1178
|
const reason = error instanceof BatchIntegrationError ? error.reason : "batch_integration_conflict";
|
|
@@ -805,6 +1187,9 @@ export async function runLaneBatch(
|
|
|
805
1187
|
}
|
|
806
1188
|
}
|
|
807
1189
|
|
|
1190
|
+
// Close each integrated child's Issue now that its commit is on the batch branch.
|
|
1191
|
+
const tracker = await closeIntegratedIssues(input, record, persist);
|
|
1192
|
+
|
|
808
1193
|
// 3. Admit offered Lanes, then enroll each admitted child into its Lane.
|
|
809
1194
|
const coordinatorReal = lanes.resolveRoot(input.root);
|
|
810
1195
|
for (const offer of input.lane_offers ?? []) {
|
|
@@ -838,16 +1223,39 @@ export async function runLaneBatch(
|
|
|
838
1223
|
|
|
839
1224
|
// 4. Schedule and report. Handoffs are observations, never readiness.
|
|
840
1225
|
if (record.children.every((c) => c.state === "integrated" || c.state === "released")) {
|
|
1226
|
+
// Full verification of the integrated batch branch, before the immutable
|
|
1227
|
+
// completion report is written. A failure is reported, never rolled back.
|
|
1228
|
+
const final = record.final_verification?.length ? await runFinalVerification(input.root, record.final_verification) : undefined;
|
|
841
1229
|
record.batch_state = "completed";
|
|
842
1230
|
persist();
|
|
843
|
-
return finalizeLane(
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1231
|
+
return finalizeLane(
|
|
1232
|
+
input.root,
|
|
1233
|
+
record,
|
|
1234
|
+
final && !final.passed
|
|
1235
|
+
? `all enrollable children integrated; final verification did not pass: ${final.results.filter((r) => !r.passed).map((r) => r.command).join(", ")}`
|
|
1236
|
+
: "all enrollable children integrated",
|
|
1237
|
+
final && !final.passed
|
|
1238
|
+
? "Every child is integrated but the batch did not pass its final verification; inspect the failing commands on the batch branch. Integrated commits were not rolled back."
|
|
1239
|
+
: "",
|
|
1240
|
+
{ handoffs: releaseHandoffs(input, record, lanes), refusals, tracker, instruction, revision, ...(final ? { final } : {}) },
|
|
1241
|
+
);
|
|
847
1242
|
}
|
|
848
1243
|
const handoffs: BatchLaneHandoff[] = [];
|
|
849
1244
|
for (const child of record.children) {
|
|
850
1245
|
if (child.state !== "enrolled" || !child.lane) continue;
|
|
1246
|
+
if (child.correction_due) {
|
|
1247
|
+
// No further Executor attempt until the coordinator's correction is recorded.
|
|
1248
|
+
handoffs.push({
|
|
1249
|
+
role: "coordinator",
|
|
1250
|
+
action: "correct",
|
|
1251
|
+
task_id: child.task_id,
|
|
1252
|
+
reason: child.correction_due,
|
|
1253
|
+
corrections_used: child.corrections ?? 0,
|
|
1254
|
+
corrections_left: MAX_COORDINATOR_CORRECTIONS - (child.corrections ?? 0),
|
|
1255
|
+
lane_path: child.lane.path,
|
|
1256
|
+
});
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
851
1259
|
const fresh = await input.kernel.projectTask(child.lane.path, child.task_id);
|
|
852
1260
|
if (fresh.error !== null) continue;
|
|
853
1261
|
handoffs.push({
|
|
@@ -873,7 +1281,7 @@ export async function runLaneBatch(
|
|
|
873
1281
|
record,
|
|
874
1282
|
parkedMessage ?? (parked ? "a parked child needs a human decision" : overBudget ? `max_children budget exhausted (${record.budget.max_children})` : "no child can start"),
|
|
875
1283
|
"",
|
|
876
|
-
{ refusals },
|
|
1284
|
+
{ refusals, tracker, instruction, revision },
|
|
877
1285
|
);
|
|
878
1286
|
}
|
|
879
1287
|
if (!overBudget) {
|
|
@@ -895,15 +1303,17 @@ export async function runLaneBatch(
|
|
|
895
1303
|
? `child ${reviewOpen[0]} holds an open Review reservation`
|
|
896
1304
|
: `children ${reviewOpen.join(", ")} hold open Review reservations`,
|
|
897
1305
|
"Submit each reserved foreground Review verdict in its own Lane, then call start_unattended_batch again to continue.",
|
|
898
|
-
{ handoffs, refusals },
|
|
1306
|
+
{ handoffs, refusals, tracker, instruction, revision },
|
|
899
1307
|
);
|
|
900
1308
|
}
|
|
901
1309
|
return laneReport(
|
|
902
1310
|
record,
|
|
903
1311
|
null,
|
|
904
|
-
handoffs.some((h) => h.role === "
|
|
905
|
-
? "
|
|
906
|
-
:
|
|
907
|
-
|
|
1312
|
+
handoffs.some((h) => h.role === "coordinator")
|
|
1313
|
+
? "For each coordinator handoff, read every round's findings in that Lane, start a new Lane session and send one design-level correction as lane_instruction kind correction; run the other handoffs as usual."
|
|
1314
|
+
: handoffs.some((h) => h.role === "lane-steward" && h.action === "provision")
|
|
1315
|
+
? "Provide a Lane for each provision handoff, then call start_unattended_batch again with lane_offers."
|
|
1316
|
+
: "Run each executor handoff in its Lane. When a Lane finishes, call start_unattended_batch again with the same Initiative.",
|
|
1317
|
+
{ handoffs, refusals, tracker, instruction, revision },
|
|
908
1318
|
);
|
|
909
1319
|
}
|