immune-brain 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +223 -12
  2. package/README.zh-CN.md +228 -11
  3. package/package.json +2 -1
  4. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +1 -0
  6. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +24 -3
  7. package/plugins/immune-brain/dist/claude/mcp-server.mjs +1339 -66
  8. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +27 -0
  9. package/plugins/immune-brain/dist/imm-run.md +198 -3
  10. package/plugins/immune-brain/dist/role-prompts/lane-steward.md +44 -0
  11. package/plugins/immune-brain/runtime/claude/interaction.ts +6 -0
  12. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +16 -4
  13. package/plugins/immune-brain/runtime/claude/mcp_server.ts +24 -2
  14. package/plugins/immune-brain/runtime/claude/role_agents.ts +4 -3
  15. package/plugins/immune-brain/runtime/local_initiative.ts +94 -0
  16. package/plugins/immune-brain/runtime/loop_contract.ts +55 -2
  17. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  18. package/plugins/immune-brain/runtime/prompts/lane-steward.md +44 -0
  19. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +15 -2
  20. package/plugins/immune-brain/runtime/unattended/batch_integration.ts +387 -0
  21. package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +2 -2
  22. package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +909 -0
  23. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +36 -11
  24. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +41 -22
  25. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +20 -1
  26. package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +162 -0
  27. package/plugins/immune-brain/runtime/unattended/batch_state.ts +356 -8
  28. package/plugins/immune-brain/runtime/unattended/types.ts +21 -2
  29. package/plugins/immune-brain/skills/imm-run/SKILL.md +4 -0
@@ -39,7 +39,7 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
39
39
  }
40
40
 
41
41
  // plugins/immune-brain/runtime/plugin_version.ts
42
- var PLUGIN_VERSION = "5.0.0";
42
+ var PLUGIN_VERSION = "5.1.0";
43
43
 
44
44
  // plugins/immune-brain/runtime/claude/interaction.ts
45
45
  import { createHash, randomUUID } from "node:crypto";
@@ -6816,8 +6816,14 @@ var INTERNAL_ROLE_PROMPTS = {
6816
6816
  file: "compounder.md",
6817
6817
  authority: "compounder",
6818
6818
  tool_policy: "learning tools"
6819
+ },
6820
+ "lane-steward": {
6821
+ file: "lane-steward.md",
6822
+ authority: "lane-provision",
6823
+ tool_policy: "workspace tools"
6819
6824
  }
6820
6825
  };
6826
+ var LANE_EXECUTOR_HOSTS = ["claude-code", "pi"];
6821
6827
  function roleSpec(role) {
6822
6828
  const spec = INTERNAL_ROLE_PROMPTS[role];
6823
6829
  if (!spec)
@@ -8192,9 +8198,9 @@ function createVerdictAuthority(options, registry = createMutationAuthorityRegis
8192
8198
  }
8193
8199
 
8194
8200
  // plugins/immune-brain/runtime/claude/kernel_ports.ts
8195
- import { existsSync as existsSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "node:fs";
8201
+ import { existsSync as existsSync12, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
8196
8202
  import { execFileSync as execFileSync8 } from "node:child_process";
8197
- import { join as join16 } from "node:path";
8203
+ import { join as join17 } from "node:path";
8198
8204
 
8199
8205
  // plugins/immune-brain/runtime/kernel/enrollment_authority.ts
8200
8206
  var ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollment-capability-brand");
@@ -10699,7 +10705,7 @@ async function runGithubTrackerOperation(root, input, gh = createGhTransport())
10699
10705
  }
10700
10706
 
10701
10707
  // plugins/immune-brain/runtime/unattended/batch_preflight.ts
10702
- import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync13 } from "node:fs";
10708
+ import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync14 } from "node:fs";
10703
10709
  import { randomUUID as randomUUID8 } from "node:crypto";
10704
10710
  import { join as join13 } from "node:path";
10705
10711
  import { spawnSync as spawnSync7 } from "node:child_process";
@@ -10711,6 +10717,9 @@ import { spawnSync as spawnSync6 } from "node:child_process";
10711
10717
  import { existsSync as existsSync7, mkdirSync as mkdirSync6, openSync as openSync6, closeSync as closeSync6, writeFileSync as writeFileSync6, renameSync as renameSync3, lstatSync as lstatSync9, constants as constants5, rmSync as rmSync6 } from "node:fs";
10712
10718
  import { randomUUID as randomUUID7 } from "node:crypto";
10713
10719
  import { dirname as dirname7, join as join12 } from "node:path";
10720
+ function isLaneBatchRecord(record) {
10721
+ return record.contract === "assurance_kernel/batch_run_state/v2";
10722
+ }
10714
10723
  var BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10715
10724
  var CHILD_RUN_STATES = new Set([
10716
10725
  "pending",
@@ -10918,6 +10927,162 @@ function writeBatchRunState(root, record) {
10918
10927
  return stored;
10919
10928
  });
10920
10929
  }
10930
+ var LANE_CHILD_STATES = new Set([
10931
+ "pending",
10932
+ "lane_admitted",
10933
+ "enrolled",
10934
+ "settled",
10935
+ "lane_committed",
10936
+ "integrated",
10937
+ "released",
10938
+ "needs_human",
10939
+ "skipped_blocked"
10940
+ ]);
10941
+ var LANE_BOUND_STATES = new Set([
10942
+ "lane_admitted",
10943
+ "enrolled",
10944
+ "settled",
10945
+ "lane_committed",
10946
+ "integrated",
10947
+ "released"
10948
+ ]);
10949
+ function validateLaneRecordShape(value, batchId) {
10950
+ if (typeof value !== "object" || value === null)
10951
+ throw new Error(`batch run state ${batchId} is not an object`);
10952
+ const record = value;
10953
+ if (record.contract !== "assurance_kernel/batch_run_state/v2")
10954
+ throw new Error(`batch run state ${batchId} has an unknown contract`);
10955
+ if (record.batch_id !== batchId)
10956
+ throw new Error(`batch run state ${batchId} carries batch_id ${String(record.batch_id)}`);
10957
+ if (typeof record.plan_digest !== "string" || !record.plan_digest)
10958
+ throw new Error(`batch run state ${batchId} has an invalid plan_digest`);
10959
+ if (typeof record.base_head !== "string" || !record.base_head)
10960
+ throw new Error(`batch run state ${batchId} has an invalid base_head`);
10961
+ if (record.branch !== undefined && (typeof record.branch !== "string" || !record.branch))
10962
+ throw new Error(`batch run state ${batchId} has an invalid branch`);
10963
+ if (!isCanonicalTimestamp(record.confirmation_time))
10964
+ throw new Error(`batch run state ${batchId} has an invalid confirmation_time`);
10965
+ if (!isCanonicalTimestamp(record.created_at) || !isCanonicalTimestamp(record.updated_at))
10966
+ throw new Error(`batch run state ${batchId} has invalid state timestamps`);
10967
+ if (!Array.isArray(record.children) || record.children.length === 0)
10968
+ throw new Error(`batch run state ${batchId} has no children`);
10969
+ if (!BATCH_RUN_STATES.has(String(record.batch_state)))
10970
+ throw new Error(`batch run state ${batchId} has an invalid batch_state`);
10971
+ if (typeof record.max_parallel !== "number" || !Number.isSafeInteger(record.max_parallel) || record.max_parallel <= 0)
10972
+ throw new Error(`batch run state ${batchId} has an invalid max_parallel`);
10973
+ const budget = record.budget;
10974
+ if (typeof record.budget !== "object" || record.budget === null || typeof budget.max_children !== "number" || !Number.isInteger(budget.max_children) || budget.max_children <= 0 || typeof budget.qa_failure_limit !== "number" || !Number.isInteger(budget.qa_failure_limit) || budget.qa_failure_limit <= 0)
10975
+ throw new Error(`batch run state ${batchId} has an invalid budget`);
10976
+ if (!Array.isArray(record.commits) || record.commits.some((c) => typeof c !== "string"))
10977
+ throw new Error(`batch run state ${batchId} has an invalid commits list`);
10978
+ if (record.adopted_heads !== undefined && (!Array.isArray(record.adopted_heads) || record.adopted_heads.some((a) => typeof a !== "object" || a === null || typeof a.from !== "string" || !a.from || typeof a.to !== "string" || !a.to)))
10979
+ throw new Error(`batch run state ${batchId} has an invalid adopted_heads list`);
10980
+ const seenTaskIds = new Set;
10981
+ for (const child of record.children) {
10982
+ if (typeof child !== "object" || child === null || typeof child.task_id !== "string" || !child.task_id || typeof child.slice_id !== "string" || !child.slice_id)
10983
+ throw new Error(`batch run state ${batchId} has an invalid child entry`);
10984
+ if (!LANE_CHILD_STATES.has(String(child.state)))
10985
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid state`);
10986
+ if (!Array.isArray(child.blocked_by) || child.blocked_by.some((b) => typeof b !== "string"))
10987
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid blocked_by`);
10988
+ if (child.reason !== null && typeof child.reason !== "string" || child.commit !== null && typeof child.commit !== "string")
10989
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has invalid terminal fields`);
10990
+ if (typeof child.qa_failures !== "number" || !Number.isInteger(child.qa_failures) || child.qa_failures < 0)
10991
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid qa_failures`);
10992
+ const lane = child.lane;
10993
+ if (lane !== null) {
10994
+ if (typeof lane !== "object" || typeof lane.path !== "string" || !lane.path || typeof lane.branch !== "string" || !lane.branch || typeof lane.base_head !== "string" || !lane.base_head || lane.lane_commit !== null && typeof lane.lane_commit !== "string")
10995
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid lane`);
10996
+ }
10997
+ if (LANE_BOUND_STATES.has(child.state) && lane === null)
10998
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a lane`);
10999
+ if ((child.state === "pending" || child.state === "skipped_blocked") && lane !== null)
11000
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} but holds a lane`);
11001
+ const integrated = child.state === "integrated" || child.state === "released";
11002
+ if (integrated && child.commit === null)
11003
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a commit`);
11004
+ if (!integrated && child.commit !== null)
11005
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has a commit but is not integrated`);
11006
+ if (integrated && lane?.lane_commit === null)
11007
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a lane commit`);
11008
+ if (child.state === "lane_committed" && lane?.lane_commit === null)
11009
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is lane_committed without a lane commit`);
11010
+ if ((child.state === "needs_human" || child.state === "skipped_blocked") && child.reason === null)
11011
+ throw new Error(`batch run state ${batchId} child ${child.task_id} needs a terminal reason`);
11012
+ if (seenTaskIds.has(child.task_id))
11013
+ throw new Error(`batch run state ${batchId} has a duplicate child ${child.task_id}`);
11014
+ seenTaskIds.add(child.task_id);
11015
+ }
11016
+ for (const child of record.children) {
11017
+ if (child.commit !== null && !record.commits.includes(child.commit))
11018
+ throw new Error(`batch run state ${batchId} child ${child.task_id} commit is missing from commits`);
11019
+ }
11020
+ const inFlight = (child) => child.state === "lane_admitted" || child.state === "enrolled" || child.state === "settled" || child.state === "lane_committed";
11021
+ if ((record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected") && record.children.some(inFlight))
11022
+ throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
11023
+ if (record.batch_state === "completed" && record.children.some((child) => child.state !== "integrated" && child.state !== "released"))
11024
+ throw new Error(`batch run state ${batchId} is completed but a child is not integrated`);
11025
+ }
11026
+ function prepareBatchLaneRunState(input) {
11027
+ validateBatchId(input.batch_id);
11028
+ return {
11029
+ contract: "assurance_kernel/batch_run_state/v2",
11030
+ batch_id: input.batch_id,
11031
+ initiative_slug: input.initiative_slug,
11032
+ plan_digest: input.plan_digest,
11033
+ base_head: input.base_head,
11034
+ branch: input.branch ?? `imm/${input.initiative_slug}`,
11035
+ confirmation_time: input.confirmation_time,
11036
+ budget: input.budget,
11037
+ max_parallel: input.max_parallel,
11038
+ batch_state: "prepared",
11039
+ children: input.children.map((child) => ({
11040
+ task_id: child.task_id,
11041
+ slice_id: child.slice_id,
11042
+ blocked_by: [...child.blocked_by],
11043
+ state: "pending",
11044
+ reason: null,
11045
+ commit: null,
11046
+ lane: null,
11047
+ qa_failures: 0
11048
+ })),
11049
+ commits: [],
11050
+ created_at: input.now,
11051
+ updated_at: input.now
11052
+ };
11053
+ }
11054
+ function parseBatchLaneRunState(raw, batchId) {
11055
+ validateBatchId(batchId);
11056
+ const parsed = JSON.parse(raw);
11057
+ validateLaneRecordShape(parsed, batchId);
11058
+ return withoutRetiredClock(parsed);
11059
+ }
11060
+ function parseAnyBatchRunState(raw, batchId) {
11061
+ validateBatchId(batchId);
11062
+ const parsed = JSON.parse(raw);
11063
+ if (typeof parsed === "object" && parsed !== null && parsed.contract === "assurance_kernel/batch_run_state/v2")
11064
+ return parseBatchLaneRunState(raw, batchId);
11065
+ return parseBatchRunState(raw, batchId);
11066
+ }
11067
+ function readAnyBatchRunState(root, batchId) {
11068
+ const path = statePath(batchId);
11069
+ if (!existsSync7(join12(root, path)))
11070
+ return null;
11071
+ return parseAnyBatchRunState(readSecureProjectFile(root, path), batchId);
11072
+ }
11073
+ function writeBatchLaneRunState(root, record) {
11074
+ const path = statePath(record.batch_id);
11075
+ validateLaneRecordShape(record, record.batch_id);
11076
+ return withKernelStoreLock(root, () => {
11077
+ const existing = existsSync7(join12(root, path)) ? readSecureProjectFile(root, path) : null;
11078
+ if (existing !== null && existing === canonicalBytes(record))
11079
+ return record;
11080
+ const stored = withoutRetiredClock({ ...record, updated_at: new Date().toISOString() });
11081
+ ensureSecureDirectory2(root, join12(".imm", "state", "batches"));
11082
+ writeFileAtomically(root, path, canonicalBytes(stored));
11083
+ return stored;
11084
+ });
11085
+ }
10921
11086
  function reportPath(batchId) {
10922
11087
  validateBatchId(batchId);
10923
11088
  return join12(".imm", "state", "batches", `${batchId}.report.json`);
@@ -11173,7 +11338,186 @@ async function captureBatchReconfirmation(root, record, children) {
11173
11338
 
11174
11339
  // plugins/immune-brain/runtime/unattended/batch_plan.ts
11175
11340
  import { createHash as createHash19 } from "node:crypto";
11341
+
11342
+ // plugins/immune-brain/runtime/local_initiative.ts
11343
+ import { lstatSync as lstatSync10, readFileSync as readFileSync13 } from "node:fs";
11344
+ import { resolve as resolve12 } from "node:path";
11176
11345
  var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
11346
+ var SLICE_HEADING = /^##\s+([^\s:]+)\s*:/;
11347
+ var BLOCKED_BY = /^blocked by\s*:(.*)$/i;
11348
+ var TASKS = /^tasks\s*:\s*$/i;
11349
+ var BULLET = /^[-*]\s+(.*)$/;
11350
+ function localInitiativePath(initiativeSlug) {
11351
+ return `docs/initiatives/${initiativeSlug}.md`;
11352
+ }
11353
+ function hasLocalInitiative(root, initiativeSlug) {
11354
+ const stat = lstatSync10(resolve12(root, localInitiativePath(initiativeSlug)), { throwIfNoEntry: false });
11355
+ if (!stat)
11356
+ return false;
11357
+ if (!stat.isFile())
11358
+ throw new Error(`${localInitiativePath(initiativeSlug)} must be a regular file`);
11359
+ return true;
11360
+ }
11361
+ function unquote(value) {
11362
+ return value.trim().replace(/^`(.*)`$/, "$1");
11363
+ }
11364
+ function observeLocalInitiative(root, initiativeSlug) {
11365
+ if (!ID_PATTERN2.test(initiativeSlug))
11366
+ throw new Error("initiative_slug is invalid");
11367
+ const path = localInitiativePath(initiativeSlug);
11368
+ if (!hasLocalInitiative(root, initiativeSlug))
11369
+ throw new Error(`Local Initiative ${path} does not exist`);
11370
+ const slices = [];
11371
+ let inTasks = false;
11372
+ let fence = false;
11373
+ for (const raw of readFileSync13(resolve12(root, path), "utf8").split(/\r?\n/)) {
11374
+ const line = raw.trim();
11375
+ if (line.startsWith("```")) {
11376
+ fence = !fence;
11377
+ continue;
11378
+ }
11379
+ if (fence)
11380
+ continue;
11381
+ const heading = SLICE_HEADING.exec(line);
11382
+ if (heading) {
11383
+ slices.push({ slice_id: heading[1], tasks: [], blocked_by: [] });
11384
+ inTasks = false;
11385
+ continue;
11386
+ }
11387
+ const slice = slices.at(-1);
11388
+ if (!slice)
11389
+ continue;
11390
+ if (TASKS.test(line)) {
11391
+ inTasks = true;
11392
+ continue;
11393
+ }
11394
+ const blocked = BLOCKED_BY.exec(line);
11395
+ if (blocked) {
11396
+ slice.blocked_by.push(...blocked[1].split(",").map(unquote).filter(Boolean));
11397
+ inTasks = false;
11398
+ continue;
11399
+ }
11400
+ const bullet = BULLET.exec(line);
11401
+ if (inTasks && bullet)
11402
+ slice.tasks.push(unquote(bullet[1]));
11403
+ else if (line)
11404
+ inTasks = false;
11405
+ }
11406
+ if (!slices.length)
11407
+ throw new Error(`Local Initiative ${path} declares no Slice`);
11408
+ return {
11409
+ contract: "immune_brain/local_initiative_observation/v1",
11410
+ initiative_id: initiativeSlug,
11411
+ path,
11412
+ tasks: slices.map((slice) => {
11413
+ if (slice.tasks.length !== 1)
11414
+ throw new Error(`Local Initiative Slice ${slice.slice_id} must name exactly one Task to run as a batch child`);
11415
+ return { task_id: slice.tasks[0], slice_id: slice.slice_id, blocked_by: slice.blocked_by };
11416
+ })
11417
+ };
11418
+ }
11419
+
11420
+ // plugins/immune-brain/runtime/unattended/batch_schedule.ts
11421
+ var IN_FLIGHT_STATES = new Set([
11422
+ "lane_admitted",
11423
+ "enrolled",
11424
+ "settled",
11425
+ "lane_committed"
11426
+ ]);
11427
+ var DONE_STATES = new Set(["committed", "integrated", "released"]);
11428
+ var WILDCARD = /[*?[\]{}!]/;
11429
+ function scopePrefix(entry) {
11430
+ if (typeof entry !== "string")
11431
+ return [];
11432
+ let value = entry.trim().toLowerCase();
11433
+ while (value.startsWith("./"))
11434
+ value = value.slice(2);
11435
+ if (!value || value.startsWith("/") || value.includes("\\"))
11436
+ return [];
11437
+ const wildcard = value.search(WILDCARD);
11438
+ const literal = wildcard === -1 ? value : value.slice(0, value.lastIndexOf("/", wildcard) + 1);
11439
+ const segments = literal.split("/").filter((segment) => segment.length > 0);
11440
+ if (segments.some((segment) => segment === "." || segment === ".."))
11441
+ return [];
11442
+ return segments;
11443
+ }
11444
+ function prefixOverlaps(left, right) {
11445
+ if (!left.length || !right.length)
11446
+ return true;
11447
+ const shared = Math.min(left.length, right.length);
11448
+ for (let index = 0;index < shared; index += 1)
11449
+ if (left[index] !== right[index])
11450
+ return false;
11451
+ return true;
11452
+ }
11453
+ function scopesOverlap(left, right) {
11454
+ if (!left.length || !right.length)
11455
+ return true;
11456
+ const rightPrefixes = right.map(scopePrefix);
11457
+ return left.some((entry) => {
11458
+ const prefix = scopePrefix(entry);
11459
+ return rightPrefixes.some((other) => prefixOverlaps(prefix, other));
11460
+ });
11461
+ }
11462
+ function positiveLimit(maxParallel) {
11463
+ if (maxParallel === Number.POSITIVE_INFINITY)
11464
+ return maxParallel;
11465
+ if (!Number.isSafeInteger(maxParallel) || maxParallel <= 0)
11466
+ throw new Error("max_parallel must be a positive safe integer");
11467
+ return maxParallel;
11468
+ }
11469
+ function startableChildren(children, maxParallel) {
11470
+ const limit = positiveLimit(maxParallel);
11471
+ const done = new Set(children.filter((child) => DONE_STATES.has(child.state)).map((child) => child.task_id));
11472
+ const occupied = children.filter((child) => IN_FLIGHT_STATES.has(child.state));
11473
+ const startable = [];
11474
+ for (const child of children) {
11475
+ if (occupied.length >= limit)
11476
+ break;
11477
+ if (child.state !== "pending")
11478
+ continue;
11479
+ if (!child.blocked_by.every((id) => done.has(id)))
11480
+ continue;
11481
+ if (occupied.some((other) => scopesOverlap(child.scope_hint, other.scope_hint)))
11482
+ continue;
11483
+ startable.push(child.task_id);
11484
+ occupied.push(child);
11485
+ }
11486
+ return startable;
11487
+ }
11488
+ function projectParallelGroups(children) {
11489
+ const remaining = [...children];
11490
+ const placed = new Set;
11491
+ const groups = [];
11492
+ const conflicts = new Map;
11493
+ while (remaining.length) {
11494
+ const ready = remaining.filter((child) => child.blocked_by.every((id) => placed.has(id)));
11495
+ if (!ready.length)
11496
+ throw new Error("Initiative Task dependencies must form an acyclic graph");
11497
+ const wave = [];
11498
+ for (const child of ready) {
11499
+ const overlapped = wave.filter((member) => scopesOverlap(child.scope_hint, member.scope_hint));
11500
+ if (overlapped.length) {
11501
+ const known = conflicts.get(child.task_id) ?? new Set;
11502
+ for (const member of overlapped)
11503
+ known.add(member.task_id);
11504
+ conflicts.set(child.task_id, known);
11505
+ continue;
11506
+ }
11507
+ wave.push(child);
11508
+ }
11509
+ groups.push(wave.map((child) => child.task_id));
11510
+ for (const child of wave) {
11511
+ placed.add(child.task_id);
11512
+ remaining.splice(remaining.indexOf(child), 1);
11513
+ }
11514
+ }
11515
+ const scopeConflicts = children.filter((child) => conflicts.has(child.task_id)).map((child) => ({ task_id: child.task_id, overlaps_with: [...conflicts.get(child.task_id)].sort() }));
11516
+ return { parallel_groups: groups, scope_conflicts: scopeConflicts };
11517
+ }
11518
+
11519
+ // plugins/immune-brain/runtime/unattended/batch_plan.ts
11520
+ var ID_PATTERN3 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
11177
11521
  var DEFAULT_QA_FAILURE_LIMIT = 2;
11178
11522
  function specBindingReason(inspection) {
11179
11523
  if (inspection.missing.length > 0)
@@ -11197,12 +11541,14 @@ function timestamp(value, name) {
11197
11541
  throw new Error(`${name} must be an ISO timestamp`);
11198
11542
  return { milliseconds, iso: new Date(milliseconds).toISOString() };
11199
11543
  }
11544
+ var observeInitiative = async (root, initiativeSlug) => hasLocalInitiative(root, initiativeSlug) ? observeLocalInitiative(root, initiativeSlug) : observeGithubInitiative(root, initiativeSlug);
11200
11545
  function normalizeObservation(value, initiativeSlug) {
11201
- if (!value || value.contract !== "immune_brain/github_initiative_observation/v1")
11546
+ const local = value?.contract === "immune_brain/local_initiative_observation/v1";
11547
+ if (!value || !local && value.contract !== "immune_brain/github_initiative_observation/v1")
11202
11548
  throw new Error("tracker returned an invalid Initiative observation contract");
11203
11549
  if (value.initiative_id !== initiativeSlug)
11204
11550
  throw new Error("tracker returned an observation for another Initiative");
11205
- if (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0)
11551
+ if (!local && (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0))
11206
11552
  throw new Error("tracker returned an invalid Initiative Issue number");
11207
11553
  if (!Array.isArray(value.tasks))
11208
11554
  throw new Error("tracker returned an invalid Initiative Task list");
@@ -11211,13 +11557,14 @@ function normalizeObservation(value, initiativeSlug) {
11211
11557
  const tasks = value.tasks.map((task, index) => {
11212
11558
  if (!task || typeof task !== "object")
11213
11559
  throw new Error(`tracker Task ${index} is invalid`);
11214
- if (!ID_PATTERN2.test(task.task_id))
11560
+ if (!ID_PATTERN3.test(task.task_id))
11215
11561
  throw new Error(`tracker Task ${index} has an invalid task_id`);
11216
- if (!ID_PATTERN2.test(task.slice_id))
11562
+ if (!ID_PATTERN3.test(task.slice_id))
11217
11563
  throw new Error(`tracker Task ${index} has an invalid slice_id`);
11218
- if (!Number.isSafeInteger(task.issue_number) || task.issue_number <= 0)
11564
+ const issueNumber = task.issue_number;
11565
+ if (!local && (!Number.isSafeInteger(issueNumber) || issueNumber <= 0))
11219
11566
  throw new Error(`tracker Task ${task.task_id} has an invalid Issue number`);
11220
- if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN2.test(id)))
11567
+ if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN3.test(id)))
11221
11568
  throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
11222
11569
  if (taskIds.has(task.task_id))
11223
11570
  throw new Error(`tracker returned duplicate Task ${task.task_id}`);
@@ -11273,8 +11620,8 @@ function budget(input, enrollableCount) {
11273
11620
  const qaFailureLimit = input.budget?.qa_failure_limit === undefined ? DEFAULT_QA_FAILURE_LIMIT : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
11274
11621
  return { max_children: maxChildren, qa_failure_limit: qaFailureLimit };
11275
11622
  }
11276
- async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeGithubInitiative) {
11277
- if (!ID_PATTERN2.test(initiativeSlug))
11623
+ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeInitiative) {
11624
+ if (!ID_PATTERN3.test(initiativeSlug))
11278
11625
  throw new Error("initiative_slug is invalid");
11279
11626
  if (!input || typeof input !== "object")
11280
11627
  throw new Error("batch plan input is required");
@@ -11283,6 +11630,7 @@ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = ob
11283
11630
  const order = dependencyOrder(trackerObservation);
11284
11631
  const closures = dependencyClosures(order);
11285
11632
  const children = [];
11633
+ const scopeHints = new Map;
11286
11634
  for (const task of order) {
11287
11635
  const blockedBy = closures.get(task.task_id);
11288
11636
  const child = { task_id: task.task_id, slice_id: task.slice_id, blocked_by: blockedBy };
@@ -11324,6 +11672,7 @@ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = ob
11324
11672
  });
11325
11673
  continue;
11326
11674
  }
11675
+ scopeHints.set(task.task_id, [...read.intent.scope_hint]);
11327
11676
  children.push({
11328
11677
  ...child,
11329
11678
  status: "enrollable",
@@ -11369,7 +11718,12 @@ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = ob
11369
11718
  children,
11370
11719
  enrollable,
11371
11720
  plan_digest: planDigest,
11372
- budget: budget(input, enrollable.length)
11721
+ budget: budget(input, enrollable.length),
11722
+ ...projectParallelGroups(enrollable.map((child) => ({
11723
+ task_id: child.task_id,
11724
+ blocked_by: child.blocked_by,
11725
+ scope_hint: scopeHints.get(child.task_id)
11726
+ })))
11373
11727
  };
11374
11728
  }
11375
11729
 
@@ -11399,8 +11753,8 @@ function findResumableBatchSlugForTask(root, taskId) {
11399
11753
  if (!file.endsWith(".json"))
11400
11754
  continue;
11401
11755
  try {
11402
- const record = JSON.parse(readFileSync13(join13(batchesDir, file), "utf8"));
11403
- if (record?.contract !== "assurance_kernel/batch_run_state/v1")
11756
+ const record = JSON.parse(readFileSync14(join13(batchesDir, file), "utf8"));
11757
+ if (record?.contract !== "assurance_kernel/batch_run_state/v1" && record?.contract !== "assurance_kernel/batch_run_state/v2")
11404
11758
  continue;
11405
11759
  if (record.batch_state !== "running" && record.batch_state !== "needs_human")
11406
11760
  continue;
@@ -11421,12 +11775,12 @@ function findExistingActiveBatch(root, initiativeSlug) {
11421
11775
  continue;
11422
11776
  let record;
11423
11777
  try {
11424
- record = JSON.parse(readFileSync13(join13(batchesDir, file), "utf8"));
11778
+ record = JSON.parse(readFileSync14(join13(batchesDir, file), "utf8"));
11425
11779
  } catch {
11426
11780
  return { corrupt: true, path: file };
11427
11781
  }
11428
11782
  const candidate = record;
11429
- if (candidate?.contract !== "assurance_kernel/batch_run_state/v1")
11783
+ if (candidate?.contract !== "assurance_kernel/batch_run_state/v1" && candidate?.contract !== "assurance_kernel/batch_run_state/v2")
11430
11784
  continue;
11431
11785
  if (candidate.initiative_slug !== initiativeSlug)
11432
11786
  continue;
@@ -11445,7 +11799,7 @@ function findExistingActiveBatch(root, initiativeSlug) {
11445
11799
  if (isTerminalBatchState(candidate.batch_state))
11446
11800
  continue;
11447
11801
  try {
11448
- const record = readBatchRunState(root, file.slice(0, -5));
11802
+ const record = readAnyBatchRunState(root, file.slice(0, -5));
11449
11803
  if (record)
11450
11804
  return { corrupt: false, record };
11451
11805
  } catch {}
@@ -11463,12 +11817,12 @@ function findSettledBatchRecord(root, initiativeSlug) {
11463
11817
  continue;
11464
11818
  let record;
11465
11819
  try {
11466
- record = JSON.parse(readFileSync13(join13(batchesDir, file), "utf8"));
11820
+ record = JSON.parse(readFileSync14(join13(batchesDir, file), "utf8"));
11467
11821
  } catch {
11468
11822
  continue;
11469
11823
  }
11470
11824
  const candidate = record;
11471
- if (candidate?.contract !== "assurance_kernel/batch_run_state/v1")
11825
+ if (candidate?.contract !== "assurance_kernel/batch_run_state/v1" && candidate?.contract !== "assurance_kernel/batch_run_state/v2")
11472
11826
  continue;
11473
11827
  if (candidate.initiative_slug !== initiativeSlug)
11474
11828
  continue;
@@ -11612,6 +11966,8 @@ async function projectPlanSurface(input) {
11612
11966
  let recoveryChildren = [];
11613
11967
  let planDigest;
11614
11968
  let excluded = [];
11969
+ let parallelGroups = [];
11970
+ let scopeConflicts = [];
11615
11971
  const riskByTask = new Map;
11616
11972
  let budget;
11617
11973
  if (isResuming && existingBatch) {
@@ -11645,7 +12001,7 @@ async function projectPlanSurface(input) {
11645
12001
  }
11646
12002
  }
11647
12003
  riskByTask.set(c.task_id, read.intent?.risk ?? "material");
11648
- const isDone = c.state === "committed" || c.state === "settled";
12004
+ const isDone = ["committed", "settled", "integrated", "released"].includes(c.state);
11649
12005
  return {
11650
12006
  task_id: c.task_id,
11651
12007
  slice_id: c.slice_id,
@@ -11680,6 +12036,8 @@ async function projectPlanSurface(input) {
11680
12036
  if (!plan.enrollable.length)
11681
12037
  return { ok: false, key: "empty_enrollable_set", detail: "" };
11682
12038
  budget = plan.budget;
12039
+ parallelGroups = plan.parallel_groups;
12040
+ scopeConflicts = plan.scope_conflicts;
11683
12041
  const enrollableChildById = new Map(plan.enrollable.map((c) => [c.task_id, c]));
11684
12042
  recoveryChildren = plan.children.filter((c) => c.status === "enrollable").map((c) => {
11685
12043
  const digestChild = enrollableChildById.get(c.task_id);
@@ -11705,7 +12063,9 @@ async function projectPlanSurface(input) {
11705
12063
  plan_digest: planDigest,
11706
12064
  recovery_children: recoveryChildren,
11707
12065
  risk_by_task: Object.fromEntries([...riskByTask.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)),
11708
- excluded
12066
+ excluded,
12067
+ parallel_groups: parallelGroups,
12068
+ scope_conflicts: scopeConflicts
11709
12069
  }
11710
12070
  };
11711
12071
  }
@@ -11785,7 +12145,7 @@ async function projectBatchPreflight(options) {
11785
12145
  if (!planSurface.ok)
11786
12146
  return reject(planSurface.key, planSurface.detail);
11787
12147
  let reconfirmation;
11788
- if (activeRecord && activeRecord.plan_digest !== planSurface.surface.plan_digest) {
12148
+ if (activeRecord && !isLaneBatchRecord(activeRecord) && activeRecord.plan_digest !== planSurface.surface.plan_digest) {
11789
12149
  try {
11790
12150
  reconfirmation = await captureBatchReconfirmation(root, activeRecord, planSurface.surface.recovery_children);
11791
12151
  } catch {
@@ -11802,7 +12162,9 @@ async function projectBatchPreflight(options) {
11802
12162
  plan_digest: planSurface.surface.plan_digest,
11803
12163
  recovery_children: planSurface.surface.recovery_children,
11804
12164
  risk_by_task: planSurface.surface.risk_by_task,
11805
- excluded: planSurface.surface.excluded
12165
+ excluded: planSurface.surface.excluded,
12166
+ parallel_groups: planSurface.surface.parallel_groups,
12167
+ scope_conflicts: planSurface.surface.scope_conflicts
11806
12168
  };
11807
12169
  if (reconfirmation)
11808
12170
  Object.defineProperty(projection, "reconfirmation", { value: reconfirmation });
@@ -11849,7 +12211,7 @@ async function authorizeBatch(options) {
11849
12211
  const branchBefore = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11850
12212
  if (branchBefore.status !== 0)
11851
12213
  return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
11852
- const ownsUnpersistedHead = isResuming && existingBatch !== null && existingBatch.plan_digest === planDigest && expectedBatchHead(existingBatch) !== baseHead && ownUnpersistedBatchHead(root, existingBatch, baseHead);
12214
+ const ownsUnpersistedHead = isResuming && existingBatch !== null && !isLaneBatchRecord(existingBatch) && existingBatch.plan_digest === planDigest && expectedBatchHead(existingBatch) !== baseHead && ownUnpersistedBatchHead(root, existingBatch, baseHead);
11853
12215
  const fastForwardsRecordedHead = isResuming && existingBatch !== null && existingBatch.branch === batchBranch && expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead && classifyBatchLineage({
11854
12216
  root,
11855
12217
  branch: batchBranch,
@@ -11884,7 +12246,9 @@ async function authorizeBatch(options) {
11884
12246
  risk: projection.risk_by_task[child.task_id] ?? "material",
11885
12247
  status: child.status
11886
12248
  })),
11887
- excluded: projection.excluded
12249
+ excluded: projection.excluded,
12250
+ parallel_groups: projection.parallel_groups,
12251
+ scope_conflicts: projection.scope_conflicts
11888
12252
  };
11889
12253
  let requestId = null;
11890
12254
  if (!reuseAuthorization) {
@@ -11951,9 +12315,9 @@ async function authorizeBatch(options) {
11951
12315
  }
11952
12316
 
11953
12317
  // plugins/immune-brain/runtime/unattended/batch_runner.ts
11954
- import { spawnSync as spawnSync9 } from "node:child_process";
11955
- import { existsSync as existsSync10 } from "node:fs";
11956
- import { join as join15 } from "node:path";
12318
+ import { spawnSync as spawnSync11 } from "node:child_process";
12319
+ import { existsSync as existsSync11 } from "node:fs";
12320
+ import { join as join16 } from "node:path";
11957
12321
 
11958
12322
  // plugins/immune-brain/runtime/unattended/batch_git.ts
11959
12323
  import { spawnSync as spawnSync8 } from "node:child_process";
@@ -11962,7 +12326,7 @@ import {
11962
12326
  constants as constants6,
11963
12327
  closeSync as closeSync7,
11964
12328
  existsSync as existsSync9,
11965
- lstatSync as lstatSync10,
12329
+ lstatSync as lstatSync11,
11966
12330
  mkdirSync as mkdirSync7,
11967
12331
  openSync as openSync7,
11968
12332
  realpathSync as realpathSync11,
@@ -12190,7 +12554,7 @@ function ensureSecureDirectory3(root, relativePath) {
12190
12554
  for (const segment of segments) {
12191
12555
  current = join14(current, segment);
12192
12556
  if (existsSync9(current)) {
12193
- const stats = lstatSync10(current);
12557
+ const stats = lstatSync11(current);
12194
12558
  if (stats.isSymbolicLink() || !stats.isDirectory()) {
12195
12559
  throw new Error(`${segment} exists but is not a real directory`);
12196
12560
  }
@@ -12204,12 +12568,12 @@ function writeFileAtomically2(root, relativePath, bytes) {
12204
12568
  const target = join14(root, relativePath);
12205
12569
  const targetDir = dirname8(target);
12206
12570
  ensureSecureDirectory3(root, dirname8(relativePath));
12207
- const stats = lstatSync10(targetDir);
12571
+ const stats = lstatSync11(targetDir);
12208
12572
  if (stats.isSymbolicLink() || !stats.isDirectory()) {
12209
12573
  throw new Error(`${dirname8(relativePath)} is not a real directory`);
12210
12574
  }
12211
12575
  if (existsSync9(target)) {
12212
- const targetStats = lstatSync10(target);
12576
+ const targetStats = lstatSync11(target);
12213
12577
  if (targetStats.isSymbolicLink()) {
12214
12578
  throw new Error(`${relativePath} is a symlink`);
12215
12579
  }
@@ -12518,6 +12882,877 @@ function createDefaultBatchGitPort() {
12518
12882
  };
12519
12883
  }
12520
12884
 
12885
+ // plugins/immune-brain/runtime/unattended/batch_lanes.ts
12886
+ import { spawnSync as spawnSync10 } from "node:child_process";
12887
+ import { existsSync as existsSync10, realpathSync as realpathSync12 } from "node:fs";
12888
+ import { isAbsolute as isAbsolute6, join as join15 } from "node:path";
12889
+
12890
+ // plugins/immune-brain/runtime/unattended/batch_integration.ts
12891
+ import { spawnSync as spawnSync9 } from "node:child_process";
12892
+ class BatchIntegrationError extends Error {
12893
+ reason;
12894
+ constructor(reason, message) {
12895
+ super(message);
12896
+ this.reason = reason;
12897
+ this.name = "BatchIntegrationError";
12898
+ }
12899
+ }
12900
+ var COMMITTER_ENV = {
12901
+ GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || "Immune-Brain Batch",
12902
+ GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || "immune-brain@local"
12903
+ };
12904
+ function git5(root, args, extra = {}) {
12905
+ const result = spawnSync9("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "-c", "commit.gpgsign=false", ...args], {
12906
+ encoding: "utf8",
12907
+ stdio: ["pipe", "pipe", "pipe"],
12908
+ input: extra.input,
12909
+ env: { ...process.env, ...COMMITTER_ENV, ...extra.env ?? {} }
12910
+ });
12911
+ return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
12912
+ }
12913
+ function gitOut(root, args, what) {
12914
+ const result = git5(root, args);
12915
+ if (result.status !== 0)
12916
+ throw new Error(`${what}: ${result.stderr.trim() || "git failed"}`);
12917
+ return result.stdout;
12918
+ }
12919
+ function changeIdentity(root, from, to) {
12920
+ const raw = gitOut(root, ["diff-tree", "-r", "-z", "--raw", "--no-renames", "--no-ext-diff", "--no-commit-id", from, to], `failed to inspect change ${from}..${to}`);
12921
+ const tokens = raw.split("\x00");
12922
+ const identity = new Map;
12923
+ for (let index = 0;index < tokens.length; index += 1) {
12924
+ const meta = tokens[index];
12925
+ if (!meta.startsWith(":"))
12926
+ continue;
12927
+ const [, newMode, , newSha] = meta.slice(1).split(" ");
12928
+ const path = tokens[index + 1];
12929
+ if (path === undefined || !newMode || !newSha)
12930
+ throw new Error(`unreadable change record for ${from}..${to}`);
12931
+ identity.set(path, `${newMode} ${newSha}`);
12932
+ index += 1;
12933
+ }
12934
+ return identity;
12935
+ }
12936
+ function identitiesEqual(left, right) {
12937
+ if (left.size !== right.size)
12938
+ return false;
12939
+ for (const [path, value] of left)
12940
+ if (right.get(path) !== value)
12941
+ return false;
12942
+ return true;
12943
+ }
12944
+ function singleParent(root, commit) {
12945
+ const parents = gitOut(root, ["rev-parse", `${commit}^@`], `failed to read parents of ${commit}`).split(/\s+/).filter(Boolean);
12946
+ if (parents.length !== 1)
12947
+ throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: lane commit ${commit} has ${parents.length} parents, expected exactly one`);
12948
+ return parents[0];
12949
+ }
12950
+ function isAncestor(root, ancestor, descendant) {
12951
+ return git5(root, ["merge-base", "--is-ancestor", ancestor, descendant]).status === 0;
12952
+ }
12953
+ function buildCandidate(input) {
12954
+ const { root, batch_head: head, lane_base: base, lane_commit: laneCommit } = input;
12955
+ if (head === base)
12956
+ return laneCommit;
12957
+ const merged = git5(root, ["merge-tree", "--write-tree", `--merge-base=${base}`, head, laneCommit]);
12958
+ if (merged.status === 1)
12959
+ throw new BatchIntegrationError("batch_integration_conflict", `batch_integration_conflict: lane commit ${laneCommit} does not apply cleanly onto batch head ${head}`);
12960
+ if (merged.status !== 0)
12961
+ throw new Error(`failed to build integration candidate: ${merged.stderr.trim() || "git merge-tree failed"}`);
12962
+ const tree = merged.stdout.split(`
12963
+ `)[0]?.trim();
12964
+ if (!tree)
12965
+ throw new Error("failed to build integration candidate: no tree produced");
12966
+ const message = gitOut(root, ["log", "-n", "1", "--format=%B", laneCommit], "failed to read lane commit message");
12967
+ const author = gitOut(root, ["log", "-n", "1", "--format=%an%x00%ae%x00%aI", laneCommit], "failed to read lane commit author").trim().split("\x00");
12968
+ const created = git5(root, ["commit-tree", tree, "-p", head, "-F", "-"], {
12969
+ input: message,
12970
+ env: {
12971
+ GIT_AUTHOR_NAME: author[0] ?? COMMITTER_ENV.GIT_COMMITTER_NAME,
12972
+ GIT_AUTHOR_EMAIL: author[1] ?? COMMITTER_ENV.GIT_COMMITTER_EMAIL,
12973
+ ...author[2] ? { GIT_AUTHOR_DATE: author[2] } : {}
12974
+ }
12975
+ });
12976
+ const candidate = created.stdout.trim();
12977
+ if (created.status !== 0 || !candidate)
12978
+ throw new Error(`failed to build integration candidate: ${created.stderr.trim() || "git commit-tree failed"}`);
12979
+ return candidate;
12980
+ }
12981
+ function prepareLaneCandidate(input) {
12982
+ const { root, branch, batch_head: head, lane_base: base, lane_commit: laneCommit } = input;
12983
+ const currentBranch = git5(root, ["symbolic-ref", "--short", "HEAD"]);
12984
+ if (currentBranch.status !== 0 || currentBranch.stdout.trim() !== branch)
12985
+ throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: current branch ${currentBranch.stdout.trim()} does not match expected branch ${branch}`);
12986
+ const currentHead = git5(root, ["rev-parse", "HEAD"]).stdout.trim();
12987
+ if (currentHead !== head)
12988
+ throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: current HEAD ${currentHead} does not match expected batch head ${head}`);
12989
+ if (singleParent(root, laneCommit) !== base)
12990
+ throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: lane commit ${laneCommit} does not descend from lane base ${base}`);
12991
+ if (!isAncestor(root, base, head))
12992
+ throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: lane base ${base} is not an ancestor of batch head ${head}`);
12993
+ const candidate = buildCandidate({ root, batch_head: head, lane_base: base, lane_commit: laneCommit });
12994
+ if (!identitiesEqual(changeIdentity(root, base, laneCommit), changeIdentity(root, head, candidate)))
12995
+ throw new BatchIntegrationError("batch_integration_conflict", `batch_integration_conflict: candidate ${candidate} does not carry the lane commit ${laneCommit} change`);
12996
+ return candidate;
12997
+ }
12998
+ function fastForwardToCandidate(root, candidate) {
12999
+ const moved = git5(root, ["merge", "--ff-only", "--quiet", candidate]);
13000
+ if (moved.status !== 0)
13001
+ throw new BatchIntegrationError("batch_integration_conflict", `batch_integration_conflict: batch branch could not fast-forward to ${candidate}: ${moved.stderr.trim() || "git merge failed"}`);
13002
+ const landed = git5(root, ["rev-parse", "HEAD"]).stdout.trim();
13003
+ if (landed !== candidate)
13004
+ throw new Error(`batch branch landed on ${landed}, expected ${candidate}`);
13005
+ return { commit: candidate };
13006
+ }
13007
+ function commitsBetween(root, from, to) {
13008
+ return new Set(gitOut(root, ["rev-list", `${from}..${to}`], `failed to list commits ${from}..${to}`).split(/\s+/).filter(Boolean));
13009
+ }
13010
+ function readCandidateAcceptance(root, candidate, check) {
13011
+ if (!check.intent_path)
13012
+ throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: ${check.task_id} has no intent to rerun`);
13013
+ const shown = git5(root, ["show", `${candidate}:${check.intent_path}`]);
13014
+ if (shown.status !== 0)
13015
+ throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: intent ${check.intent_path} of ${check.task_id} is absent from candidate ${candidate}`);
13016
+ try {
13017
+ const intent = parseTaskIntentV1(JSON.parse(shown.stdout));
13018
+ return intent.acceptance.map((item) => ({ id: `${check.task_id}:${item.id}`, assertion: item.assertion, verification: item.verification }));
13019
+ } catch (error) {
13020
+ throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: intent of ${check.task_id} is unreadable in candidate ${candidate}: ${error instanceof Error ? error.message : String(error)}`);
13021
+ }
13022
+ }
13023
+ async function verifyCandidateDescriptors(input) {
13024
+ const { root, candidate } = input;
13025
+ const since = commitsBetween(root, input.lane_base, input.batch_head);
13026
+ const checks = [input.child, ...input.siblings.filter((sibling) => sibling.commit !== null && since.has(sibling.commit))];
13027
+ const acceptance = checks.flatMap((check) => readCandidateAcceptance(root, candidate, check));
13028
+ const descriptors = new Map;
13029
+ try {
13030
+ for (const item of acceptance)
13031
+ descriptors.set(item.id, parseVerificationDescriptor(item.verification));
13032
+ } catch (error) {
13033
+ throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: a verification descriptor is invalid: ${error instanceof Error ? error.message : String(error)}`);
13034
+ }
13035
+ if (acceptance.length === 0)
13036
+ throw new BatchIntegrationError("batch_integration_check_failed", "batch_integration_check_failed: no descriptor to rerun");
13037
+ const tree = gitOut(root, ["rev-parse", `${candidate}^{tree}`], `failed to read the tree of ${candidate}`).trim();
13038
+ const snapshot = {
13039
+ contract: "assurance_kernel/assurance_snapshot/v2",
13040
+ task_id: input.child.task_id,
13041
+ run_id: null,
13042
+ role: "qa",
13043
+ record_revision: "",
13044
+ workspace_revision: "",
13045
+ intent_revision: 0,
13046
+ intent_content_hash: "",
13047
+ diff_hash: "",
13048
+ lifecycle: "active",
13049
+ artifact_state: "frozen",
13050
+ risk: "material",
13051
+ fresh_acceptance_ids: [],
13052
+ missing_acceptance_ids: [],
13053
+ stale_attestation_ids: [],
13054
+ acceptance,
13055
+ dirty_files: [],
13056
+ review_bundle_digest: null,
13057
+ review_revision: {
13058
+ contract: "assurance_kernel/review_revision_identity/v1",
13059
+ base_head: input.lane_base,
13060
+ review_commit: candidate,
13061
+ review_tree: tree,
13062
+ manifest_digest: ""
13063
+ },
13064
+ root
13065
+ };
13066
+ let verdict;
13067
+ try {
13068
+ verdict = await runDeterministicQa(snapshot, descriptors, { signal: input.signal });
13069
+ } catch (error) {
13070
+ if (error instanceof VerificationAbortedError)
13071
+ throw error;
13072
+ throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: the descriptor rerun could not complete: ${error instanceof Error ? error.message : String(error)}`);
13073
+ }
13074
+ if (verdict.decision !== "pass")
13075
+ throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: ${(verdict.findings ?? []).map((finding) => finding.acceptance_id).join(", ") || "a descriptor"} failed on candidate ${candidate}`);
13076
+ }
13077
+ async function integrateGuardedLaneCommit(input) {
13078
+ const candidate = prepareLaneCandidate(input);
13079
+ await verifyCandidateDescriptors({
13080
+ root: input.root,
13081
+ candidate,
13082
+ lane_base: input.lane_base,
13083
+ batch_head: input.batch_head,
13084
+ child: input.child,
13085
+ siblings: input.siblings,
13086
+ signal: input.signal
13087
+ });
13088
+ return fastForwardToCandidate(input.root, candidate);
13089
+ }
13090
+ function findIntegratedCandidate(input) {
13091
+ const { root, task_id: taskId, batch_id: batchId, from_head: from, lane_base: base, lane_commit: laneCommit } = input;
13092
+ const listed = git5(root, [
13093
+ "log",
13094
+ "--fixed-strings",
13095
+ `--grep=imm(${taskId}):`,
13096
+ "--format=%x01%H%x00%(trailers:key=Immune-Brain-Batch,valueonly)%x00%s",
13097
+ `${from}..HEAD`
13098
+ ]);
13099
+ if (listed.status !== 0)
13100
+ throw new Error(`failed to search for integrated ${taskId}: ${listed.stderr.trim()}`);
13101
+ const lane = changeIdentity(root, base, laneCommit);
13102
+ for (const record of listed.stdout.split("\x01")) {
13103
+ const [commit, trailer, subject] = record.split("\x00");
13104
+ if (!commit || trailer?.trim() !== batchId || !subject?.trim().startsWith(`imm(${taskId}):`))
13105
+ continue;
13106
+ const parent = git5(root, ["rev-parse", `${commit}^`]).stdout.trim();
13107
+ if (parent && identitiesEqual(lane, changeIdentity(root, parent, commit)))
13108
+ return commit;
13109
+ }
13110
+ return null;
13111
+ }
13112
+
13113
+ // plugins/immune-brain/runtime/unattended/batch_lanes.ts
13114
+ var PARALLEL_MISMATCH = "batch_parallel_mismatch";
13115
+ var TASK_ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
13116
+ var MAX_LANE_OFFERS = 64;
13117
+ var MAX_PATH_LENGTH = 4096;
13118
+ function resolveLaneParallel(root, initiativeSlug, maxParallel, laneOffers) {
13119
+ if (maxParallel !== undefined || laneOffers === undefined)
13120
+ return maxParallel;
13121
+ const lookup = findExistingActiveBatch(root, initiativeSlug);
13122
+ if (lookup === null || lookup.corrupt || !isLaneBatchRecord(lookup.record))
13123
+ throw new Error("lane_offers requires max_parallel unless a recorded lane batch is being resumed");
13124
+ return lookup.record.max_parallel;
13125
+ }
13126
+ function parseMaxParallel(value) {
13127
+ if (value === undefined || value === null)
13128
+ return;
13129
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0)
13130
+ throw new Error("invalid max_parallel: expected a positive integer");
13131
+ return value;
13132
+ }
13133
+ function parseLaneOffers(value) {
13134
+ if (value === undefined || value === null)
13135
+ return;
13136
+ if (!Array.isArray(value))
13137
+ throw new Error("invalid lane_offers: expected an array");
13138
+ if (value.length > MAX_LANE_OFFERS)
13139
+ throw new Error(`invalid lane_offers: at most ${MAX_LANE_OFFERS} offers`);
13140
+ const seen = new Set;
13141
+ return value.map((entry) => {
13142
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
13143
+ throw new Error("invalid lane_offers: each offer must be an object");
13144
+ const { task_id: taskId, path, ...rest } = entry;
13145
+ if (Object.keys(rest).length > 0)
13146
+ throw new Error("invalid lane_offers: unknown offer field");
13147
+ if (typeof taskId !== "string" || !TASK_ID_PATTERN2.test(taskId))
13148
+ throw new Error("invalid lane_offers: task_id is not a valid task id");
13149
+ if (typeof path !== "string" || !path || path.length > MAX_PATH_LENGTH || path.includes("\x00") || !isAbsolute6(path))
13150
+ throw new Error("invalid lane_offers: path must be an absolute path");
13151
+ if (seen.has(taskId))
13152
+ throw new Error(`invalid lane_offers: duplicate offer for ${taskId}`);
13153
+ seen.add(taskId);
13154
+ return { task_id: taskId, path };
13155
+ });
13156
+ }
13157
+ function laneBranchName(initiativeSlug, taskId) {
13158
+ return `imm-lane/${initiativeSlug}/${taskId}`;
13159
+ }
13160
+ function gitRead(root, args) {
13161
+ const result = spawnSync10("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13162
+ return { status: result.status, stdout: (result.stdout ?? "").trim() };
13163
+ }
13164
+ function commonDir(root) {
13165
+ const result = gitRead(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
13166
+ if (result.status !== 0 || !result.stdout)
13167
+ return null;
13168
+ try {
13169
+ return realpathSync12(result.stdout);
13170
+ } catch {
13171
+ return null;
13172
+ }
13173
+ }
13174
+ function resolveRealPath(path) {
13175
+ try {
13176
+ return realpathSync12(path);
13177
+ } catch {
13178
+ return null;
13179
+ }
13180
+ }
13181
+ function createDefaultLaneGitPort() {
13182
+ return {
13183
+ resolveRoot: (root) => resolveRealPath(root) ?? root,
13184
+ inspectLane(coordinatorRoot, lanePath) {
13185
+ const real = existsSync10(lanePath) ? resolveRealPath(lanePath) : null;
13186
+ const absent = {
13187
+ exists: real !== null,
13188
+ real_path: real,
13189
+ same_repository: false,
13190
+ is_worktree_root: false,
13191
+ branch: null,
13192
+ head: null,
13193
+ clean: false,
13194
+ active_claim_task_id: null
13195
+ };
13196
+ if (!real)
13197
+ return absent;
13198
+ const laneCommon = commonDir(real);
13199
+ const coordinatorCommon = commonDir(coordinatorRoot);
13200
+ if (!laneCommon || !coordinatorCommon || laneCommon !== coordinatorCommon)
13201
+ return absent;
13202
+ const top = gitRead(real, ["rev-parse", "--show-toplevel"]);
13203
+ const topReal = top.status === 0 ? resolveRealPath(top.stdout) : null;
13204
+ const branch = gitRead(real, ["symbolic-ref", "--short", "HEAD"]);
13205
+ const head = gitRead(real, ["rev-parse", "HEAD"]);
13206
+ const status = gitRead(real, ["status", "--porcelain", "--untracked-files=all", "--ignore-submodules=none"]);
13207
+ let activeClaim = null;
13208
+ try {
13209
+ activeClaim = readActiveClaimTaskId(real);
13210
+ } catch {
13211
+ activeClaim = "unreadable";
13212
+ }
13213
+ return {
13214
+ exists: true,
13215
+ real_path: real,
13216
+ same_repository: true,
13217
+ is_worktree_root: topReal === real,
13218
+ branch: branch.status === 0 ? branch.stdout : null,
13219
+ head: head.status === 0 ? head.stdout : null,
13220
+ clean: status.status === 0 && status.stdout === "",
13221
+ active_claim_task_id: activeClaim
13222
+ };
13223
+ },
13224
+ integrate: integrateGuardedLaneCommit,
13225
+ findIntegrated: findIntegratedCandidate,
13226
+ auditReachable({ root, head, task_id }) {
13227
+ const listing = gitRead(root, ["ls-tree", "-r", "--name-only", head, "--", `.imm/audit/${task_id}`]);
13228
+ if (listing.status !== 0)
13229
+ return false;
13230
+ const files = listing.stdout.split(`
13231
+ `);
13232
+ const runDirs = new Set(files.map((file) => file.slice(0, file.lastIndexOf("/"))));
13233
+ return [...runDirs].some((dir) => files.includes(`${dir}/task-record.json`) && files.includes(`${dir}/terminal-proof.json`));
13234
+ }
13235
+ };
13236
+ }
13237
+ function decideLaneAdmission(input) {
13238
+ const { facts } = input;
13239
+ if (!facts.exists || !facts.real_path || !facts.same_repository || !facts.is_worktree_root)
13240
+ return "batch_lane_foreign_repository";
13241
+ if (facts.real_path === input.coordinator_real_path)
13242
+ return "batch_lane_is_coordinator";
13243
+ if (facts.branch !== input.expected_branch)
13244
+ return "batch_lane_branch_mismatch";
13245
+ if (facts.head !== input.batch_head)
13246
+ return "batch_lane_base_mismatch";
13247
+ if (!facts.clean)
13248
+ return "batch_lane_dirty";
13249
+ if (facts.active_claim_task_id !== null || input.bound_paths.includes(facts.real_path))
13250
+ return "batch_lane_occupied";
13251
+ return null;
13252
+ }
13253
+ function laneGitOf(input) {
13254
+ return input.git?.lane ?? createDefaultLaneGitPort();
13255
+ }
13256
+ function serialGitOf(input) {
13257
+ return input.git ?? defaultSerialGit();
13258
+ }
13259
+ function releaseHandoffs(input, record, lanes) {
13260
+ const handoffs = [];
13261
+ for (const child of record.children) {
13262
+ if (child.state !== "integrated" || !child.lane)
13263
+ continue;
13264
+ const facts = lanes.inspectLane(input.root, child.lane.path);
13265
+ if (!facts.exists || !facts.same_repository || !facts.clean || facts.branch !== child.lane.branch)
13266
+ continue;
13267
+ if (!lanes.auditReachable({ root: input.root, head: expectedBatchHead(record), task_id: child.task_id }))
13268
+ continue;
13269
+ handoffs.push({ role: "lane-steward", action: "release", task_id: child.task_id, lane_branch: child.lane.branch });
13270
+ }
13271
+ return handoffs;
13272
+ }
13273
+ function defaultSerialGit() {
13274
+ return createDefaultBatchGitPort();
13275
+ }
13276
+ var IN_FLIGHT = new Set(["lane_admitted", "enrolled", "settled", "lane_committed"]);
13277
+ function dependentsOf(record, taskId) {
13278
+ return record.children.filter((child) => child.blocked_by.includes(taskId));
13279
+ }
13280
+ function skipDependents(record, taskId, reason) {
13281
+ const skip = new Set([taskId]);
13282
+ const queue = [taskId];
13283
+ while (queue.length > 0) {
13284
+ const current = queue.shift();
13285
+ for (const dependent of dependentsOf(record, current)) {
13286
+ if (skip.has(dependent.task_id))
13287
+ continue;
13288
+ skip.add(dependent.task_id);
13289
+ queue.push(dependent.task_id);
13290
+ }
13291
+ }
13292
+ record.children = record.children.map((child) => skip.has(child.task_id) && child.state === "pending" ? { ...child, state: "skipped_blocked", reason } : child);
13293
+ }
13294
+ function consumedSlots(record) {
13295
+ return record.children.filter((child) => child.state !== "pending" && child.state !== "skipped_blocked").length;
13296
+ }
13297
+ function scopeOfChild(root, child) {
13298
+ try {
13299
+ return [...readTaskIntent(root, child.task_id, child.intent_path ?? undefined).intent.scope_hint];
13300
+ } catch {
13301
+ return ["**"];
13302
+ }
13303
+ }
13304
+ function scheduleView(input, record) {
13305
+ return record.children.map((child) => ({
13306
+ task_id: child.task_id,
13307
+ state: child.state,
13308
+ blocked_by: child.blocked_by,
13309
+ scope_hint: scopeOfChild(input.root, input.children.find((c) => c.task_id === child.task_id) ?? { task_id: child.task_id, intent_path: null })
13310
+ }));
13311
+ }
13312
+ var LANE_NEEDS_HUMAN_NEXT_ACTION = "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.";
13313
+ var TERMINAL_NEXT_ACTIONS = {
13314
+ completed: "The batch integrated every enrollable child; review the commits and the tracker.",
13315
+ budget_stopped: "The child budget stopped new enrollments; re-confirm to continue under a new authorization.",
13316
+ failed: "A lineage failure stopped the batch; inspect the failing child and the branch state.",
13317
+ rejected: "The batch was rejected before any enrollment; correct the stated reason and re-confirm.",
13318
+ needs_human: LANE_NEEDS_HUMAN_NEXT_ACTION,
13319
+ running: "The batch is still running; no terminal report is due yet.",
13320
+ prepared: "The batch is prepared but not started."
13321
+ };
13322
+ function laneReport(record, reason, nextAction, extra = {}) {
13323
+ return {
13324
+ contract: "assurance_kernel/batch_run_report/v1",
13325
+ batch_id: record.batch_id,
13326
+ initiative_slug: record.initiative_slug,
13327
+ batch_state: record.batch_state,
13328
+ max_parallel: record.max_parallel,
13329
+ children: record.children,
13330
+ commits: record.commits,
13331
+ reason,
13332
+ handoffs: extra.handoffs ?? [],
13333
+ ...extra.refusals?.length ? { lane_refusals: extra.refusals } : {},
13334
+ next_action: nextAction || (TERMINAL_NEXT_ACTIONS[record.batch_state] ?? "Inspect the batch run state."),
13335
+ created_at: record.updated_at
13336
+ };
13337
+ }
13338
+ function laneRejectionReport(record, reason, nextAction) {
13339
+ return laneReport({ ...record, batch_state: "rejected" }, reason, nextAction);
13340
+ }
13341
+ function finalizeLane(root, record, reason, nextAction, extra = {}) {
13342
+ const report = laneReport(record, reason, nextAction, extra);
13343
+ if (isTerminalBatchState(record.batch_state) || record.batch_state === "needs_human")
13344
+ writeBatchRunReport(root, report);
13345
+ return report;
13346
+ }
13347
+ function refuse2(input, persisted, code, detail) {
13348
+ const base = persisted && isLaneBatchRecord(persisted) ? persisted : prepareBatchLaneRunState({
13349
+ batch_id: input.batch_id,
13350
+ initiative_slug: input.initiative_slug,
13351
+ children: persisted ? [] : input.children,
13352
+ plan_digest: input.plan_digest,
13353
+ base_head: input.base_head,
13354
+ confirmation_time: input.confirmation_time,
13355
+ budget: input.budget,
13356
+ max_parallel: input.max_parallel ?? 1,
13357
+ now: input.now
13358
+ });
13359
+ return laneReport({ ...base, batch_state: "rejected" }, `${code}: ${detail}`, "Correct the lane-mode parameters and call start_unattended_batch again.");
13360
+ }
13361
+ function assertPlanMatches(input, record) {
13362
+ const plan = input.registry.children(input.capability);
13363
+ if (record.plan_digest !== computeBatchPlanDigest(plan) || record.children.length !== plan.length || record.children.some((child, index) => {
13364
+ const expected = plan[index];
13365
+ return child.task_id !== expected.task_id || JSON.stringify(child.blocked_by) !== JSON.stringify(expected.blocked_by);
13366
+ }))
13367
+ throw new Error("plan_digest mismatch: persisted children do not match the authorized plan");
13368
+ }
13369
+ function validateNewAuthorization(input) {
13370
+ try {
13371
+ const authorized = input.kernel.validateBatchAuthorization({
13372
+ registry: input.registry,
13373
+ capability: input.capability,
13374
+ binding: {
13375
+ batch_id: input.batch_id,
13376
+ plan_digest: input.plan_digest,
13377
+ base_head: input.base_head,
13378
+ initiative_slug: input.initiative_slug,
13379
+ budget: input.budget
13380
+ }
13381
+ });
13382
+ if (authorized.issued_at !== input.confirmation_time)
13383
+ return "the parked batch requires a fresh literal-user confirmation";
13384
+ const children = input.children.map((child) => {
13385
+ const { intent_path, intent_revision, intent_content_hash } = child;
13386
+ if (intent_path === null || intent_revision === null || intent_content_hash === null)
13387
+ throw new Error(`batch child ${child.task_id} has no complete intent identity`);
13388
+ return { ...child, intent_path, intent_revision, intent_content_hash };
13389
+ });
13390
+ if (computeBatchPlanDigest(children) !== authorized.plan_digest || input.plan_digest !== authorized.plan_digest || input.base_head !== authorized.base_head)
13391
+ return "batch run input does not match the authorized plan or base_head";
13392
+ return null;
13393
+ } catch (error) {
13394
+ return error instanceof Error ? error.message : String(error);
13395
+ }
13396
+ }
13397
+ function rehydrateConsumption(input, record) {
13398
+ const { issued_at: _issuedAt, ...binding } = input.kernel.validateBatchAuthorization({
13399
+ registry: input.registry,
13400
+ capability: input.capability,
13401
+ binding: {
13402
+ batch_id: record.batch_id,
13403
+ plan_digest: record.plan_digest,
13404
+ base_head: record.base_head,
13405
+ initiative_slug: record.initiative_slug,
13406
+ budget: record.budget
13407
+ }
13408
+ });
13409
+ for (const child of record.children) {
13410
+ if (child.state === "pending" || child.state === "skipped_blocked" || child.state === "lane_admitted")
13411
+ continue;
13412
+ if (!input.registry.isChildConsumed(input.capability, child.task_id))
13413
+ input.registry.consumeChild(input.capability, binding, child.task_id);
13414
+ }
13415
+ }
13416
+ function failLineage(root, record, message) {
13417
+ const next = {
13418
+ ...record,
13419
+ batch_state: "failed",
13420
+ children: record.children.map((child) => IN_FLIGHT.has(child.state) ? { ...child, state: "needs_human", reason: message } : child)
13421
+ };
13422
+ for (const child of next.children)
13423
+ if (child.state === "needs_human")
13424
+ skipDependents(next, child.task_id, `dependency ${child.task_id} parked`);
13425
+ return finalizeLane(root, writeBatchLaneRunState(root, next), message, "");
13426
+ }
13427
+ function park(record, taskId, reason) {
13428
+ record.children = record.children.map((child) => child.task_id === taskId ? { ...child, state: "needs_human", reason } : child);
13429
+ skipDependents(record, taskId, `dependency ${taskId} parked`);
13430
+ }
13431
+ function isLineageBreak(message) {
13432
+ return message.includes("batch_head_lineage_broken");
13433
+ }
13434
+ async function runLaneBatch(input, persisted) {
13435
+ if (persisted && !isLaneBatchRecord(persisted))
13436
+ return refuse2(input, persisted, PARALLEL_MISMATCH, "the recorded batch runs serially; max_parallel cannot be added on a resume");
13437
+ if (persisted && input.max_parallel !== undefined && input.max_parallel !== persisted.max_parallel)
13438
+ return refuse2(input, persisted, PARALLEL_MISMATCH, `the recorded batch runs with max_parallel ${persisted.max_parallel}`);
13439
+ const limit = persisted?.max_parallel ?? input.max_parallel ?? 1;
13440
+ if (!input.children.length)
13441
+ return refuse2(input, persisted, "batch_plan_empty", "batch plan is empty");
13442
+ const git = serialGitOf(input);
13443
+ const lanes = laneGitOf(input);
13444
+ let record;
13445
+ if (persisted) {
13446
+ record = persisted;
13447
+ assertPlanMatches(input, record);
13448
+ if (isTerminalBatchState(record.batch_state))
13449
+ return finalizeLane(input.root, record, `terminal state already reached: ${record.batch_state}`, "", {
13450
+ handoffs: record.batch_state === "completed" ? releaseHandoffs(input, record, lanes) : []
13451
+ });
13452
+ if (record.batch_state === "needs_human")
13453
+ return finalizeLane(input.root, record, "a parked child needs a human decision", LANE_NEEDS_HUMAN_NEXT_ACTION);
13454
+ } else {
13455
+ const invalid = validateNewAuthorization(input);
13456
+ if (invalid)
13457
+ return refuse2(input, null, "batch_authorization_invalid", invalid);
13458
+ const preflight = await git.preflight({
13459
+ root: input.root,
13460
+ initiative_slug: input.initiative_slug,
13461
+ base_head: input.base_head
13462
+ });
13463
+ if (!preflight.ok)
13464
+ return refuse2(input, null, preflight.reason, preflight.message || "Correct the preflight condition and re-confirm.");
13465
+ record = prepareBatchLaneRunState({
13466
+ batch_id: input.batch_id,
13467
+ initiative_slug: input.initiative_slug,
13468
+ children: input.children,
13469
+ plan_digest: input.plan_digest,
13470
+ base_head: input.base_head,
13471
+ confirmation_time: input.confirmation_time,
13472
+ budget: input.budget,
13473
+ max_parallel: limit,
13474
+ now: input.now
13475
+ });
13476
+ }
13477
+ const persist = () => {
13478
+ record = writeBatchLaneRunState(input.root, record);
13479
+ };
13480
+ if (record.batch_state === "prepared") {
13481
+ record.batch_state = "running";
13482
+ persist();
13483
+ }
13484
+ for (const child of record.children) {
13485
+ if (child.state !== "lane_committed" || !child.lane?.lane_commit)
13486
+ continue;
13487
+ const found = lanes.findIntegrated({
13488
+ root: input.root,
13489
+ task_id: child.task_id,
13490
+ batch_id: record.batch_id,
13491
+ from_head: expectedBatchHead(record),
13492
+ lane_base: child.lane.base_head,
13493
+ lane_commit: child.lane.lane_commit
13494
+ });
13495
+ if (!found)
13496
+ continue;
13497
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "integrated", commit: found } : c);
13498
+ record.commits = [...record.commits, found];
13499
+ persist();
13500
+ }
13501
+ for (const child of record.children) {
13502
+ if (child.state !== "integrated" || !child.lane)
13503
+ continue;
13504
+ if (lanes.inspectLane(input.root, child.lane.path).exists)
13505
+ continue;
13506
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "released" } : c);
13507
+ persist();
13508
+ }
13509
+ const lineage = existsSync10(join15(input.root, ".git")) ? classifyBatchLineage({
13510
+ root: input.root,
13511
+ branch: record.branch ?? "",
13512
+ expectedHead: expectedBatchHead(record),
13513
+ childCommits: record.commits,
13514
+ batchId: record.batch_id
13515
+ }) : { kind: "equal", head: expectedBatchHead(record) };
13516
+ if (lineage.kind === "broken")
13517
+ return failLineage(input.root, record, lineage.message);
13518
+ if (lineage.kind === "fast_forward") {
13519
+ record = writeBatchLaneRunState(input.root, {
13520
+ ...record,
13521
+ adopted_heads: [...record.adopted_heads ?? [], { from: expectedBatchHead(record), to: lineage.head }]
13522
+ });
13523
+ }
13524
+ try {
13525
+ rehydrateConsumption(input, record);
13526
+ } catch (error) {
13527
+ return failLineage(input.root, record, error instanceof Error ? error.message : String(error));
13528
+ }
13529
+ const refusals = [];
13530
+ let parkedMessage = null;
13531
+ const reviewOpen = [];
13532
+ const parkChild = (taskId, reason) => {
13533
+ park(record, taskId, reason);
13534
+ parkedMessage ??= reason;
13535
+ persist();
13536
+ };
13537
+ const enrollInLane = async (taskId) => {
13538
+ const lane = record.children.find((c) => c.task_id === taskId).lane;
13539
+ try {
13540
+ await input.kernel.enrollTask({
13541
+ root: lane.path,
13542
+ task_id: taskId,
13543
+ batch: { registry: input.registry, capability: input.capability, binding: { batch_id: record.batch_id, expected_head: lane.base_head } }
13544
+ });
13545
+ record.children = record.children.map((c) => c.task_id === taskId ? { ...c, state: "enrolled", reason: null } : c);
13546
+ persist();
13547
+ return null;
13548
+ } catch (error) {
13549
+ const message = error instanceof Error ? error.message : String(error);
13550
+ parkChild(taskId, message);
13551
+ if (!isLineageBreak(message))
13552
+ return null;
13553
+ record.batch_state = "failed";
13554
+ persist();
13555
+ return finalizeLane(input.root, record, message, "", { refusals });
13556
+ }
13557
+ };
13558
+ for (const child of [...record.children]) {
13559
+ if (child.state !== "lane_admitted" && child.state !== "enrolled" || !child.lane)
13560
+ continue;
13561
+ let fresh;
13562
+ try {
13563
+ fresh = await input.kernel.projectTask(child.lane.path, child.task_id);
13564
+ } catch {
13565
+ fresh = { error: "unreadable" };
13566
+ }
13567
+ if (fresh.error !== null) {
13568
+ parkChild(child.task_id, "batch_lane_lost");
13569
+ continue;
13570
+ }
13571
+ const holdsClaim = fresh.claim !== null && fresh.claim.task_id === child.task_id;
13572
+ if (child.state === "lane_admitted") {
13573
+ if (holdsClaim) {
13574
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "enrolled", reason: "adopted existing lane claim after interruption" } : c);
13575
+ persist();
13576
+ } else {
13577
+ const failed = await enrollInLane(child.task_id);
13578
+ if (failed)
13579
+ return failed;
13580
+ }
13581
+ continue;
13582
+ }
13583
+ if (fresh.projection.lifecycle === "done" && fresh.projection.completion_ready) {
13584
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "settled", reason: null } : c);
13585
+ persist();
13586
+ continue;
13587
+ }
13588
+ if (!holdsClaim) {
13589
+ parkChild(child.task_id, "batch_lane_lost");
13590
+ continue;
13591
+ }
13592
+ if (!input.kernel.ownsTaskClaim(child.task_id)) {
13593
+ parkChild(child.task_id, "claim held by another batch");
13594
+ continue;
13595
+ }
13596
+ if (fresh.projection.next_obligation === "run_qa" || fresh.projection.artifact_state !== "frozen")
13597
+ continue;
13598
+ const terminal = await input.kernel.advanceTask(child.lane.path, child.task_id);
13599
+ if (terminal.state === "completed") {
13600
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "settled", reason: null, qa_failures: 0 } : c);
13601
+ persist();
13602
+ } else if (terminal.state === "review_ready") {
13603
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, reason: `review reservation ${terminal.operation_id} open` } : c);
13604
+ persist();
13605
+ reviewOpen.push(child.task_id);
13606
+ } else if (terminal.state === "rework" && terminal.operation === "qa") {
13607
+ const failures = child.qa_failures + 1;
13608
+ if (failures >= record.budget.qa_failure_limit) {
13609
+ const reason = "QA failure limit reached";
13610
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason, qa_failures: failures } : c);
13611
+ skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
13612
+ parkedMessage ??= reason;
13613
+ persist();
13614
+ continue;
13615
+ }
13616
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, qa_failures: failures } : c);
13617
+ persist();
13618
+ } else if (terminal.state === "rework" || terminal.environment_failure || terminal.state === "review_preparation_failed" || terminal.recovery?.category === "repair") {
13619
+ continue;
13620
+ } else {
13621
+ const reason = terminal.state === "stopped" ? "Kernel reported the child stopped" : terminal.reason;
13622
+ parkChild(child.task_id, reason);
13623
+ }
13624
+ }
13625
+ for (const child of [...record.children]) {
13626
+ if (child.state !== "settled" || !child.lane)
13627
+ continue;
13628
+ const planChild = input.children.find((c) => c.task_id === child.task_id);
13629
+ let laneCommit;
13630
+ try {
13631
+ const existing = await git.lookupBatchCommit(child.lane.path, child.task_id, record.batch_id, child.lane.base_head, child.lane.branch);
13632
+ laneCommit = existing?.commit ?? (await git.commitChild(child.lane.path, child.task_id, record.batch_id, child.lane.base_head, child.lane.branch, planChild?.intent_path ?? undefined)).commit;
13633
+ } catch (error) {
13634
+ const message = error instanceof Error ? error.message : String(error);
13635
+ parkChild(child.task_id, message);
13636
+ if (!isLineageBreak(message))
13637
+ continue;
13638
+ record.batch_state = "failed";
13639
+ persist();
13640
+ return finalizeLane(input.root, record, message, "");
13641
+ }
13642
+ record.children = record.children.map((c) => c.task_id === child.task_id && c.lane ? { ...c, state: "lane_committed", lane: { ...c.lane, lane_commit: laneCommit } } : c);
13643
+ persist();
13644
+ }
13645
+ for (const child of [...record.children]) {
13646
+ if (child.state !== "lane_committed" || !child.lane?.lane_commit)
13647
+ continue;
13648
+ const intentPathOf = (taskId) => input.children.find((c) => c.task_id === taskId)?.intent_path ?? null;
13649
+ try {
13650
+ const { commit } = await lanes.integrate({
13651
+ root: input.root,
13652
+ branch: record.branch ?? "",
13653
+ batch_head: expectedBatchHead(record),
13654
+ lane_base: child.lane.base_head,
13655
+ lane_commit: child.lane.lane_commit,
13656
+ child: { task_id: child.task_id, intent_path: intentPathOf(child.task_id) },
13657
+ siblings: record.children.filter((c) => c.state === "integrated" && c.task_id !== child.task_id).map((c) => ({ task_id: c.task_id, intent_path: intentPathOf(c.task_id), commit: c.commit }))
13658
+ });
13659
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "integrated", commit, reason: null } : c);
13660
+ record.commits = [...record.commits, commit];
13661
+ persist();
13662
+ } catch (error) {
13663
+ const message = error instanceof Error ? error.message : String(error);
13664
+ const reason = error instanceof BatchIntegrationError ? error.reason : "batch_integration_conflict";
13665
+ parkChild(child.task_id, isLineageBreak(message) || message.startsWith(`${reason}:`) ? message : `${reason}: ${message}`);
13666
+ if (isLineageBreak(message)) {
13667
+ record.batch_state = "failed";
13668
+ persist();
13669
+ return finalizeLane(input.root, record, message, "");
13670
+ }
13671
+ }
13672
+ }
13673
+ const coordinatorReal = lanes.resolveRoot(input.root);
13674
+ for (const offer of input.lane_offers ?? []) {
13675
+ const startable = new Set(startableChildren(scheduleView(input, record), limit));
13676
+ const child = record.children.find((c) => c.task_id === offer.task_id);
13677
+ if (!child || child.state !== "pending" || !startable.has(child.task_id)) {
13678
+ refusals.push({ ...offer, reason: "batch_lane_unknown_child" });
13679
+ continue;
13680
+ }
13681
+ const head = expectedBatchHead(record);
13682
+ const facts = lanes.inspectLane(input.root, offer.path);
13683
+ const reason = decideLaneAdmission({
13684
+ facts,
13685
+ coordinator_real_path: coordinatorReal,
13686
+ expected_branch: laneBranchName(record.initiative_slug, child.task_id),
13687
+ batch_head: head,
13688
+ bound_paths: record.children.flatMap((c) => c.lane && c.state !== "released" ? [c.lane.path] : [])
13689
+ });
13690
+ if (reason) {
13691
+ refusals.push({ ...offer, reason });
13692
+ continue;
13693
+ }
13694
+ if (consumedSlots(record) >= record.budget.max_children)
13695
+ break;
13696
+ const lane = { path: facts.real_path, branch: laneBranchName(record.initiative_slug, child.task_id), base_head: head, lane_commit: null };
13697
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "lane_admitted", lane } : c);
13698
+ persist();
13699
+ const failed = await enrollInLane(child.task_id);
13700
+ if (failed)
13701
+ return failed;
13702
+ }
13703
+ if (record.children.every((c) => c.state === "integrated" || c.state === "released")) {
13704
+ record.batch_state = "completed";
13705
+ persist();
13706
+ return finalizeLane(input.root, record, "all enrollable children integrated", "", {
13707
+ handoffs: releaseHandoffs(input, record, lanes),
13708
+ refusals
13709
+ });
13710
+ }
13711
+ const handoffs = [];
13712
+ for (const child of record.children) {
13713
+ if (child.state !== "enrolled" || !child.lane)
13714
+ continue;
13715
+ const fresh = await input.kernel.projectTask(child.lane.path, child.task_id);
13716
+ if (fresh.error !== null)
13717
+ continue;
13718
+ handoffs.push({
13719
+ role: "executor",
13720
+ task_id: child.task_id,
13721
+ run_id: fresh.projection.run_id ?? null,
13722
+ record_revision: fresh.projection.record_revision,
13723
+ next_obligation: fresh.projection.next_obligation,
13724
+ lane_branch: child.lane.branch,
13725
+ lane_path: child.lane.path
13726
+ });
13727
+ }
13728
+ handoffs.push(...releaseHandoffs(input, record, lanes));
13729
+ const inFlight = record.children.filter((c) => IN_FLIGHT.has(c.state));
13730
+ const startable = startableChildren(scheduleView(input, record), limit);
13731
+ const overBudget = consumedSlots(record) >= record.budget.max_children;
13732
+ if (!inFlight.length && (!startable.length || overBudget)) {
13733
+ const parked = record.children.some((c) => c.state === "needs_human" || c.state === "skipped_blocked");
13734
+ record.batch_state = parked ? "needs_human" : overBudget ? "budget_stopped" : "needs_human";
13735
+ persist();
13736
+ return finalizeLane(input.root, record, parkedMessage ?? (parked ? "a parked child needs a human decision" : overBudget ? `max_children budget exhausted (${record.budget.max_children})` : "no child can start"), "", { refusals });
13737
+ }
13738
+ if (!overBudget) {
13739
+ for (const taskId of startable) {
13740
+ handoffs.push({
13741
+ role: "lane-steward",
13742
+ action: "provision",
13743
+ task_id: taskId,
13744
+ lane_branch: laneBranchName(record.initiative_slug, taskId),
13745
+ base_head: expectedBatchHead(record),
13746
+ executor_hosts: LANE_EXECUTOR_HOSTS
13747
+ });
13748
+ }
13749
+ }
13750
+ if (reviewOpen.length > 0) {
13751
+ return laneReport(record, reviewOpen.length === 1 ? `child ${reviewOpen[0]} holds an open Review reservation` : `children ${reviewOpen.join(", ")} hold open Review reservations`, "Submit each reserved foreground Review verdict in its own Lane, then call start_unattended_batch again to continue.", { handoffs, refusals });
13752
+ }
13753
+ return laneReport(record, null, handoffs.some((h) => h.role === "lane-steward" && h.action === "provision") ? "Provide a Lane for each provision handoff, then call start_unattended_batch again with lane_offers." : "Run each executor handoff in its Lane. When a Lane finishes, call start_unattended_batch again with the same Initiative.", { handoffs, refusals });
13754
+ }
13755
+
12521
13756
  // plugins/immune-brain/runtime/unattended/batch_runner.ts
12522
13757
  function batchQaFailureFacts(record) {
12523
13758
  const failed = record.history.filter((event) => event.type === "request_rework" && event.authority?.authority_kind === "qa");
@@ -12530,15 +13765,15 @@ function batchQaFailureFacts(record) {
12530
13765
  function batchGitPortOf(input) {
12531
13766
  return input.git ?? createDefaultBatchGitPort();
12532
13767
  }
12533
- function dependentsOf(record, taskId) {
13768
+ function dependentsOf2(record, taskId) {
12534
13769
  return record.children.filter((child) => child.blocked_by.includes(taskId));
12535
13770
  }
12536
- function skipDependents(record, taskId, reason) {
13771
+ function skipDependents2(record, taskId, reason) {
12537
13772
  const skip = new Set([taskId]);
12538
13773
  const queue = [taskId];
12539
13774
  while (queue.length > 0) {
12540
13775
  const current = queue.shift();
12541
- for (const dependent of dependentsOf(record, current)) {
13776
+ for (const dependent of dependentsOf2(record, current)) {
12542
13777
  if (skip.has(dependent.task_id))
12543
13778
  continue;
12544
13779
  skip.add(dependent.task_id);
@@ -12561,7 +13796,7 @@ function requireFreshProjection(result, taskId) {
12561
13796
  function nextEnrollableChild(record) {
12562
13797
  return record.children.find((child) => child.state === "pending" && record.children.every((other) => !child.blocked_by.includes(other.task_id) || other.state === "committed")) ?? null;
12563
13798
  }
12564
- var TERMINAL_NEXT_ACTIONS = {
13799
+ var TERMINAL_NEXT_ACTIONS2 = {
12565
13800
  completed: "The batch settled every enrollable child; review the commits and the tracker.",
12566
13801
  budget_stopped: "The child budget stopped new enrollments; re-confirm to continue under a new authorization.",
12567
13802
  failed: "A commit or lineage failure stopped the batch; inspect the failing child and the branch state.",
@@ -12579,7 +13814,7 @@ function reportFor(record, reason, nextAction) {
12579
13814
  children: record.children,
12580
13815
  commits: record.commits,
12581
13816
  reason,
12582
- next_action: nextAction || (TERMINAL_NEXT_ACTIONS[record.batch_state] ?? "Inspect the batch run state."),
13817
+ next_action: nextAction || (TERMINAL_NEXT_ACTIONS2[record.batch_state] ?? "Inspect the batch run state."),
12583
13818
  created_at: record.updated_at
12584
13819
  };
12585
13820
  }
@@ -12613,13 +13848,13 @@ function failPersistedLineage(root, existing, message) {
12613
13848
  const record = { ...existing, batch_state: "failed", children };
12614
13849
  for (const child of record.children) {
12615
13850
  if (child.state === "needs_human") {
12616
- skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
13851
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
12617
13852
  }
12618
13853
  }
12619
13854
  return writeBatchRunState(root, record);
12620
13855
  }
12621
13856
  function reconcileLineage(root, record) {
12622
- if (!existsSync10(join15(root, ".git")))
13857
+ if (!existsSync11(join16(root, ".git")))
12623
13858
  return { record, failure: null };
12624
13859
  const expected = expectedBatchHead(record);
12625
13860
  const lineage = classifyBatchLineage({
@@ -12642,8 +13877,8 @@ function reconcileLineage(root, record) {
12642
13877
  };
12643
13878
  }
12644
13879
  async function validatePersistedRun(input, record) {
12645
- if (!record.commits.length && record.children[0]?.state === "settled" && existsSync10(join15(input.root, ".git"))) {
12646
- const head = spawnSync9("git", ["-C", input.root, "rev-parse", "HEAD"], { encoding: "utf8" });
13880
+ if (!record.commits.length && record.children[0]?.state === "settled" && existsSync11(join16(input.root, ".git"))) {
13881
+ const head = spawnSync11("git", ["-C", input.root, "rev-parse", "HEAD"], { encoding: "utf8" });
12647
13882
  const live = head.stdout.trim();
12648
13883
  const expected = expectedBatchHead(record);
12649
13884
  const adoptable = () => classifyBatchLineage({
@@ -12672,8 +13907,8 @@ async function validatePersistedRun(input, record) {
12672
13907
  const git = batchGitPortOf(input);
12673
13908
  const evidence = await git.lookupBatchCommit(input.root, child.task_id, record.batch_id, undefined, record.branch);
12674
13909
  if (!evidence || evidence.commit !== child.commit) {
12675
- if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync10(join15(input.root, ".git"))) {
12676
- const reach = spawnSync9("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13910
+ if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync11(join16(input.root, ".git"))) {
13911
+ const reach = spawnSync11("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
12677
13912
  if (reach.status !== 0) {
12678
13913
  throw new Error(`batch_head_lineage_broken: recorded commit ${child.commit} for ${child.task_id} is no longer reachable from HEAD`);
12679
13914
  }
@@ -12758,17 +13993,22 @@ async function startBatch(input) {
12758
13993
  function rejectionReport(input, reason) {
12759
13994
  const persisted = (() => {
12760
13995
  try {
12761
- return readBatchRunState(input.root, input.batch_id);
13996
+ return readAnyBatchRunState(input.root, input.batch_id);
12762
13997
  } catch {
12763
13998
  return null;
12764
13999
  }
12765
14000
  })();
14001
+ if (persisted !== null && isLaneBatchRecord(persisted))
14002
+ return laneRejectionReport(persisted, reason, "settle the reported kernel store condition and retry in the current Host");
12766
14003
  return reportFor({
12767
14004
  ...persisted ?? prepareBatchRunState({ ...input, children: [], now: input.now }),
12768
14005
  batch_state: "rejected"
12769
14006
  }, reason, "settle the reported kernel store condition and retry in the current Host");
12770
14007
  }
12771
14008
  async function startBatchLocked(input) {
14009
+ const recorded = readAnyBatchRunState(input.root, input.batch_id);
14010
+ if (input.max_parallel !== undefined || recorded !== null && isLaneBatchRecord(recorded))
14011
+ return await runLaneBatch(input, recorded);
12772
14012
  if (!input.children.length) {
12773
14013
  const rejected = prepareBatchRunState({ ...input, children: [], now: input.now });
12774
14014
  return reportFor({ ...rejected, batch_state: "rejected" }, "batch plan is empty", "Provide a non-empty enrollable child plan.");
@@ -12818,7 +14058,7 @@ async function startBatchLocked(input) {
12818
14058
  const reparked = { ...existing, children: remapped };
12819
14059
  for (const parked of remapped) {
12820
14060
  if (parked.state === "needs_human")
12821
- skipDependents(reparked, parked.task_id, `dependency ${parked.task_id} parked`);
14061
+ skipDependents2(reparked, parked.task_id, `dependency ${parked.task_id} parked`);
12822
14062
  }
12823
14063
  existing = writeBatchRunState(input.root, {
12824
14064
  ...reparked,
@@ -12901,7 +14141,7 @@ async function startBatchLocked(input) {
12901
14141
  adoptedClaim = true;
12902
14142
  } else if (claimState === "foreign") {
12903
14143
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: "claim held by another batch" } : c);
12904
- skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
14144
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
12905
14145
  record.batch_state = "needs_human";
12906
14146
  persist();
12907
14147
  return finalize(input.root, record, "claim held by another batch", "needs-human-attention");
@@ -12947,13 +14187,13 @@ async function startBatchLocked(input) {
12947
14187
  const message = error instanceof Error ? error.message : String(error);
12948
14188
  if (isLineageBreakError(message)) {
12949
14189
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
12950
- skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
14190
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
12951
14191
  record.batch_state = "failed";
12952
14192
  persist();
12953
14193
  break;
12954
14194
  }
12955
14195
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
12956
- skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
14196
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
12957
14197
  record.batch_state = "needs_human";
12958
14198
  persist();
12959
14199
  break;
@@ -13011,7 +14251,7 @@ async function resumeBatch(input, projection) {
13011
14251
  const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, driven.task_id), driven.task_id);
13012
14252
  const holdsClaim = fresh.claim !== null && fresh.claim.task_id === driven.task_id && input.kernel.ownsTaskClaim(driven.task_id);
13013
14253
  if (holdsClaim === false && fresh.claim !== null && fresh.claim.task_id === driven.task_id && !input.kernel.ownsTaskClaim(driven.task_id)) {
13014
- skipDependents(existing, driven.task_id, `dependency ${driven.task_id} parked`);
14254
+ skipDependents2(existing, driven.task_id, `dependency ${driven.task_id} parked`);
13015
14255
  existing = writeBatchRunState(input.root, {
13016
14256
  ...existing,
13017
14257
  batch_state: "needs_human",
@@ -13023,7 +14263,7 @@ async function resumeBatch(input, projection) {
13023
14263
  const failures = Math.max(existing.consecutive_qa_failures, fresh.qa_failure_count ?? 0);
13024
14264
  if (failures >= existing.budget.qa_failure_limit && fresh.last_qa_failure_at && Date.parse(fresh.last_qa_failure_at) > Date.parse(existing.confirmation_time)) {
13025
14265
  const reason = "QA failure limit reached; repair and settle the own child through Kernel before resuming the batch";
13026
- skipDependents(existing, driven.task_id, `dependency ${driven.task_id} parked`);
14266
+ skipDependents2(existing, driven.task_id, `dependency ${driven.task_id} parked`);
13027
14267
  existing = writeBatchRunState(input.root, {
13028
14268
  ...existing,
13029
14269
  batch_state: "needs_human",
@@ -13094,7 +14334,7 @@ async function driveInterruptedChild(input, child) {
13094
14334
  } catch (error) {
13095
14335
  const message = error instanceof Error ? error.message : String(error);
13096
14336
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: `commit lookup failed: ${message}` } : c);
13097
- skipDependents(record, child.task_id, `dependency ${child.task_id} failed to commit`);
14337
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} failed to commit`);
13098
14338
  const isLineageError = message.includes("batch_head_lineage_broken") || message.includes("lineage");
13099
14339
  record.batch_state = isLineageError ? "failed" : "needs_human";
13100
14340
  persist();
@@ -13107,12 +14347,12 @@ async function driveInterruptedChild(input, child) {
13107
14347
  const planChild = input.children.find((c) => c.task_id === child.task_id);
13108
14348
  const intentPath = planChild?.intent_path ?? undefined;
13109
14349
  const doCommit = async () => batchGitPortOf(input).commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath);
13110
- if (existing && !record.commits.length && existsSync10(join15(input.root, ".git")) && !ownUnpersistedBatchHead(input.root, record, existing.commit))
14350
+ if (existing && !record.commits.length && existsSync11(join16(input.root, ".git")) && !ownUnpersistedBatchHead(input.root, record, existing.commit))
13111
14351
  throw new Error("first unpersisted batch commit provenance is invalid");
13112
14352
  const adopted = existing ?? await doCommit().catch((error) => {
13113
14353
  const message = error instanceof Error ? error.message : String(error);
13114
14354
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
13115
- skipDependents(record, child.task_id, `dependency ${child.task_id} failed to commit`);
14355
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} failed to commit`);
13116
14356
  record.batch_state = "failed";
13117
14357
  persist();
13118
14358
  throw new BatchCommitAbortError(message);
@@ -13142,7 +14382,7 @@ async function driveInterruptedChild(input, child) {
13142
14382
  const reason = "QA failure limit reached";
13143
14383
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
13144
14384
  record.batch_state = "needs_human";
13145
- skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
14385
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
13146
14386
  persist();
13147
14387
  const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
13148
14388
  return finalize(input.root, record, reason, "Repair and settle the own child through Kernel, then re-confirm the batch.", { recovery: deriveAssuranceRecovery(child.task_id, fresh, terminal, fresh.recovery_findings) ?? undefined, diagnostics: terminal.diagnostics });
@@ -13157,7 +14397,7 @@ async function driveInterruptedChild(input, child) {
13157
14397
  const reason = terminal.state === "stopped" ? "Kernel reported the child stopped" : terminal.reason;
13158
14398
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
13159
14399
  record.batch_state = "needs_human";
13160
- skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
14400
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
13161
14401
  persist();
13162
14402
  return finalize(input.root, record, reason, "");
13163
14403
  }
@@ -13496,7 +14736,7 @@ class ClaudeRuntime {
13496
14736
  if (!slug)
13497
14737
  return result;
13498
14738
  try {
13499
- if (existsSync11(join16(this.cwd, ".imm", "audit", taskId)))
14739
+ if (existsSync12(join17(this.cwd, ".imm", "audit", taskId)))
13500
14740
  execFileSync8("git", ["-C", this.cwd, "add", "--", `.imm/audit/${taskId}`], { stdio: "ignore" });
13501
14741
  const batch = await this.startUnattendedBatch(slug, meta, { reuseOnly: true });
13502
14742
  return { ...result, batch };
@@ -13593,7 +14833,7 @@ class ClaudeRuntime {
13593
14833
  throw new Error("approve_breaking_intent_revision requires next_intent");
13594
14834
  const nextIntentHash = nextIntent ? canonicalIntentHash(nextIntent) : undefined;
13595
14835
  const nextIntentRef = nextIntent ? { path: `docs/plans/${nextIntent.task_id}.intent.json`, content_hash: nextIntentHash } : undefined;
13596
- const sidecar = nextIntent ? join16(this.cwd, priorIntent.intent_ref.path) : undefined;
14836
+ const sidecar = nextIntent ? join17(this.cwd, priorIntent.intent_ref.path) : undefined;
13597
14837
  const stagedSnapshot = sidecar ? captureStagedIntent(this.cwd, priorIntent.intent_ref.path) : undefined;
13598
14838
  const restoreStagedIntent2 = () => {
13599
14839
  if (!stagedSnapshot)
@@ -13702,8 +14942,8 @@ class ClaudeRuntime {
13702
14942
  const { app } = await this.authority();
13703
14943
  const operation = input.operation.op === "revise_intent" ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) } : input.operation;
13704
14944
  const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
13705
- const sidecar = join16(ctx.cwd, priorIntent.intent_ref.path);
13706
- const priorBytes = operation.op === "revise_intent" ? readFileSync14(sidecar) : null;
14945
+ const sidecar = join17(ctx.cwd, priorIntent.intent_ref.path);
14946
+ const priorBytes = operation.op === "revise_intent" ? readFileSync15(sidecar) : null;
13707
14947
  const priorStaged = priorBytes !== null ? captureStagedIntent(ctx.cwd, priorIntent.intent_ref.path) : null;
13708
14948
  try {
13709
14949
  if (priorBytes) {
@@ -13750,7 +14990,7 @@ class ClaudeRuntime {
13750
14990
  root: this.cwd,
13751
14991
  initiative_slug: initiativeSlug,
13752
14992
  now,
13753
- readInitiative: this.readInitiative ?? observeGithubInitiative
14993
+ readInitiative: this.readInitiative ?? observeInitiative
13754
14994
  });
13755
14995
  if (!preflight.ok) {
13756
14996
  return {
@@ -13770,7 +15010,7 @@ class ClaudeRuntime {
13770
15010
  initiative_slug: initiativeSlug,
13771
15011
  now,
13772
15012
  projection: preflight.projection,
13773
- readInitiative: this.readInitiative ?? observeGithubInitiative,
15013
+ readInitiative: this.readInitiative ?? observeInitiative,
13774
15014
  nonce: enrollmentNonce(),
13775
15015
  gate: async (facts) => {
13776
15016
  if (reuseOnly)
@@ -13803,6 +15043,13 @@ class ClaudeRuntime {
13803
15043
  reason: child.reason
13804
15044
  })),
13805
15045
  budget: facts.budget,
15046
+ ...options.max_parallel !== undefined ? {
15047
+ lane_mode: {
15048
+ max_parallel: options.max_parallel,
15049
+ parallel_groups: facts.parallel_groups,
15050
+ serialized: facts.scope_conflicts
15051
+ }
15052
+ } : {},
13806
15053
  ...facts.reuse_blockers.length > 0 ? {
13807
15054
  re_confirmation_required: facts.reuse_blockers,
13808
15055
  recovery: "confirm to issue a fresh authorization bound to the current plan and HEAD"
@@ -13902,7 +15149,9 @@ class ClaudeRuntime {
13902
15149
  budget,
13903
15150
  now,
13904
15151
  kernel: kernelPort,
13905
- git: this.batchGit
15152
+ git: this.batchGit,
15153
+ ...options.max_parallel !== undefined ? { max_parallel: options.max_parallel } : {},
15154
+ ...options.lane_offers !== undefined ? { lane_offers: options.lane_offers } : {}
13906
15155
  });
13907
15156
  if (report.batch_state === "rejected")
13908
15157
  return batchReason("batch_run_rejected", report.reason ?? "");
@@ -13937,7 +15186,19 @@ function listMcpTools() {
13937
15186
  inputSchema: {
13938
15187
  type: "object",
13939
15188
  properties: {
13940
- ...tool.name === "start_unattended_batch" ? { initiative_slug: { type: "string" } } : {
15189
+ ...tool.name === "start_unattended_batch" ? {
15190
+ initiative_slug: { type: "string" },
15191
+ max_parallel: { type: "integer", minimum: 1 },
15192
+ lane_offers: {
15193
+ type: "array",
15194
+ items: {
15195
+ type: "object",
15196
+ properties: { task_id: { type: "string" }, path: { type: "string" } },
15197
+ required: ["task_id", "path"],
15198
+ additionalProperties: false
15199
+ }
15200
+ }
15201
+ } : {
13941
15202
  task_id: { type: "string" },
13942
15203
  ...tool.name === "approve_breaking_intent_revision" || tool.name === "revise_intent" ? { next_intent: { type: "object" } } : {},
13943
15204
  ...tool.name === "stop" ? { reason: { type: "string" } } : {},
@@ -14008,7 +15269,12 @@ function createMcpRuntime(options = {}) {
14008
15269
  interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
14009
15270
  signal: meta.signal
14010
15271
  };
14011
- return runtime.startUnattendedBatch(initiativeSlug, toolMeta);
15272
+ const laneOffers = parseLaneOffers(args.lane_offers);
15273
+ const maxParallel = resolveLaneParallel(options.cwd ?? process.cwd(), initiativeSlug, parseMaxParallel(args.max_parallel), laneOffers);
15274
+ return runtime.startUnattendedBatch(initiativeSlug, toolMeta, {
15275
+ ...maxParallel !== undefined ? { max_parallel: maxParallel } : {},
15276
+ ...laneOffers !== undefined ? { lane_offers: laneOffers } : {}
15277
+ });
14012
15278
  }
14013
15279
  const taskId = String(args.task_id ?? "");
14014
15280
  if (!taskId)
@@ -14235,7 +15501,14 @@ ${b.children.map((c) => ` - ${c.task_id} (${c.slice_id}) [risk: ${c.risk ?? "un
14235
15501
  b?.excluded && b.excluded.length > 0 ? `Excluded children (${b.excluded.length}):
14236
15502
  ${b.excluded.map((e) => ` - ${e.task_id} (${e.slice_id}): ${e.reason}`).join(`
14237
15503
  `)}` : null,
14238
- b?.budget ? `Budget: max_children=${b.budget.max_children}, qa_failure_limit=${b.budget.qa_failure_limit}` : null
15504
+ b?.budget ? `Budget: max_children=${b.budget.max_children}, qa_failure_limit=${b.budget.qa_failure_limit}` : null,
15505
+ b?.lane_mode ? `Lane mode: max_parallel=${b.lane_mode.max_parallel}` : null,
15506
+ b?.lane_mode ? `Parallel groups (${b.lane_mode.parallel_groups.length}):
15507
+ ${b.lane_mode.parallel_groups.map((group) => ` - ${group.join(", ")}`).join(`
15508
+ `)}` : null,
15509
+ b?.lane_mode && b.lane_mode.serialized.length > 0 ? `Serialized by overlapping scope (${b.lane_mode.serialized.length}):
15510
+ ${b.lane_mode.serialized.map((c) => ` - ${c.task_id} after ${c.overlaps_with.join(", ")}`).join(`
15511
+ `)}` : null
14239
15512
  ].filter(Boolean);
14240
15513
  return {
14241
15514
  mode: "form",