immune-brain 5.0.0 → 5.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) 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 +18 -5
  6. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +189 -3
  7. package/plugins/immune-brain/.pi-extension/pi-canary-assurance-progression.ts +66 -3
  8. package/plugins/immune-brain/agents/immune-brain-reviewer.md +11 -1
  9. package/plugins/immune-brain/dist/claude/mcp-server.mjs +1885 -149
  10. package/plugins/immune-brain/dist/docs/reference/immune-brain-config.md +27 -0
  11. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +8 -6
  12. package/plugins/immune-brain/dist/imm-planner.md +4 -1
  13. package/plugins/immune-brain/dist/imm-run.md +198 -3
  14. package/plugins/immune-brain/dist/role-prompts/code-review.md +10 -0
  15. package/plugins/immune-brain/dist/role-prompts/lane-steward.md +44 -0
  16. package/plugins/immune-brain/runtime/assurance/coordinator.ts +22 -4
  17. package/plugins/immune-brain/runtime/assurance/review_mediation.ts +107 -0
  18. package/plugins/immune-brain/runtime/assurance/verdict_authority.ts +3 -0
  19. package/plugins/immune-brain/runtime/claude/interaction.ts +16 -0
  20. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +105 -58
  21. package/plugins/immune-brain/runtime/claude/mcp_server.ts +89 -16
  22. package/plugins/immune-brain/runtime/claude/role_agents.ts +4 -3
  23. package/plugins/immune-brain/runtime/kernel/assurance_projection.ts +4 -1
  24. package/plugins/immune-brain/runtime/kernel/canary_application.ts +9 -2
  25. package/plugins/immune-brain/runtime/kernel/completion.ts +24 -1
  26. package/plugins/immune-brain/runtime/kernel/reducer.ts +15 -2
  27. package/plugins/immune-brain/runtime/kernel/spec_binding.ts +6 -0
  28. package/plugins/immune-brain/runtime/kernel/storage.ts +20 -4
  29. package/plugins/immune-brain/runtime/kernel/storage_paths.ts +33 -3
  30. package/plugins/immune-brain/runtime/kernel/types.ts +7 -0
  31. package/plugins/immune-brain/runtime/kernel/validation.ts +13 -2
  32. package/plugins/immune-brain/runtime/local_initiative.ts +94 -0
  33. package/plugins/immune-brain/runtime/loop_contract.ts +55 -2
  34. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  35. package/plugins/immune-brain/runtime/prompts/code-review.md +10 -0
  36. package/plugins/immune-brain/runtime/prompts/lane-steward.md +44 -0
  37. package/plugins/immune-brain/runtime/role_prompt_bridge.ts +16 -3
  38. package/plugins/immune-brain/runtime/unattended/batch_disposition.ts +222 -0
  39. package/plugins/immune-brain/runtime/unattended/batch_integration.ts +387 -0
  40. package/plugins/immune-brain/runtime/unattended/batch_kernel_port.ts +2 -2
  41. package/plugins/immune-brain/runtime/unattended/batch_lanes.ts +946 -0
  42. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +36 -11
  43. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +48 -33
  44. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +13 -1
  45. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +22 -1
  46. package/plugins/immune-brain/runtime/unattended/batch_schedule.ts +162 -0
  47. package/plugins/immune-brain/runtime/unattended/batch_state.ts +398 -12
  48. package/plugins/immune-brain/runtime/unattended/types.ts +21 -2
  49. 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.1";
43
43
 
44
44
  // plugins/immune-brain/runtime/claude/interaction.ts
45
45
  import { createHash, randomUUID } from "node:crypto";
@@ -48,7 +48,8 @@ var PRIVILEGED_OPERATIONS = [
48
48
  "request_authorization",
49
49
  "approve_breaking_intent_revision",
50
50
  "stop",
51
- "start_unattended_batch"
51
+ "start_unattended_batch",
52
+ "retire_stale_batch"
52
53
  ];
53
54
  var RECOVERY_ACTIONS = {
54
55
  interaction_not_opened: "retry through a fresh native gate in the current Host",
@@ -732,6 +733,7 @@ var KERNEL_DB_RELATIVE = ".imm/state/kernel.sqlite";
732
733
  var KERNEL_STORE_SCHEMA_VERSION = 1;
733
734
  var FILE_STORE_CLAIM_RELATIVE = ".imm/state/active-claim.json";
734
735
  var FILE_STORE_WORKSPACE_RELATIVE = ".imm/state/workspace.json";
736
+ var FILE_STORE_TASKS_RELATIVE = ".imm/state/tasks";
735
737
  var FILE_STORE_TRANSACTIONS_RELATIVE = ".imm/state/transactions";
736
738
  var BATCH_STATE_RELATIVE = ".imm/state/batches";
737
739
  function stateDatabasePath() {
@@ -790,6 +792,25 @@ function batchCommitEvidencePath(batchId, taskId) {
790
792
  validateTaskId(taskId);
791
793
  return `${BATCH_STATE_RELATIVE}/commits/${batchId}-${taskId}.json`;
792
794
  }
795
+ function entryStatus(root, relativePath) {
796
+ const candidate = resolve(root, relativePath);
797
+ let stat;
798
+ try {
799
+ stat = lstatSync2(candidate);
800
+ } catch (error) {
801
+ const code = error.code;
802
+ if (code === "ENOENT" || code === "ENOTDIR")
803
+ return "absent";
804
+ return "other";
805
+ }
806
+ if (stat.isSymbolicLink())
807
+ return "symlink";
808
+ if (stat.isFile())
809
+ return "file";
810
+ if (stat.isDirectory())
811
+ return "directory";
812
+ return "other";
813
+ }
793
814
  function listEntries(root, relativePath) {
794
815
  const candidate = resolve(root, relativePath);
795
816
  try {
@@ -798,6 +819,44 @@ function listEntries(root, relativePath) {
798
819
  return null;
799
820
  }
800
821
  }
822
+ function readSmallFile(root, relativePath) {
823
+ const candidate = resolve(root, relativePath);
824
+ try {
825
+ const fd = openSync2(candidate, FS_CONSTANTS.O_RDONLY | (FS_CONSTANTS.O_NOFOLLOW ?? 0));
826
+ try {
827
+ const stat = fstatSync2(fd);
828
+ if (stat.size > 4 * 1024 * 1024)
829
+ throw new Error("file exceeds the inspection read bound");
830
+ return readFileSync(fd, "utf8");
831
+ } finally {
832
+ closeSync2(fd);
833
+ }
834
+ } catch {
835
+ return null;
836
+ }
837
+ }
838
+ function inspectRetiredStateResidue(root) {
839
+ const tasksStatus = entryStatus(root, FILE_STORE_TASKS_RELATIVE);
840
+ if (tasksStatus !== "absent" && tasksStatus !== "directory")
841
+ throw new Error(`${FILE_STORE_TASKS_RELATIVE} is not a regular directory`);
842
+ const entries = tasksStatus === "absent" ? [] : listEntries(root, FILE_STORE_TASKS_RELATIVE);
843
+ if (entries === null)
844
+ throw new Error(`${FILE_STORE_TASKS_RELATIVE} is unreadable`);
845
+ const workspaceStatus = entryStatus(root, FILE_STORE_WORKSPACE_RELATIVE);
846
+ let owner = null;
847
+ if (workspaceStatus !== "absent") {
848
+ if (workspaceStatus !== "file")
849
+ throw new Error(`${FILE_STORE_WORKSPACE_RELATIVE} is not a regular file`);
850
+ const content = readSmallFile(root, FILE_STORE_WORKSPACE_RELATIVE);
851
+ if (content === null)
852
+ throw new Error(`${FILE_STORE_WORKSPACE_RELATIVE} is unreadable`);
853
+ const raw = JSON.parse(content);
854
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw) || raw.contract !== "assurance_kernel/workspace/v1" || raw.current_working !== null && (typeof raw.current_working !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(raw.current_working)) || raw.current_working === null && Object.keys(raw).some((key) => !["contract", "current_working"].includes(key)))
855
+ throw new Error(`${FILE_STORE_WORKSPACE_RELATIVE} has invalid workspace content`);
856
+ owner = raw.current_working;
857
+ }
858
+ return { task_entries: entries, workspace_owner: owner };
859
+ }
801
860
 
802
861
  // plugins/immune-brain/runtime/kernel/sqlite_store.ts
803
862
  import { randomUUID as randomUUID2 } from "node:crypto";
@@ -2144,6 +2203,7 @@ function parseApprovalV2(value, index, violations, allowReviewRevision = false)
2144
2203
  "actor_id",
2145
2204
  "summary",
2146
2205
  "advisory_findings",
2206
+ "reviewer_verdict_sha256",
2147
2207
  ...allowReviewRevision ? ["review_revision"] : []
2148
2208
  ], `record.approvals[${index}]`, violations);
2149
2209
  const intentContentHash = stringAt(item.intent_content_hash, `record.approvals[${index}].intent_content_hash`, violations);
@@ -2164,7 +2224,8 @@ function parseApprovalV2(value, index, violations, allowReviewRevision = false)
2164
2224
  ...allowReviewRevision && item.review_revision !== undefined ? { review_revision: parseReviewRevisionIdentity(item.review_revision, `record.approvals[${index}].review_revision`, violations) } : {},
2165
2225
  ...item.advisory_findings !== undefined ? {
2166
2226
  advisory_findings: arrayAt(item.advisory_findings, `record.approvals[${index}].advisory_findings`, violations).map((entry, advisoryIndex) => parseAdvisoryFinding(entry, `record.approvals[${index}].advisory_findings[${advisoryIndex}]`, violations))
2167
- } : {}
2227
+ } : {},
2228
+ ...item.reviewer_verdict_sha256 !== undefined ? { reviewer_verdict_sha256: stringAt(item.reviewer_verdict_sha256, `record.approvals[${index}].reviewer_verdict_sha256`, violations) } : {}
2168
2229
  };
2169
2230
  }
2170
2231
  function parseAdvisoryFinding(value, path, violations) {
@@ -2211,6 +2272,7 @@ function parseAttestationV3(value, index, acceptanceIds, violations, allowReview
2211
2272
  "summary",
2212
2273
  "acceptance_results",
2213
2274
  "advisory_findings",
2275
+ "reviewer_verdict_sha256",
2214
2276
  ...allowReviewRevision ? ["review_revision"] : []
2215
2277
  ], path, violations);
2216
2278
  const kind = enumAt(item.kind, APPROVAL_KINDS, `${path}.kind`, violations);
@@ -2248,6 +2310,11 @@ function parseAttestationV3(value, index, acceptanceIds, violations, allowReview
2248
2310
  const advisoryFindings = item.advisory_findings === undefined ? undefined : arrayAt(item.advisory_findings, `${path}.advisory_findings`, violations).map((entry, advisoryIndex) => parseAdvisoryFinding(entry, `${path}.advisory_findings[${advisoryIndex}]`, violations));
2249
2311
  if (advisoryFindings !== undefined && kind !== "review")
2250
2312
  violations.push(`${path}.advisory_findings is only valid on review attestations`);
2313
+ const reviewerVerdictSha256 = item.reviewer_verdict_sha256 === undefined ? undefined : stringAt(item.reviewer_verdict_sha256, `${path}.reviewer_verdict_sha256`, violations);
2314
+ if (reviewerVerdictSha256 !== undefined && kind !== "review")
2315
+ violations.push(`${path}.reviewer_verdict_sha256 is only valid on review attestations`);
2316
+ if (reviewerVerdictSha256 !== undefined && !SHA256_HEX.test(reviewerVerdictSha256))
2317
+ violations.push(`${path}.reviewer_verdict_sha256 must be sha256:<64 hex>`);
2251
2318
  return {
2252
2319
  id: stringAt(item.id, `${path}.id`, violations),
2253
2320
  kind,
@@ -2259,7 +2326,8 @@ function parseAttestationV3(value, index, acceptanceIds, violations, allowReview
2259
2326
  summary: stringAt(item.summary, `${path}.summary`, violations),
2260
2327
  acceptance_results: acceptanceResults,
2261
2328
  ...reviewRevision ? { review_revision: reviewRevision } : {},
2262
- ...advisoryFindings !== undefined ? { advisory_findings: advisoryFindings } : {}
2329
+ ...advisoryFindings !== undefined ? { advisory_findings: advisoryFindings } : {},
2330
+ ...reviewerVerdictSha256 !== undefined ? { reviewer_verdict_sha256: reviewerVerdictSha256 } : {}
2263
2331
  };
2264
2332
  }
2265
2333
  function parseTaskRecordAtVersion(raw, version) {
@@ -2643,6 +2711,11 @@ function readBoundActiveSpec(root, intent, required = false) {
2643
2711
  throw error;
2644
2712
  }
2645
2713
  }
2714
+ function assertValidSpecBinding(intent) {
2715
+ const binding = inspectSpecBinding(intent);
2716
+ if (!binding.ok)
2717
+ throw new KernelInvariantError([binding.message]);
2718
+ }
2646
2719
  function inspectSpecBinding(intent) {
2647
2720
  const active = intent.scope_hint.filter((path) => ACTIVE_SPEC_RE.test(path));
2648
2721
  const archived = intent.scope_hint.filter((path) => ARCHIVED_SPEC_RE.test(path));
@@ -2696,6 +2769,8 @@ function asTaskDiffSnapshot(value) {
2696
2769
  return { diff_hash: value.diff_hash, changed_paths: value.changed_paths };
2697
2770
  }
2698
2771
  function resolveProjectedRisk(intent, changedPaths = []) {
2772
+ if (!inspectSpecBinding(intent).ok)
2773
+ return classifyTaskRisk(changedPaths, intent.risk);
2699
2774
  const excluded = ownSidecarPaths(intent);
2700
2775
  return classifyTaskRisk(changedPaths.filter((path) => !excluded.has(path)), intent.risk);
2701
2776
  }
@@ -2754,6 +2829,27 @@ function completionDecision(intent, record, currentDiffHash, currentIntentConten
2754
2829
  };
2755
2830
  }
2756
2831
  function projectTask(intent, record, currentDiffHash, currentIntentContentHash, changedPaths = []) {
2832
+ if (record.lifecycle === "active" && !inspectSpecBinding(intent).ok) {
2833
+ assertKernelInvariantsV3(intent, record);
2834
+ return {
2835
+ contract: "assurance_kernel/projection/v3",
2836
+ task_id: record.task_id,
2837
+ intent_revision: intent.revision,
2838
+ lifecycle: record.lifecycle,
2839
+ artifact_state: record.artifact_state,
2840
+ blocked: true,
2841
+ next_obligation: "revise_intent",
2842
+ complete: false,
2843
+ fresh_acceptance_ids: [],
2844
+ missing_acceptance_ids: intent.acceptance.map((item) => item.id),
2845
+ stale_attestation_ids: record.attestations.map((item) => item.id),
2846
+ missing_approval_kinds: REQUIRED_ATTESTATIONS[resolveProjectedRisk(intent, changedPaths)],
2847
+ blocking_finding_ids: record.findings.filter((item) => item.kind === "blocking" && item.status !== "resolved").map((item) => item.id),
2848
+ unresolved_user_decision_ids: record.findings.filter((item) => item.kind === "unresolved_user_decision" && item.status === "open").map((item) => item.id),
2849
+ replan_required_ids: record.findings.filter((item) => item.kind === "replan_required" && item.status === "open").map((item) => item.id),
2850
+ independence_violations: []
2851
+ };
2852
+ }
2757
2853
  const decision = completionDecision(intent, record, currentDiffHash, currentIntentContentHash, changedPaths);
2758
2854
  const blocked = decision.blocking_finding_ids.length > 0 || decision.unresolved_user_decision_ids.length > 0 || decision.replan_required_ids.length > 0 || decision.independence_violations.length > 0;
2759
2855
  let nextObligation = "none";
@@ -3044,27 +3140,37 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
3044
3140
  const reviewRevision = approval.review_revision;
3045
3141
  if (reviewRevision && approval.kind !== "review")
3046
3142
  throw new KernelInvariantError(["review_revision is only valid on review approvals"]);
3143
+ if (approval.reviewer_verdict_sha256 && approval.kind !== "review")
3144
+ throw new KernelInvariantError(["reviewer_verdict_sha256 is only valid on review approvals"]);
3047
3145
  if (approval.kind === "review") {
3048
3146
  if (!reviewRevision)
3049
3147
  throw new KernelInvariantError(["v4 review approval requires review_revision"]);
3050
3148
  if (reviewRevision.base_head !== record.git_base_head)
3051
3149
  throw new KernelInvariantError(["review_revision.base_head must equal the Enrollment git_base_head"]);
3052
3150
  }
3053
- const { review_revision: storedReviewRevision, ...approvalWithoutRevision } = approval;
3151
+ const {
3152
+ review_revision: storedReviewRevision,
3153
+ advisory_findings: storedAdvisoryFindings,
3154
+ reviewer_verdict_sha256: storedReviewerDigest,
3155
+ ...approvalWithoutExtras
3156
+ } = approval;
3054
3157
  record.attestations.push({
3055
- ...approvalWithoutRevision,
3158
+ ...approvalWithoutExtras,
3056
3159
  acceptance_results: approval.kind === "qa" ? record.intent_snapshot.acceptance.map((item) => ({
3057
3160
  acceptance_id: item.id,
3058
3161
  status: "passed",
3059
3162
  summary: `host-attested QA: ${approval.summary}`
3060
3163
  })) : [],
3061
- ...storedReviewRevision ? { review_revision: storedReviewRevision } : {}
3164
+ ...storedReviewRevision ? { review_revision: storedReviewRevision } : {},
3165
+ ...storedAdvisoryFindings ? { advisory_findings: storedAdvisoryFindings } : {},
3166
+ ...storedReviewerDigest ? { reviewer_verdict_sha256: storedReviewerDigest } : {}
3062
3167
  });
3063
3168
  appendHistory(record, action, from, approval.id, authorityAudit);
3064
3169
  break;
3065
3170
  }
3066
3171
  case "revise_intent":
3067
3172
  case "approve_breaking_intent_revision": {
3173
+ assertValidSpecBinding(action.next_intent);
3068
3174
  if (record.lifecycle !== "active")
3069
3175
  throw new KernelInvariantError([
3070
3176
  `cannot revise intent while lifecycle is ${record.lifecycle}`
@@ -3544,10 +3650,16 @@ function isRetiredFileProvablySuperseded(path, db, taskId) {
3544
3650
  }
3545
3651
  function assertNoRetiredFileStore(root, db, taskId) {
3546
3652
  const canonical = canonicalRoot2(root);
3653
+ let residue;
3654
+ try {
3655
+ residue = inspectRetiredStateResidue(canonical);
3656
+ } catch (error) {
3657
+ throw new KernelStoreSecurityError(`retired file-store residue is invalid: ${error instanceof Error ? error.message : String(error)}`);
3658
+ }
3547
3659
  const retired = [
3548
3660
  [".imm/tasks", "pre-cutover task store"],
3549
3661
  [".imm/workspace.json", "pre-cutover workspace owner"],
3550
- [".imm/state/tasks", "task records"]
3662
+ ...residue.task_entries.length > 0 ? [[".imm/state/tasks", "task records"]] : []
3551
3663
  ];
3552
3664
  for (const [path, label] of retired) {
3553
3665
  if (existsSync2(resolve5(canonical, path)))
@@ -3559,6 +3671,8 @@ function assertNoRetiredFileStore(root, db, taskId) {
3559
3671
  ];
3560
3672
  for (const [path, label] of derived) {
3561
3673
  const full = resolve5(canonical, path);
3674
+ if (path === FILE_STORE_WORKSPACE_RELATIVE && residue.workspace_owner === null)
3675
+ continue;
3562
3676
  if (!existsSync2(full))
3563
3677
  continue;
3564
3678
  if (db === undefined || typeof taskId !== "string" || taskId.length === 0)
@@ -3575,10 +3689,16 @@ function assertNoRetiredFileStore(root, db, taskId) {
3575
3689
  }
3576
3690
  function retiredFileStoreConflict(root, db, taskId) {
3577
3691
  const canonical = canonicalRoot2(root);
3692
+ let residue;
3693
+ try {
3694
+ residue = inspectRetiredStateResidue(canonical);
3695
+ } catch (error) {
3696
+ return `retired file-store residue is invalid: ${error instanceof Error ? error.message : String(error)}`;
3697
+ }
3578
3698
  const authority = [
3579
3699
  [".imm/tasks", "pre-cutover task store"],
3580
3700
  [".imm/workspace.json", "pre-cutover workspace owner"],
3581
- [".imm/state/tasks", "task records"]
3701
+ ...residue.task_entries.length > 0 ? [[".imm/state/tasks", "task records"]] : []
3582
3702
  ];
3583
3703
  for (const [path, label] of authority)
3584
3704
  if (existsSync2(resolve5(canonical, path)))
@@ -3590,7 +3710,7 @@ function retiredFileStoreConflict(root, db, taskId) {
3590
3710
  [FILE_STORE_WORKSPACE_RELATIVE, "workspace owner"]
3591
3711
  ];
3592
3712
  for (const [path, label] of derived)
3593
- if (existsSync2(resolve5(canonical, path)))
3713
+ if ((path !== FILE_STORE_WORKSPACE_RELATIVE || residue.workspace_owner !== null) && existsSync2(resolve5(canonical, path)))
3594
3714
  return `retired file-store authority is present (${label}: ${path}); import it with the supported migration before mutating this worktree`;
3595
3715
  }
3596
3716
  const transactions = resolve5(canonical, FILE_STORE_TRANSACTIONS_RELATIVE);
@@ -5222,7 +5342,7 @@ function projectFromRecord(record, recordRevision, workspaceRevision, snapshot)
5222
5342
  independence_violations: decision.independence_violations,
5223
5343
  open_user_decision_count: openUserDecisionCount,
5224
5344
  completion_ready: decision.complete,
5225
- authorization: deriveAssuranceAuthorization({
5345
+ authorization: !inspectSpecBinding(intent).ok ? { state: "none", blocked: "invalid Spec binding requires a valid breaking intent revision" } : deriveAssuranceAuthorization({
5226
5346
  next_obligation: decision.next_obligation,
5227
5347
  open_user_decision_count: openUserDecisionCount
5228
5348
  })
@@ -5555,6 +5675,7 @@ function createCanaryApplication(registry) {
5555
5675
  const current = readTaskRecordRaw(input.root, input.task_id);
5556
5676
  if (!current.record)
5557
5677
  throw new KernelInvariantError([`task ${input.task_id} has no TaskRecord v3`]);
5678
+ assertValidSpecBinding(current.record.intent_snapshot);
5558
5679
  const workspace = readWorkspaceStateRaw(input.root);
5559
5680
  if (current.record.artifact_state === "frozen")
5560
5681
  return { revision: current.revision, record: current.record, workspace };
@@ -5636,7 +5757,11 @@ function createCanaryApplication(registry) {
5636
5757
  const diffHash = asTaskDiffSnapshot((input.diffProvider ?? taskDeliveryIdentity)(input.root, snapshot.record)).diff_hash;
5637
5758
  if (operation.op === "stop" && !("capability" in operation))
5638
5759
  throw new KernelInvariantError(["stop requires user authority capability"]);
5639
- const hasBoundSpec = boundSpecPath(snapshot.intent_snapshot) !== undefined;
5760
+ if (operation.op === "approve_breaking_intent_revision")
5761
+ assertValidSpecBinding(operation.next_intent);
5762
+ else
5763
+ assertValidSpecBinding(snapshot.intent_snapshot);
5764
+ const hasBoundSpec = operation.op === "approve_breaking_intent_revision" ? boundSpecPath(operation.next_intent) !== undefined : boundSpecPath(snapshot.intent_snapshot) !== undefined;
5640
5765
  if (operation.op === "complete" && hasBoundSpec && snapshot.record.artifact_state !== "frozen")
5641
5766
  throw new KernelInvariantError(["complete requires frozen planning artifacts"]);
5642
5767
  const artifactTransition = snapshot.record.artifact_state === "frozen" && (operation.op === "request_rework" || operation.op === "authorize_rework" || operation.op === "approve_breaking_intent_revision") ? transitionFor(input.root, snapshot.record, "restore") : operation.op === "stop" && snapshot.record.artifact_state !== "frozen" ? transitionFor(input.root, snapshot.record, "freeze", true) : undefined;
@@ -6816,8 +6941,14 @@ var INTERNAL_ROLE_PROMPTS = {
6816
6941
  file: "compounder.md",
6817
6942
  authority: "compounder",
6818
6943
  tool_policy: "learning tools"
6944
+ },
6945
+ "lane-steward": {
6946
+ file: "lane-steward.md",
6947
+ authority: "lane-provision",
6948
+ tool_policy: "workspace tools"
6819
6949
  }
6820
6950
  };
6951
+ var LANE_EXECUTOR_HOSTS = ["claude-code", "pi"];
6821
6952
  function roleSpec(role) {
6822
6953
  const spec = INTERNAL_ROLE_PROMPTS[role];
6823
6954
  if (!spec)
@@ -7163,8 +7294,9 @@ function parseVerdictEvidence(value, index) {
7163
7294
  function parseAssuranceVerdict(input, snapshot) {
7164
7295
  let raw;
7165
7296
  if (typeof input === "string") {
7166
- const cleaned = input.split(`
7167
- `).map((line) => line.trim()).filter((line) => line.startsWith("{") && line.endsWith("}")).join("");
7297
+ const first = input.indexOf("{");
7298
+ const last = input.lastIndexOf("}");
7299
+ const cleaned = first >= 0 && last >= first ? input.slice(first, last + 1) : "";
7168
7300
  if (!cleaned)
7169
7301
  throw new Error("reviewer returned no strict JSON verdict");
7170
7302
  try {
@@ -7792,10 +7924,10 @@ class AssuranceCoordinator {
7792
7924
  signal?.removeEventListener("abort", relayExternalAbort);
7793
7925
  }
7794
7926
  }
7795
- async submitReview(taskId, ctx, verdictInput) {
7796
- return this.withRecovery(taskId, ctx, await this.submitReviewOnce(taskId, ctx, verdictInput));
7927
+ async submitReview(taskId, ctx, verdictInput, options) {
7928
+ return this.withRecovery(taskId, ctx, await this.submitReviewOnce(taskId, ctx, verdictInput, options));
7797
7929
  }
7798
- async submitReviewOnce(taskId, ctx, verdictInput) {
7930
+ async submitReviewOnce(taskId, ctx, verdictInput, options) {
7799
7931
  const unknown = this.unknownOperations.get(taskId);
7800
7932
  if (unknown)
7801
7933
  return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason };
@@ -7836,6 +7968,9 @@ class AssuranceCoordinator {
7836
7968
  reservation.verdictCorrectionRequired = true;
7837
7969
  return { state: "blocked", code: "verdict_invalid", reason: boundedAssuranceError(error) };
7838
7970
  }
7971
+ const receiptError = options?.validateReceipt?.();
7972
+ if (receiptError)
7973
+ return { state: "blocked", reason: receiptError, ...options?.receiptRecoveryAction ? { recovery_action: options.receiptRecoveryAction } : {} };
7839
7974
  const invocation = this.openInvocation(taskId);
7840
7975
  this.releaseReviewReservation(taskId, reservation);
7841
7976
  try {
@@ -7844,7 +7979,8 @@ class AssuranceCoordinator {
7844
7979
  snapshot: reservation.snapshot,
7845
7980
  verdict,
7846
7981
  invocation,
7847
- actorId: "parent-mediated-review"
7982
+ actorId: "parent-mediated-review",
7983
+ ...options?.reviewer_verdict_sha256 ? { reviewer_verdict_sha256: options.reviewer_verdict_sha256 } : {}
7848
7984
  });
7849
7985
  } catch (error) {
7850
7986
  const reason = boundedAssuranceError(error);
@@ -8137,7 +8273,8 @@ function createVerdictAuthority(options, registry = createMutationAuthorityRegis
8137
8273
  actor_id: actorId,
8138
8274
  summary: verdict.approval.summary,
8139
8275
  ...snapshot.role === "review" && snapshot.review_revision ? { review_revision: snapshot.review_revision } : {},
8140
- ...snapshot.role === "review" && advisories.length ? { advisory_findings: advisories } : {}
8276
+ ...snapshot.role === "review" && advisories.length ? { advisory_findings: advisories } : {},
8277
+ ...snapshot.role === "review" && input.reviewer_verdict_sha256 ? { reviewer_verdict_sha256: input.reviewer_verdict_sha256 } : {}
8141
8278
  } : undefined;
8142
8279
  const op = verdict.decision === "rework" ? "request_rework" : "record_approval";
8143
8280
  const payload = approval ? { approval } : { findings };
@@ -8192,9 +8329,9 @@ function createVerdictAuthority(options, registry = createMutationAuthorityRegis
8192
8329
  }
8193
8330
 
8194
8331
  // plugins/immune-brain/runtime/claude/kernel_ports.ts
8195
- import { existsSync as existsSync11, readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "node:fs";
8332
+ import { existsSync as existsSync12, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
8196
8333
  import { execFileSync as execFileSync8 } from "node:child_process";
8197
- import { join as join16 } from "node:path";
8334
+ import { join as join17 } from "node:path";
8198
8335
 
8199
8336
  // plugins/immune-brain/runtime/kernel/enrollment_authority.ts
8200
8337
  var ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollment-capability-brand");
@@ -9483,6 +9620,16 @@ var BATCH_REASONS = Object.freeze({
9483
9620
  state: "rejected",
9484
9621
  reason: (detail) => detail || "batch run rejected",
9485
9622
  recovery_action: "delete or rename the conflicting branch, or commit working changes and retry in the current Host"
9623
+ },
9624
+ stale_batch_absent: {
9625
+ state: "rejected",
9626
+ reason: (detail) => `no retirable batch record to dispose: ${detail}`,
9627
+ recovery_action: "start a batch for this Initiative, or inspect the batch records under .imm/state/batches in the current Host"
9628
+ },
9629
+ stale_batch_in_flight: {
9630
+ state: "blocked",
9631
+ reason: (detail) => `a batch child is still mid-flight, so the record cannot be disposed: ${detail}`,
9632
+ recovery_action: "settle or stop the in-flight child through its own task, then dispose the batch in the current Host"
9486
9633
  }
9487
9634
  });
9488
9635
  function batchReason(key, detail = "") {
@@ -10699,7 +10846,7 @@ async function runGithubTrackerOperation(root, input, gh = createGhTransport())
10699
10846
  }
10700
10847
 
10701
10848
  // plugins/immune-brain/runtime/unattended/batch_preflight.ts
10702
- import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync13 } from "node:fs";
10849
+ import { existsSync as existsSync8, readdirSync as readdirSync6, readFileSync as readFileSync14 } from "node:fs";
10703
10850
  import { randomUUID as randomUUID8 } from "node:crypto";
10704
10851
  import { join as join13 } from "node:path";
10705
10852
  import { spawnSync as spawnSync7 } from "node:child_process";
@@ -10711,6 +10858,9 @@ import { spawnSync as spawnSync6 } from "node:child_process";
10711
10858
  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
10859
  import { randomUUID as randomUUID7 } from "node:crypto";
10713
10860
  import { dirname as dirname7, join as join12 } from "node:path";
10861
+ function isLaneBatchRecord(record) {
10862
+ return record.contract === "assurance_kernel/batch_run_state/v2";
10863
+ }
10714
10864
  var BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10715
10865
  var CHILD_RUN_STATES = new Set([
10716
10866
  "pending",
@@ -10727,7 +10877,8 @@ var BATCH_RUN_STATES = new Set([
10727
10877
  "completed",
10728
10878
  "budget_stopped",
10729
10879
  "failed",
10730
- "rejected"
10880
+ "rejected",
10881
+ "superseded"
10731
10882
  ]);
10732
10883
  var ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
10733
10884
  function isCanonicalTimestamp(value) {
@@ -10808,7 +10959,7 @@ function validateRecordShape(value, batchId) {
10808
10959
  if (child.state === "committed" && child.commit !== null && !record.commits.includes(child.commit))
10809
10960
  throw new Error(`batch run state ${batchId} child ${child.task_id} commit is missing from commits`);
10810
10961
  }
10811
- if ((record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected") && record.children.some((child) => child.state === "enrolled" || child.state === "settled"))
10962
+ if ((record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected" || record.batch_state === "superseded") && record.children.some((child) => child.state === "enrolled" || child.state === "settled"))
10812
10963
  throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
10813
10964
  if (record.batch_state === "completed" && record.children.some((child) => child.state !== "committed"))
10814
10965
  throw new Error(`batch run state ${batchId} is completed but a child is not committed`);
@@ -10902,6 +11053,21 @@ function replaceBatchRunState(root, expected, next, validate) {
10902
11053
  return stored;
10903
11054
  });
10904
11055
  }
11056
+ function replaceAnyBatchRunState(root, expected, next, validate) {
11057
+ const path = statePath(next.batch_id);
11058
+ if (isLaneBatchRecord(next))
11059
+ validateLaneRecordShape(next, next.batch_id);
11060
+ else
11061
+ validateRecordShape(next, next.batch_id);
11062
+ return withKernelStoreLock(root, () => {
11063
+ if (!readSecureProjectBytes(root, path).equals(expected))
11064
+ throw new Error("batch state CAS mismatch");
11065
+ validate();
11066
+ const stored = withoutRetiredClock({ ...next, updated_at: new Date().toISOString() });
11067
+ writeFileAtomically(root, path, canonicalBytes(stored));
11068
+ return stored;
11069
+ });
11070
+ }
10905
11071
  function writeBatchRunState(root, record) {
10906
11072
  const path = statePath(record.batch_id);
10907
11073
  validateRecordShape(record, record.batch_id);
@@ -10918,6 +11084,162 @@ function writeBatchRunState(root, record) {
10918
11084
  return stored;
10919
11085
  });
10920
11086
  }
11087
+ var LANE_CHILD_STATES = new Set([
11088
+ "pending",
11089
+ "lane_admitted",
11090
+ "enrolled",
11091
+ "settled",
11092
+ "lane_committed",
11093
+ "integrated",
11094
+ "released",
11095
+ "needs_human",
11096
+ "skipped_blocked"
11097
+ ]);
11098
+ var LANE_BOUND_STATES = new Set([
11099
+ "lane_admitted",
11100
+ "enrolled",
11101
+ "settled",
11102
+ "lane_committed",
11103
+ "integrated",
11104
+ "released"
11105
+ ]);
11106
+ function validateLaneRecordShape(value, batchId) {
11107
+ if (typeof value !== "object" || value === null)
11108
+ throw new Error(`batch run state ${batchId} is not an object`);
11109
+ const record = value;
11110
+ if (record.contract !== "assurance_kernel/batch_run_state/v2")
11111
+ throw new Error(`batch run state ${batchId} has an unknown contract`);
11112
+ if (record.batch_id !== batchId)
11113
+ throw new Error(`batch run state ${batchId} carries batch_id ${String(record.batch_id)}`);
11114
+ if (typeof record.plan_digest !== "string" || !record.plan_digest)
11115
+ throw new Error(`batch run state ${batchId} has an invalid plan_digest`);
11116
+ if (typeof record.base_head !== "string" || !record.base_head)
11117
+ throw new Error(`batch run state ${batchId} has an invalid base_head`);
11118
+ if (record.branch !== undefined && (typeof record.branch !== "string" || !record.branch))
11119
+ throw new Error(`batch run state ${batchId} has an invalid branch`);
11120
+ if (!isCanonicalTimestamp(record.confirmation_time))
11121
+ throw new Error(`batch run state ${batchId} has an invalid confirmation_time`);
11122
+ if (!isCanonicalTimestamp(record.created_at) || !isCanonicalTimestamp(record.updated_at))
11123
+ throw new Error(`batch run state ${batchId} has invalid state timestamps`);
11124
+ if (!Array.isArray(record.children) || record.children.length === 0)
11125
+ throw new Error(`batch run state ${batchId} has no children`);
11126
+ if (!BATCH_RUN_STATES.has(String(record.batch_state)))
11127
+ throw new Error(`batch run state ${batchId} has an invalid batch_state`);
11128
+ if (typeof record.max_parallel !== "number" || !Number.isSafeInteger(record.max_parallel) || record.max_parallel <= 0)
11129
+ throw new Error(`batch run state ${batchId} has an invalid max_parallel`);
11130
+ const budget = record.budget;
11131
+ 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)
11132
+ throw new Error(`batch run state ${batchId} has an invalid budget`);
11133
+ if (!Array.isArray(record.commits) || record.commits.some((c) => typeof c !== "string"))
11134
+ throw new Error(`batch run state ${batchId} has an invalid commits list`);
11135
+ 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)))
11136
+ throw new Error(`batch run state ${batchId} has an invalid adopted_heads list`);
11137
+ const seenTaskIds = new Set;
11138
+ for (const child of record.children) {
11139
+ if (typeof child !== "object" || child === null || typeof child.task_id !== "string" || !child.task_id || typeof child.slice_id !== "string" || !child.slice_id)
11140
+ throw new Error(`batch run state ${batchId} has an invalid child entry`);
11141
+ if (!LANE_CHILD_STATES.has(String(child.state)))
11142
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid state`);
11143
+ if (!Array.isArray(child.blocked_by) || child.blocked_by.some((b) => typeof b !== "string"))
11144
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid blocked_by`);
11145
+ if (child.reason !== null && typeof child.reason !== "string" || child.commit !== null && typeof child.commit !== "string")
11146
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has invalid terminal fields`);
11147
+ if (typeof child.qa_failures !== "number" || !Number.isInteger(child.qa_failures) || child.qa_failures < 0)
11148
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid qa_failures`);
11149
+ const lane = child.lane;
11150
+ if (lane !== null) {
11151
+ 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" || lane.run_id !== undefined && lane.run_id !== null && (typeof lane.run_id !== "string" || !lane.run_id))
11152
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid lane`);
11153
+ }
11154
+ if (LANE_BOUND_STATES.has(child.state) && lane === null)
11155
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a lane`);
11156
+ if ((child.state === "pending" || child.state === "skipped_blocked") && lane !== null)
11157
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} but holds a lane`);
11158
+ const integrated = child.state === "integrated" || child.state === "released";
11159
+ if (integrated && child.commit === null)
11160
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a commit`);
11161
+ if (!integrated && child.commit !== null)
11162
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has a commit but is not integrated`);
11163
+ if (integrated && lane?.lane_commit === null)
11164
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is ${child.state} without a lane commit`);
11165
+ if (child.state === "lane_committed" && lane?.lane_commit === null)
11166
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is lane_committed without a lane commit`);
11167
+ if ((child.state === "needs_human" || child.state === "skipped_blocked") && child.reason === null)
11168
+ throw new Error(`batch run state ${batchId} child ${child.task_id} needs a terminal reason`);
11169
+ if (seenTaskIds.has(child.task_id))
11170
+ throw new Error(`batch run state ${batchId} has a duplicate child ${child.task_id}`);
11171
+ seenTaskIds.add(child.task_id);
11172
+ }
11173
+ for (const child of record.children) {
11174
+ if (child.commit !== null && !record.commits.includes(child.commit))
11175
+ throw new Error(`batch run state ${batchId} child ${child.task_id} commit is missing from commits`);
11176
+ }
11177
+ const inFlight = (child) => child.state === "lane_admitted" || child.state === "enrolled" || child.state === "settled" || child.state === "lane_committed";
11178
+ if ((record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected" || record.batch_state === "superseded") && record.children.some(inFlight))
11179
+ throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
11180
+ if (record.batch_state === "completed" && record.children.some((child) => child.state !== "integrated" && child.state !== "released"))
11181
+ throw new Error(`batch run state ${batchId} is completed but a child is not integrated`);
11182
+ }
11183
+ function prepareBatchLaneRunState(input) {
11184
+ validateBatchId(input.batch_id);
11185
+ return {
11186
+ contract: "assurance_kernel/batch_run_state/v2",
11187
+ batch_id: input.batch_id,
11188
+ initiative_slug: input.initiative_slug,
11189
+ plan_digest: input.plan_digest,
11190
+ base_head: input.base_head,
11191
+ branch: input.branch ?? `imm/${input.initiative_slug}`,
11192
+ confirmation_time: input.confirmation_time,
11193
+ budget: input.budget,
11194
+ max_parallel: input.max_parallel,
11195
+ batch_state: "prepared",
11196
+ children: input.children.map((child) => ({
11197
+ task_id: child.task_id,
11198
+ slice_id: child.slice_id,
11199
+ blocked_by: [...child.blocked_by],
11200
+ state: "pending",
11201
+ reason: null,
11202
+ commit: null,
11203
+ lane: null,
11204
+ qa_failures: 0
11205
+ })),
11206
+ commits: [],
11207
+ created_at: input.now,
11208
+ updated_at: input.now
11209
+ };
11210
+ }
11211
+ function parseBatchLaneRunState(raw, batchId) {
11212
+ validateBatchId(batchId);
11213
+ const parsed = JSON.parse(raw);
11214
+ validateLaneRecordShape(parsed, batchId);
11215
+ return withoutRetiredClock(parsed);
11216
+ }
11217
+ function parseAnyBatchRunState(raw, batchId) {
11218
+ validateBatchId(batchId);
11219
+ const parsed = JSON.parse(raw);
11220
+ if (typeof parsed === "object" && parsed !== null && parsed.contract === "assurance_kernel/batch_run_state/v2")
11221
+ return parseBatchLaneRunState(raw, batchId);
11222
+ return parseBatchRunState(raw, batchId);
11223
+ }
11224
+ function readAnyBatchRunState(root, batchId) {
11225
+ const path = statePath(batchId);
11226
+ if (!existsSync7(join12(root, path)))
11227
+ return null;
11228
+ return parseAnyBatchRunState(readSecureProjectFile(root, path), batchId);
11229
+ }
11230
+ function writeBatchLaneRunState(root, record) {
11231
+ const path = statePath(record.batch_id);
11232
+ validateLaneRecordShape(record, record.batch_id);
11233
+ return withKernelStoreLock(root, () => {
11234
+ const existing = existsSync7(join12(root, path)) ? readSecureProjectFile(root, path) : null;
11235
+ if (existing !== null && existing === canonicalBytes(record))
11236
+ return record;
11237
+ const stored = withoutRetiredClock({ ...record, updated_at: new Date().toISOString() });
11238
+ ensureSecureDirectory2(root, join12(".imm", "state", "batches"));
11239
+ writeFileAtomically(root, path, canonicalBytes(stored));
11240
+ return stored;
11241
+ });
11242
+ }
10921
11243
  function reportPath(batchId) {
10922
11244
  validateBatchId(batchId);
10923
11245
  return join12(".imm", "state", "batches", `${batchId}.report.json`);
@@ -10946,7 +11268,7 @@ function canonicalReportBytes(report) {
10946
11268
  `;
10947
11269
  }
10948
11270
  function isTerminalBatchState(state) {
10949
- return state === "completed" || state === "budget_stopped" || state === "failed" || state === "rejected";
11271
+ return state === "completed" || state === "budget_stopped" || state === "failed" || state === "rejected" || state === "superseded";
10950
11272
  }
10951
11273
 
10952
11274
  // plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts
@@ -11173,7 +11495,186 @@ async function captureBatchReconfirmation(root, record, children) {
11173
11495
 
11174
11496
  // plugins/immune-brain/runtime/unattended/batch_plan.ts
11175
11497
  import { createHash as createHash19 } from "node:crypto";
11498
+
11499
+ // plugins/immune-brain/runtime/local_initiative.ts
11500
+ import { lstatSync as lstatSync10, readFileSync as readFileSync13 } from "node:fs";
11501
+ import { resolve as resolve12 } from "node:path";
11176
11502
  var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
11503
+ var SLICE_HEADING = /^##\s+([^\s:]+)\s*:/;
11504
+ var BLOCKED_BY = /^blocked by\s*:(.*)$/i;
11505
+ var TASKS = /^tasks\s*:\s*$/i;
11506
+ var BULLET = /^[-*]\s+(.*)$/;
11507
+ function localInitiativePath(initiativeSlug) {
11508
+ return `docs/initiatives/${initiativeSlug}.md`;
11509
+ }
11510
+ function hasLocalInitiative(root, initiativeSlug) {
11511
+ const stat = lstatSync10(resolve12(root, localInitiativePath(initiativeSlug)), { throwIfNoEntry: false });
11512
+ if (!stat)
11513
+ return false;
11514
+ if (!stat.isFile())
11515
+ throw new Error(`${localInitiativePath(initiativeSlug)} must be a regular file`);
11516
+ return true;
11517
+ }
11518
+ function unquote(value) {
11519
+ return value.trim().replace(/^`(.*)`$/, "$1");
11520
+ }
11521
+ function observeLocalInitiative(root, initiativeSlug) {
11522
+ if (!ID_PATTERN2.test(initiativeSlug))
11523
+ throw new Error("initiative_slug is invalid");
11524
+ const path = localInitiativePath(initiativeSlug);
11525
+ if (!hasLocalInitiative(root, initiativeSlug))
11526
+ throw new Error(`Local Initiative ${path} does not exist`);
11527
+ const slices = [];
11528
+ let inTasks = false;
11529
+ let fence = false;
11530
+ for (const raw of readFileSync13(resolve12(root, path), "utf8").split(/\r?\n/)) {
11531
+ const line = raw.trim();
11532
+ if (line.startsWith("```")) {
11533
+ fence = !fence;
11534
+ continue;
11535
+ }
11536
+ if (fence)
11537
+ continue;
11538
+ const heading = SLICE_HEADING.exec(line);
11539
+ if (heading) {
11540
+ slices.push({ slice_id: heading[1], tasks: [], blocked_by: [] });
11541
+ inTasks = false;
11542
+ continue;
11543
+ }
11544
+ const slice = slices.at(-1);
11545
+ if (!slice)
11546
+ continue;
11547
+ if (TASKS.test(line)) {
11548
+ inTasks = true;
11549
+ continue;
11550
+ }
11551
+ const blocked = BLOCKED_BY.exec(line);
11552
+ if (blocked) {
11553
+ slice.blocked_by.push(...blocked[1].split(",").map(unquote).filter(Boolean));
11554
+ inTasks = false;
11555
+ continue;
11556
+ }
11557
+ const bullet = BULLET.exec(line);
11558
+ if (inTasks && bullet)
11559
+ slice.tasks.push(unquote(bullet[1]));
11560
+ else if (line)
11561
+ inTasks = false;
11562
+ }
11563
+ if (!slices.length)
11564
+ throw new Error(`Local Initiative ${path} declares no Slice`);
11565
+ return {
11566
+ contract: "immune_brain/local_initiative_observation/v1",
11567
+ initiative_id: initiativeSlug,
11568
+ path,
11569
+ tasks: slices.map((slice) => {
11570
+ if (slice.tasks.length !== 1)
11571
+ throw new Error(`Local Initiative Slice ${slice.slice_id} must name exactly one Task to run as a batch child`);
11572
+ return { task_id: slice.tasks[0], slice_id: slice.slice_id, blocked_by: slice.blocked_by };
11573
+ })
11574
+ };
11575
+ }
11576
+
11577
+ // plugins/immune-brain/runtime/unattended/batch_schedule.ts
11578
+ var IN_FLIGHT_STATES = new Set([
11579
+ "lane_admitted",
11580
+ "enrolled",
11581
+ "settled",
11582
+ "lane_committed"
11583
+ ]);
11584
+ var DONE_STATES = new Set(["committed", "integrated", "released"]);
11585
+ var WILDCARD = /[*?[\]{}!]/;
11586
+ function scopePrefix(entry) {
11587
+ if (typeof entry !== "string")
11588
+ return [];
11589
+ let value = entry.trim().toLowerCase();
11590
+ while (value.startsWith("./"))
11591
+ value = value.slice(2);
11592
+ if (!value || value.startsWith("/") || value.includes("\\"))
11593
+ return [];
11594
+ const wildcard = value.search(WILDCARD);
11595
+ const literal = wildcard === -1 ? value : value.slice(0, value.lastIndexOf("/", wildcard) + 1);
11596
+ const segments = literal.split("/").filter((segment) => segment.length > 0);
11597
+ if (segments.some((segment) => segment === "." || segment === ".."))
11598
+ return [];
11599
+ return segments;
11600
+ }
11601
+ function prefixOverlaps(left, right) {
11602
+ if (!left.length || !right.length)
11603
+ return true;
11604
+ const shared = Math.min(left.length, right.length);
11605
+ for (let index = 0;index < shared; index += 1)
11606
+ if (left[index] !== right[index])
11607
+ return false;
11608
+ return true;
11609
+ }
11610
+ function scopesOverlap(left, right) {
11611
+ if (!left.length || !right.length)
11612
+ return true;
11613
+ const rightPrefixes = right.map(scopePrefix);
11614
+ return left.some((entry) => {
11615
+ const prefix = scopePrefix(entry);
11616
+ return rightPrefixes.some((other) => prefixOverlaps(prefix, other));
11617
+ });
11618
+ }
11619
+ function positiveLimit(maxParallel) {
11620
+ if (maxParallel === Number.POSITIVE_INFINITY)
11621
+ return maxParallel;
11622
+ if (!Number.isSafeInteger(maxParallel) || maxParallel <= 0)
11623
+ throw new Error("max_parallel must be a positive safe integer");
11624
+ return maxParallel;
11625
+ }
11626
+ function startableChildren(children, maxParallel) {
11627
+ const limit = positiveLimit(maxParallel);
11628
+ const done = new Set(children.filter((child) => DONE_STATES.has(child.state)).map((child) => child.task_id));
11629
+ const occupied = children.filter((child) => IN_FLIGHT_STATES.has(child.state));
11630
+ const startable = [];
11631
+ for (const child of children) {
11632
+ if (occupied.length >= limit)
11633
+ break;
11634
+ if (child.state !== "pending")
11635
+ continue;
11636
+ if (!child.blocked_by.every((id) => done.has(id)))
11637
+ continue;
11638
+ if (occupied.some((other) => scopesOverlap(child.scope_hint, other.scope_hint)))
11639
+ continue;
11640
+ startable.push(child.task_id);
11641
+ occupied.push(child);
11642
+ }
11643
+ return startable;
11644
+ }
11645
+ function projectParallelGroups(children) {
11646
+ const remaining = [...children];
11647
+ const placed = new Set;
11648
+ const groups = [];
11649
+ const conflicts = new Map;
11650
+ while (remaining.length) {
11651
+ const ready = remaining.filter((child) => child.blocked_by.every((id) => placed.has(id)));
11652
+ if (!ready.length)
11653
+ throw new Error("Initiative Task dependencies must form an acyclic graph");
11654
+ const wave = [];
11655
+ for (const child of ready) {
11656
+ const overlapped = wave.filter((member) => scopesOverlap(child.scope_hint, member.scope_hint));
11657
+ if (overlapped.length) {
11658
+ const known = conflicts.get(child.task_id) ?? new Set;
11659
+ for (const member of overlapped)
11660
+ known.add(member.task_id);
11661
+ conflicts.set(child.task_id, known);
11662
+ continue;
11663
+ }
11664
+ wave.push(child);
11665
+ }
11666
+ groups.push(wave.map((child) => child.task_id));
11667
+ for (const child of wave) {
11668
+ placed.add(child.task_id);
11669
+ remaining.splice(remaining.indexOf(child), 1);
11670
+ }
11671
+ }
11672
+ 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() }));
11673
+ return { parallel_groups: groups, scope_conflicts: scopeConflicts };
11674
+ }
11675
+
11676
+ // plugins/immune-brain/runtime/unattended/batch_plan.ts
11677
+ var ID_PATTERN3 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
11177
11678
  var DEFAULT_QA_FAILURE_LIMIT = 2;
11178
11679
  function specBindingReason(inspection) {
11179
11680
  if (inspection.missing.length > 0)
@@ -11197,12 +11698,14 @@ function timestamp(value, name) {
11197
11698
  throw new Error(`${name} must be an ISO timestamp`);
11198
11699
  return { milliseconds, iso: new Date(milliseconds).toISOString() };
11199
11700
  }
11701
+ var observeInitiative = async (root, initiativeSlug) => hasLocalInitiative(root, initiativeSlug) ? observeLocalInitiative(root, initiativeSlug) : observeGithubInitiative(root, initiativeSlug);
11200
11702
  function normalizeObservation(value, initiativeSlug) {
11201
- if (!value || value.contract !== "immune_brain/github_initiative_observation/v1")
11703
+ const local = value?.contract === "immune_brain/local_initiative_observation/v1";
11704
+ if (!value || !local && value.contract !== "immune_brain/github_initiative_observation/v1")
11202
11705
  throw new Error("tracker returned an invalid Initiative observation contract");
11203
11706
  if (value.initiative_id !== initiativeSlug)
11204
11707
  throw new Error("tracker returned an observation for another Initiative");
11205
- if (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0)
11708
+ if (!local && (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0))
11206
11709
  throw new Error("tracker returned an invalid Initiative Issue number");
11207
11710
  if (!Array.isArray(value.tasks))
11208
11711
  throw new Error("tracker returned an invalid Initiative Task list");
@@ -11211,13 +11714,14 @@ function normalizeObservation(value, initiativeSlug) {
11211
11714
  const tasks = value.tasks.map((task, index) => {
11212
11715
  if (!task || typeof task !== "object")
11213
11716
  throw new Error(`tracker Task ${index} is invalid`);
11214
- if (!ID_PATTERN2.test(task.task_id))
11717
+ if (!ID_PATTERN3.test(task.task_id))
11215
11718
  throw new Error(`tracker Task ${index} has an invalid task_id`);
11216
- if (!ID_PATTERN2.test(task.slice_id))
11719
+ if (!ID_PATTERN3.test(task.slice_id))
11217
11720
  throw new Error(`tracker Task ${index} has an invalid slice_id`);
11218
- if (!Number.isSafeInteger(task.issue_number) || task.issue_number <= 0)
11721
+ const issueNumber = task.issue_number;
11722
+ if (!local && (!Number.isSafeInteger(issueNumber) || issueNumber <= 0))
11219
11723
  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)))
11724
+ if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN3.test(id)))
11221
11725
  throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
11222
11726
  if (taskIds.has(task.task_id))
11223
11727
  throw new Error(`tracker returned duplicate Task ${task.task_id}`);
@@ -11273,8 +11777,8 @@ function budget(input, enrollableCount) {
11273
11777
  const qaFailureLimit = input.budget?.qa_failure_limit === undefined ? DEFAULT_QA_FAILURE_LIMIT : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
11274
11778
  return { max_children: maxChildren, qa_failure_limit: qaFailureLimit };
11275
11779
  }
11276
- async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeGithubInitiative) {
11277
- if (!ID_PATTERN2.test(initiativeSlug))
11780
+ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeInitiative) {
11781
+ if (!ID_PATTERN3.test(initiativeSlug))
11278
11782
  throw new Error("initiative_slug is invalid");
11279
11783
  if (!input || typeof input !== "object")
11280
11784
  throw new Error("batch plan input is required");
@@ -11283,6 +11787,7 @@ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = ob
11283
11787
  const order = dependencyOrder(trackerObservation);
11284
11788
  const closures = dependencyClosures(order);
11285
11789
  const children = [];
11790
+ const scopeHints = new Map;
11286
11791
  for (const task of order) {
11287
11792
  const blockedBy = closures.get(task.task_id);
11288
11793
  const child = { task_id: task.task_id, slice_id: task.slice_id, blocked_by: blockedBy };
@@ -11324,6 +11829,7 @@ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = ob
11324
11829
  });
11325
11830
  continue;
11326
11831
  }
11832
+ scopeHints.set(task.task_id, [...read.intent.scope_hint]);
11327
11833
  children.push({
11328
11834
  ...child,
11329
11835
  status: "enrollable",
@@ -11369,7 +11875,12 @@ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = ob
11369
11875
  children,
11370
11876
  enrollable,
11371
11877
  plan_digest: planDigest,
11372
- budget: budget(input, enrollable.length)
11878
+ budget: budget(input, enrollable.length),
11879
+ ...projectParallelGroups(enrollable.map((child) => ({
11880
+ task_id: child.task_id,
11881
+ blocked_by: child.blocked_by,
11882
+ scope_hint: scopeHints.get(child.task_id)
11883
+ })))
11373
11884
  };
11374
11885
  }
11375
11886
 
@@ -11399,8 +11910,8 @@ function findResumableBatchSlugForTask(root, taskId) {
11399
11910
  if (!file.endsWith(".json"))
11400
11911
  continue;
11401
11912
  try {
11402
- const record = JSON.parse(readFileSync13(join13(batchesDir, file), "utf8"));
11403
- if (record?.contract !== "assurance_kernel/batch_run_state/v1")
11913
+ const record = JSON.parse(readFileSync14(join13(batchesDir, file), "utf8"));
11914
+ if (record?.contract !== "assurance_kernel/batch_run_state/v1" && record?.contract !== "assurance_kernel/batch_run_state/v2")
11404
11915
  continue;
11405
11916
  if (record.batch_state !== "running" && record.batch_state !== "needs_human")
11406
11917
  continue;
@@ -11421,31 +11932,23 @@ function findExistingActiveBatch(root, initiativeSlug) {
11421
11932
  continue;
11422
11933
  let record;
11423
11934
  try {
11424
- record = JSON.parse(readFileSync13(join13(batchesDir, file), "utf8"));
11935
+ record = JSON.parse(readFileSync14(join13(batchesDir, file), "utf8"));
11425
11936
  } catch {
11426
11937
  return { corrupt: true, path: file };
11427
11938
  }
11428
11939
  const candidate = record;
11429
- if (candidate?.contract !== "assurance_kernel/batch_run_state/v1")
11940
+ if (candidate?.contract !== "assurance_kernel/batch_run_state/v1" && candidate?.contract !== "assurance_kernel/batch_run_state/v2")
11430
11941
  continue;
11431
11942
  if (candidate.initiative_slug !== initiativeSlug)
11432
11943
  continue;
11433
- const validStates = new Set([
11434
- "prepared",
11435
- "running",
11436
- "needs_human",
11437
- "completed",
11438
- "budget_stopped",
11439
- "failed",
11440
- "rejected"
11441
- ]);
11944
+ const validStates = new Set(BATCH_RUN_STATES);
11442
11945
  if (typeof candidate.batch_id !== "string" || typeof candidate.base_head !== "string" || !Array.isArray(candidate.children) || !validStates.has(candidate.batch_state)) {
11443
11946
  return { corrupt: true, path: file };
11444
11947
  }
11445
11948
  if (isTerminalBatchState(candidate.batch_state))
11446
11949
  continue;
11447
11950
  try {
11448
- const record = readBatchRunState(root, file.slice(0, -5));
11951
+ const record = readAnyBatchRunState(root, file.slice(0, -5));
11449
11952
  if (record)
11450
11953
  return { corrupt: false, record };
11451
11954
  } catch {}
@@ -11463,12 +11966,12 @@ function findSettledBatchRecord(root, initiativeSlug) {
11463
11966
  continue;
11464
11967
  let record;
11465
11968
  try {
11466
- record = JSON.parse(readFileSync13(join13(batchesDir, file), "utf8"));
11969
+ record = JSON.parse(readFileSync14(join13(batchesDir, file), "utf8"));
11467
11970
  } catch {
11468
11971
  continue;
11469
11972
  }
11470
11973
  const candidate = record;
11471
- if (candidate?.contract !== "assurance_kernel/batch_run_state/v1")
11974
+ if (candidate?.contract !== "assurance_kernel/batch_run_state/v1" && candidate?.contract !== "assurance_kernel/batch_run_state/v2")
11472
11975
  continue;
11473
11976
  if (candidate.initiative_slug !== initiativeSlug)
11474
11977
  continue;
@@ -11612,6 +12115,8 @@ async function projectPlanSurface(input) {
11612
12115
  let recoveryChildren = [];
11613
12116
  let planDigest;
11614
12117
  let excluded = [];
12118
+ let parallelGroups = [];
12119
+ let scopeConflicts = [];
11615
12120
  const riskByTask = new Map;
11616
12121
  let budget;
11617
12122
  if (isResuming && existingBatch) {
@@ -11645,7 +12150,7 @@ async function projectPlanSurface(input) {
11645
12150
  }
11646
12151
  }
11647
12152
  riskByTask.set(c.task_id, read.intent?.risk ?? "material");
11648
- const isDone = c.state === "committed" || c.state === "settled";
12153
+ const isDone = ["committed", "settled", "integrated", "released"].includes(c.state);
11649
12154
  return {
11650
12155
  task_id: c.task_id,
11651
12156
  slice_id: c.slice_id,
@@ -11680,6 +12185,8 @@ async function projectPlanSurface(input) {
11680
12185
  if (!plan.enrollable.length)
11681
12186
  return { ok: false, key: "empty_enrollable_set", detail: "" };
11682
12187
  budget = plan.budget;
12188
+ parallelGroups = plan.parallel_groups;
12189
+ scopeConflicts = plan.scope_conflicts;
11683
12190
  const enrollableChildById = new Map(plan.enrollable.map((c) => [c.task_id, c]));
11684
12191
  recoveryChildren = plan.children.filter((c) => c.status === "enrollable").map((c) => {
11685
12192
  const digestChild = enrollableChildById.get(c.task_id);
@@ -11705,7 +12212,9 @@ async function projectPlanSurface(input) {
11705
12212
  plan_digest: planDigest,
11706
12213
  recovery_children: recoveryChildren,
11707
12214
  risk_by_task: Object.fromEntries([...riskByTask.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)),
11708
- excluded
12215
+ excluded,
12216
+ parallel_groups: parallelGroups,
12217
+ scope_conflicts: scopeConflicts
11709
12218
  }
11710
12219
  };
11711
12220
  }
@@ -11785,11 +12294,11 @@ async function projectBatchPreflight(options) {
11785
12294
  if (!planSurface.ok)
11786
12295
  return reject(planSurface.key, planSurface.detail);
11787
12296
  let reconfirmation;
11788
- if (activeRecord && activeRecord.plan_digest !== planSurface.surface.plan_digest) {
12297
+ if (activeRecord && !isLaneBatchRecord(activeRecord) && activeRecord.plan_digest !== planSurface.surface.plan_digest) {
11789
12298
  try {
11790
12299
  reconfirmation = await captureBatchReconfirmation(root, activeRecord, planSurface.surface.recovery_children);
11791
12300
  } catch {
11792
- return reject("plan_projection_failed", "batch plan reconfirmation is not eligible");
12301
+ return reject("plan_projection_failed", activeRecord.commits.length ? `batch plan changed after ${activeRecord.commits.length} recorded child commit(s). The old authorization cannot execute the revised plan. Preserve the batch record, report, commit evidence and audit; inspect each remaining child's Kernel run before an explicit imm-run handoff. Do not re-enroll settled children or manufacture batch trailers. To close the parked record explicitly, dispose it with the retire_stale_batch Tool for this Initiative; a child that is still mid-flight must settle first. This rejection neither terminates the old batch nor authorizes the revised plan.` : "batch plan reconfirmation is not eligible");
11793
12302
  }
11794
12303
  }
11795
12304
  const projection = {
@@ -11802,7 +12311,9 @@ async function projectBatchPreflight(options) {
11802
12311
  plan_digest: planSurface.surface.plan_digest,
11803
12312
  recovery_children: planSurface.surface.recovery_children,
11804
12313
  risk_by_task: planSurface.surface.risk_by_task,
11805
- excluded: planSurface.surface.excluded
12314
+ excluded: planSurface.surface.excluded,
12315
+ parallel_groups: planSurface.surface.parallel_groups,
12316
+ scope_conflicts: planSurface.surface.scope_conflicts
11806
12317
  };
11807
12318
  if (reconfirmation)
11808
12319
  Object.defineProperty(projection, "reconfirmation", { value: reconfirmation });
@@ -11849,7 +12360,7 @@ async function authorizeBatch(options) {
11849
12360
  const branchBefore = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11850
12361
  if (branchBefore.status !== 0)
11851
12362
  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);
12363
+ const ownsUnpersistedHead = isResuming && existingBatch !== null && !isLaneBatchRecord(existingBatch) && existingBatch.plan_digest === planDigest && expectedBatchHead(existingBatch) !== baseHead && ownUnpersistedBatchHead(root, existingBatch, baseHead);
11853
12364
  const fastForwardsRecordedHead = isResuming && existingBatch !== null && existingBatch.branch === batchBranch && expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead && classifyBatchLineage({
11854
12365
  root,
11855
12366
  branch: batchBranch,
@@ -11884,7 +12395,9 @@ async function authorizeBatch(options) {
11884
12395
  risk: projection.risk_by_task[child.task_id] ?? "material",
11885
12396
  status: child.status
11886
12397
  })),
11887
- excluded: projection.excluded
12398
+ excluded: projection.excluded,
12399
+ parallel_groups: projection.parallel_groups,
12400
+ scope_conflicts: projection.scope_conflicts
11888
12401
  };
11889
12402
  let requestId = null;
11890
12403
  if (!reuseAuthorization) {
@@ -11951,9 +12464,9 @@ async function authorizeBatch(options) {
11951
12464
  }
11952
12465
 
11953
12466
  // 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";
12467
+ import { spawnSync as spawnSync11 } from "node:child_process";
12468
+ import { existsSync as existsSync11 } from "node:fs";
12469
+ import { join as join16 } from "node:path";
11957
12470
 
11958
12471
  // plugins/immune-brain/runtime/unattended/batch_git.ts
11959
12472
  import { spawnSync as spawnSync8 } from "node:child_process";
@@ -11962,7 +12475,7 @@ import {
11962
12475
  constants as constants6,
11963
12476
  closeSync as closeSync7,
11964
12477
  existsSync as existsSync9,
11965
- lstatSync as lstatSync10,
12478
+ lstatSync as lstatSync11,
11966
12479
  mkdirSync as mkdirSync7,
11967
12480
  openSync as openSync7,
11968
12481
  realpathSync as realpathSync11,
@@ -12190,7 +12703,7 @@ function ensureSecureDirectory3(root, relativePath) {
12190
12703
  for (const segment of segments) {
12191
12704
  current = join14(current, segment);
12192
12705
  if (existsSync9(current)) {
12193
- const stats = lstatSync10(current);
12706
+ const stats = lstatSync11(current);
12194
12707
  if (stats.isSymbolicLink() || !stats.isDirectory()) {
12195
12708
  throw new Error(`${segment} exists but is not a real directory`);
12196
12709
  }
@@ -12204,12 +12717,12 @@ function writeFileAtomically2(root, relativePath, bytes) {
12204
12717
  const target = join14(root, relativePath);
12205
12718
  const targetDir = dirname8(target);
12206
12719
  ensureSecureDirectory3(root, dirname8(relativePath));
12207
- const stats = lstatSync10(targetDir);
12720
+ const stats = lstatSync11(targetDir);
12208
12721
  if (stats.isSymbolicLink() || !stats.isDirectory()) {
12209
12722
  throw new Error(`${dirname8(relativePath)} is not a real directory`);
12210
12723
  }
12211
12724
  if (existsSync9(target)) {
12212
- const targetStats = lstatSync10(target);
12725
+ const targetStats = lstatSync11(target);
12213
12726
  if (targetStats.isSymbolicLink()) {
12214
12727
  throw new Error(`${relativePath} is a symlink`);
12215
12728
  }
@@ -12518,40 +13031,958 @@ function createDefaultBatchGitPort() {
12518
13031
  };
12519
13032
  }
12520
13033
 
12521
- // plugins/immune-brain/runtime/unattended/batch_runner.ts
12522
- function batchQaFailureFacts(record) {
12523
- const failed = record.history.filter((event) => event.type === "request_rework" && event.authority?.authority_kind === "qa");
12524
- return {
12525
- qa_failure_count: failed.length,
12526
- last_qa_failure_at: failed.at(-1)?.at ?? null,
12527
- recovery_findings: record.findings.filter((f) => f.status === "open").map((f) => ({ id: f.id, kind: f.kind, status: f.status, acceptance_id: f.acceptance_id }))
12528
- };
13034
+ // plugins/immune-brain/runtime/unattended/batch_lanes.ts
13035
+ import { spawnSync as spawnSync10 } from "node:child_process";
13036
+ import { existsSync as existsSync10, realpathSync as realpathSync12 } from "node:fs";
13037
+ import { isAbsolute as isAbsolute6, join as join15 } from "node:path";
13038
+
13039
+ // plugins/immune-brain/runtime/unattended/batch_integration.ts
13040
+ import { spawnSync as spawnSync9 } from "node:child_process";
13041
+ class BatchIntegrationError extends Error {
13042
+ reason;
13043
+ constructor(reason, message) {
13044
+ super(message);
13045
+ this.reason = reason;
13046
+ this.name = "BatchIntegrationError";
13047
+ }
12529
13048
  }
12530
- function batchGitPortOf(input) {
12531
- return input.git ?? createDefaultBatchGitPort();
13049
+ var COMMITTER_ENV = {
13050
+ GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || "Immune-Brain Batch",
13051
+ GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || "immune-brain@local"
13052
+ };
13053
+ function git5(root, args, extra = {}) {
13054
+ const result = spawnSync9("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "-c", "commit.gpgsign=false", ...args], {
13055
+ encoding: "utf8",
13056
+ stdio: ["pipe", "pipe", "pipe"],
13057
+ input: extra.input,
13058
+ env: { ...process.env, ...COMMITTER_ENV, ...extra.env ?? {} }
13059
+ });
13060
+ return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
12532
13061
  }
12533
- function dependentsOf(record, taskId) {
12534
- return record.children.filter((child) => child.blocked_by.includes(taskId));
13062
+ function gitOut(root, args, what) {
13063
+ const result = git5(root, args);
13064
+ if (result.status !== 0)
13065
+ throw new Error(`${what}: ${result.stderr.trim() || "git failed"}`);
13066
+ return result.stdout;
12535
13067
  }
12536
- function skipDependents(record, taskId, reason) {
12537
- const skip = new Set([taskId]);
12538
- const queue = [taskId];
12539
- while (queue.length > 0) {
12540
- const current = queue.shift();
12541
- for (const dependent of dependentsOf(record, current)) {
12542
- if (skip.has(dependent.task_id))
12543
- continue;
12544
- skip.add(dependent.task_id);
12545
- queue.push(dependent.task_id);
12546
- }
13068
+ function changeIdentity(root, from, to) {
13069
+ 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}`);
13070
+ const tokens = raw.split("\x00");
13071
+ const identity = new Map;
13072
+ for (let index = 0;index < tokens.length; index += 1) {
13073
+ const meta = tokens[index];
13074
+ if (!meta.startsWith(":"))
13075
+ continue;
13076
+ const [, newMode, , newSha] = meta.slice(1).split(" ");
13077
+ const path = tokens[index + 1];
13078
+ if (path === undefined || !newMode || !newSha)
13079
+ throw new Error(`unreadable change record for ${from}..${to}`);
13080
+ identity.set(path, `${newMode} ${newSha}`);
13081
+ index += 1;
12547
13082
  }
12548
- record.children = record.children.map((child) => skip.has(child.task_id) && child.state === "pending" ? { ...child, state: "skipped_blocked", reason } : child);
13083
+ return identity;
12549
13084
  }
12550
- function budgetStopReason(record) {
12551
- const enrolledCount = record.children.filter((child) => child.state !== "pending" && child.state !== "skipped_blocked").length;
12552
- if (enrolledCount >= record.budget.max_children)
12553
- return `max_children budget exhausted (${record.budget.max_children})`;
12554
- return null;
13085
+ function identitiesEqual(left, right) {
13086
+ if (left.size !== right.size)
13087
+ return false;
13088
+ for (const [path, value] of left)
13089
+ if (right.get(path) !== value)
13090
+ return false;
13091
+ return true;
13092
+ }
13093
+ function singleParent(root, commit) {
13094
+ const parents = gitOut(root, ["rev-parse", `${commit}^@`], `failed to read parents of ${commit}`).split(/\s+/).filter(Boolean);
13095
+ if (parents.length !== 1)
13096
+ throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: lane commit ${commit} has ${parents.length} parents, expected exactly one`);
13097
+ return parents[0];
13098
+ }
13099
+ function isAncestor(root, ancestor, descendant) {
13100
+ return git5(root, ["merge-base", "--is-ancestor", ancestor, descendant]).status === 0;
13101
+ }
13102
+ function buildCandidate(input) {
13103
+ const { root, batch_head: head, lane_base: base, lane_commit: laneCommit } = input;
13104
+ if (head === base)
13105
+ return laneCommit;
13106
+ const merged = git5(root, ["merge-tree", "--write-tree", `--merge-base=${base}`, head, laneCommit]);
13107
+ if (merged.status === 1)
13108
+ throw new BatchIntegrationError("batch_integration_conflict", `batch_integration_conflict: lane commit ${laneCommit} does not apply cleanly onto batch head ${head}`);
13109
+ if (merged.status !== 0)
13110
+ throw new Error(`failed to build integration candidate: ${merged.stderr.trim() || "git merge-tree failed"}`);
13111
+ const tree = merged.stdout.split(`
13112
+ `)[0]?.trim();
13113
+ if (!tree)
13114
+ throw new Error("failed to build integration candidate: no tree produced");
13115
+ const message = gitOut(root, ["log", "-n", "1", "--format=%B", laneCommit], "failed to read lane commit message");
13116
+ const author = gitOut(root, ["log", "-n", "1", "--format=%an%x00%ae%x00%aI", laneCommit], "failed to read lane commit author").trim().split("\x00");
13117
+ const created = git5(root, ["commit-tree", tree, "-p", head, "-F", "-"], {
13118
+ input: message,
13119
+ env: {
13120
+ GIT_AUTHOR_NAME: author[0] ?? COMMITTER_ENV.GIT_COMMITTER_NAME,
13121
+ GIT_AUTHOR_EMAIL: author[1] ?? COMMITTER_ENV.GIT_COMMITTER_EMAIL,
13122
+ ...author[2] ? { GIT_AUTHOR_DATE: author[2] } : {}
13123
+ }
13124
+ });
13125
+ const candidate = created.stdout.trim();
13126
+ if (created.status !== 0 || !candidate)
13127
+ throw new Error(`failed to build integration candidate: ${created.stderr.trim() || "git commit-tree failed"}`);
13128
+ return candidate;
13129
+ }
13130
+ function prepareLaneCandidate(input) {
13131
+ const { root, branch, batch_head: head, lane_base: base, lane_commit: laneCommit } = input;
13132
+ const currentBranch = git5(root, ["symbolic-ref", "--short", "HEAD"]);
13133
+ if (currentBranch.status !== 0 || currentBranch.stdout.trim() !== branch)
13134
+ throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: current branch ${currentBranch.stdout.trim()} does not match expected branch ${branch}`);
13135
+ const currentHead = git5(root, ["rev-parse", "HEAD"]).stdout.trim();
13136
+ if (currentHead !== head)
13137
+ throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: current HEAD ${currentHead} does not match expected batch head ${head}`);
13138
+ if (singleParent(root, laneCommit) !== base)
13139
+ throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: lane commit ${laneCommit} does not descend from lane base ${base}`);
13140
+ if (!isAncestor(root, base, head))
13141
+ throw new BatchIntegrationError("batch_head_lineage_broken", `batch_head_lineage_broken: lane base ${base} is not an ancestor of batch head ${head}`);
13142
+ const candidate = buildCandidate({ root, batch_head: head, lane_base: base, lane_commit: laneCommit });
13143
+ if (!identitiesEqual(changeIdentity(root, base, laneCommit), changeIdentity(root, head, candidate)))
13144
+ throw new BatchIntegrationError("batch_integration_conflict", `batch_integration_conflict: candidate ${candidate} does not carry the lane commit ${laneCommit} change`);
13145
+ return candidate;
13146
+ }
13147
+ function fastForwardToCandidate(root, candidate) {
13148
+ const moved = git5(root, ["merge", "--ff-only", "--quiet", candidate]);
13149
+ if (moved.status !== 0)
13150
+ throw new BatchIntegrationError("batch_integration_conflict", `batch_integration_conflict: batch branch could not fast-forward to ${candidate}: ${moved.stderr.trim() || "git merge failed"}`);
13151
+ const landed = git5(root, ["rev-parse", "HEAD"]).stdout.trim();
13152
+ if (landed !== candidate)
13153
+ throw new Error(`batch branch landed on ${landed}, expected ${candidate}`);
13154
+ return { commit: candidate };
13155
+ }
13156
+ function commitsBetween(root, from, to) {
13157
+ return new Set(gitOut(root, ["rev-list", `${from}..${to}`], `failed to list commits ${from}..${to}`).split(/\s+/).filter(Boolean));
13158
+ }
13159
+ function readCandidateAcceptance(root, candidate, check) {
13160
+ if (!check.intent_path)
13161
+ throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: ${check.task_id} has no intent to rerun`);
13162
+ const shown = git5(root, ["show", `${candidate}:${check.intent_path}`]);
13163
+ if (shown.status !== 0)
13164
+ throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: intent ${check.intent_path} of ${check.task_id} is absent from candidate ${candidate}`);
13165
+ try {
13166
+ const intent = parseTaskIntentV1(JSON.parse(shown.stdout));
13167
+ return intent.acceptance.map((item) => ({ id: `${check.task_id}:${item.id}`, assertion: item.assertion, verification: item.verification }));
13168
+ } catch (error) {
13169
+ 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)}`);
13170
+ }
13171
+ }
13172
+ async function verifyCandidateDescriptors(input) {
13173
+ const { root, candidate } = input;
13174
+ const since = commitsBetween(root, input.lane_base, input.batch_head);
13175
+ const checks = [input.child, ...input.siblings.filter((sibling) => sibling.commit !== null && since.has(sibling.commit))];
13176
+ const acceptance = checks.flatMap((check) => readCandidateAcceptance(root, candidate, check));
13177
+ const descriptors = new Map;
13178
+ try {
13179
+ for (const item of acceptance)
13180
+ descriptors.set(item.id, parseVerificationDescriptor(item.verification));
13181
+ } catch (error) {
13182
+ throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: a verification descriptor is invalid: ${error instanceof Error ? error.message : String(error)}`);
13183
+ }
13184
+ if (acceptance.length === 0)
13185
+ throw new BatchIntegrationError("batch_integration_check_failed", "batch_integration_check_failed: no descriptor to rerun");
13186
+ const tree = gitOut(root, ["rev-parse", `${candidate}^{tree}`], `failed to read the tree of ${candidate}`).trim();
13187
+ const snapshot = {
13188
+ contract: "assurance_kernel/assurance_snapshot/v2",
13189
+ task_id: input.child.task_id,
13190
+ run_id: null,
13191
+ role: "qa",
13192
+ record_revision: "",
13193
+ workspace_revision: "",
13194
+ intent_revision: 0,
13195
+ intent_content_hash: "",
13196
+ diff_hash: "",
13197
+ lifecycle: "active",
13198
+ artifact_state: "frozen",
13199
+ risk: "material",
13200
+ fresh_acceptance_ids: [],
13201
+ missing_acceptance_ids: [],
13202
+ stale_attestation_ids: [],
13203
+ acceptance,
13204
+ dirty_files: [],
13205
+ review_bundle_digest: null,
13206
+ review_revision: {
13207
+ contract: "assurance_kernel/review_revision_identity/v1",
13208
+ base_head: input.lane_base,
13209
+ review_commit: candidate,
13210
+ review_tree: tree,
13211
+ manifest_digest: ""
13212
+ },
13213
+ root
13214
+ };
13215
+ let verdict;
13216
+ try {
13217
+ verdict = await runDeterministicQa(snapshot, descriptors, { signal: input.signal });
13218
+ } catch (error) {
13219
+ if (error instanceof VerificationAbortedError)
13220
+ throw error;
13221
+ throw new BatchIntegrationError("batch_integration_check_failed", `batch_integration_check_failed: the descriptor rerun could not complete: ${error instanceof Error ? error.message : String(error)}`);
13222
+ }
13223
+ if (verdict.decision !== "pass")
13224
+ 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}`);
13225
+ }
13226
+ async function integrateGuardedLaneCommit(input) {
13227
+ const candidate = prepareLaneCandidate(input);
13228
+ await verifyCandidateDescriptors({
13229
+ root: input.root,
13230
+ candidate,
13231
+ lane_base: input.lane_base,
13232
+ batch_head: input.batch_head,
13233
+ child: input.child,
13234
+ siblings: input.siblings,
13235
+ signal: input.signal
13236
+ });
13237
+ return fastForwardToCandidate(input.root, candidate);
13238
+ }
13239
+ function findIntegratedCandidate(input) {
13240
+ const { root, task_id: taskId, batch_id: batchId, from_head: from, lane_base: base, lane_commit: laneCommit } = input;
13241
+ const listed = git5(root, [
13242
+ "log",
13243
+ "--fixed-strings",
13244
+ `--grep=imm(${taskId}):`,
13245
+ "--format=%x01%H%x00%(trailers:key=Immune-Brain-Batch,valueonly)%x00%s",
13246
+ `${from}..HEAD`
13247
+ ]);
13248
+ if (listed.status !== 0)
13249
+ throw new Error(`failed to search for integrated ${taskId}: ${listed.stderr.trim()}`);
13250
+ const lane = changeIdentity(root, base, laneCommit);
13251
+ for (const record of listed.stdout.split("\x01")) {
13252
+ const [commit, trailer, subject] = record.split("\x00");
13253
+ if (!commit || trailer?.trim() !== batchId || !subject?.trim().startsWith(`imm(${taskId}):`))
13254
+ continue;
13255
+ const parent = git5(root, ["rev-parse", `${commit}^`]).stdout.trim();
13256
+ if (parent && identitiesEqual(lane, changeIdentity(root, parent, commit)))
13257
+ return commit;
13258
+ }
13259
+ return null;
13260
+ }
13261
+
13262
+ // plugins/immune-brain/runtime/unattended/batch_lanes.ts
13263
+ var PARALLEL_MISMATCH = "batch_parallel_mismatch";
13264
+ var TASK_ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
13265
+ var MAX_LANE_OFFERS = 64;
13266
+ var MAX_PATH_LENGTH = 4096;
13267
+ function resolveLaneParallel(root, initiativeSlug, maxParallel, laneOffers) {
13268
+ if (maxParallel !== undefined || laneOffers === undefined)
13269
+ return maxParallel;
13270
+ const lookup = findExistingActiveBatch(root, initiativeSlug);
13271
+ if (lookup === null || lookup.corrupt || !isLaneBatchRecord(lookup.record))
13272
+ throw new Error("lane_offers requires max_parallel unless a recorded lane batch is being resumed");
13273
+ return lookup.record.max_parallel;
13274
+ }
13275
+ function parseMaxParallel(value) {
13276
+ if (value === undefined || value === null)
13277
+ return;
13278
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0)
13279
+ throw new Error("invalid max_parallel: expected a positive integer");
13280
+ return value;
13281
+ }
13282
+ function parseLaneOffers(value) {
13283
+ if (value === undefined || value === null)
13284
+ return;
13285
+ if (!Array.isArray(value))
13286
+ throw new Error("invalid lane_offers: expected an array");
13287
+ if (value.length > MAX_LANE_OFFERS)
13288
+ throw new Error(`invalid lane_offers: at most ${MAX_LANE_OFFERS} offers`);
13289
+ const seen = new Set;
13290
+ return value.map((entry) => {
13291
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry))
13292
+ throw new Error("invalid lane_offers: each offer must be an object");
13293
+ const { task_id: taskId, path, ...rest } = entry;
13294
+ if (Object.keys(rest).length > 0)
13295
+ throw new Error("invalid lane_offers: unknown offer field");
13296
+ if (typeof taskId !== "string" || !TASK_ID_PATTERN2.test(taskId))
13297
+ throw new Error("invalid lane_offers: task_id is not a valid task id");
13298
+ if (typeof path !== "string" || !path || path.length > MAX_PATH_LENGTH || path.includes("\x00") || !isAbsolute6(path))
13299
+ throw new Error("invalid lane_offers: path must be an absolute path");
13300
+ if (seen.has(taskId))
13301
+ throw new Error(`invalid lane_offers: duplicate offer for ${taskId}`);
13302
+ seen.add(taskId);
13303
+ return { task_id: taskId, path };
13304
+ });
13305
+ }
13306
+ function laneBranchName(initiativeSlug, taskId) {
13307
+ return `imm-lane/${initiativeSlug}/${taskId}`;
13308
+ }
13309
+ function gitRead(root, args) {
13310
+ const result = spawnSync10("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
13311
+ return { status: result.status, stdout: (result.stdout ?? "").trim() };
13312
+ }
13313
+ function commonDir(root) {
13314
+ const result = gitRead(root, ["rev-parse", "--path-format=absolute", "--git-common-dir"]);
13315
+ if (result.status !== 0 || !result.stdout)
13316
+ return null;
13317
+ try {
13318
+ return realpathSync12(result.stdout);
13319
+ } catch {
13320
+ return null;
13321
+ }
13322
+ }
13323
+ function resolveRealPath(path) {
13324
+ try {
13325
+ return realpathSync12(path);
13326
+ } catch {
13327
+ return null;
13328
+ }
13329
+ }
13330
+ function createDefaultLaneGitPort() {
13331
+ return {
13332
+ resolveRoot: (root) => resolveRealPath(root) ?? root,
13333
+ inspectLane(coordinatorRoot, lanePath) {
13334
+ const real = existsSync10(lanePath) ? resolveRealPath(lanePath) : null;
13335
+ const absent = {
13336
+ exists: real !== null,
13337
+ real_path: real,
13338
+ same_repository: false,
13339
+ is_worktree_root: false,
13340
+ branch: null,
13341
+ head: null,
13342
+ clean: false,
13343
+ active_claim_task_id: null
13344
+ };
13345
+ if (!real)
13346
+ return absent;
13347
+ const laneCommon = commonDir(real);
13348
+ const coordinatorCommon = commonDir(coordinatorRoot);
13349
+ if (!laneCommon || !coordinatorCommon || laneCommon !== coordinatorCommon)
13350
+ return absent;
13351
+ const top = gitRead(real, ["rev-parse", "--show-toplevel"]);
13352
+ const topReal = top.status === 0 ? resolveRealPath(top.stdout) : null;
13353
+ const branch = gitRead(real, ["symbolic-ref", "--short", "HEAD"]);
13354
+ const head = gitRead(real, ["rev-parse", "HEAD"]);
13355
+ const status = gitRead(real, ["status", "--porcelain", "--untracked-files=all", "--ignore-submodules=none"]);
13356
+ let activeClaim = null;
13357
+ try {
13358
+ activeClaim = readActiveClaimTaskId(real);
13359
+ } catch {
13360
+ activeClaim = "unreadable";
13361
+ }
13362
+ return {
13363
+ exists: true,
13364
+ real_path: real,
13365
+ same_repository: true,
13366
+ is_worktree_root: topReal === real,
13367
+ branch: branch.status === 0 ? branch.stdout : null,
13368
+ head: head.status === 0 ? head.stdout : null,
13369
+ clean: status.status === 0 && status.stdout === "",
13370
+ active_claim_task_id: activeClaim
13371
+ };
13372
+ },
13373
+ integrate: integrateGuardedLaneCommit,
13374
+ findIntegrated: findIntegratedCandidate,
13375
+ auditReachable({ root, head, task_id }) {
13376
+ const listing = gitRead(root, ["ls-tree", "-r", "--name-only", head, "--", `.imm/audit/${task_id}`]);
13377
+ if (listing.status !== 0)
13378
+ return false;
13379
+ const files = listing.stdout.split(`
13380
+ `);
13381
+ const runDirs = new Set(files.map((file) => file.slice(0, file.lastIndexOf("/"))));
13382
+ return [...runDirs].some((dir) => files.includes(`${dir}/task-record.json`) && files.includes(`${dir}/terminal-proof.json`));
13383
+ }
13384
+ };
13385
+ }
13386
+ function decideLaneAdmission(input) {
13387
+ const { facts } = input;
13388
+ if (!facts.exists || !facts.real_path || !facts.same_repository || !facts.is_worktree_root)
13389
+ return "batch_lane_foreign_repository";
13390
+ if (facts.real_path === input.coordinator_real_path)
13391
+ return "batch_lane_is_coordinator";
13392
+ if (facts.branch !== input.expected_branch)
13393
+ return "batch_lane_branch_mismatch";
13394
+ if (facts.head !== input.batch_head)
13395
+ return "batch_lane_base_mismatch";
13396
+ if (!facts.clean)
13397
+ return "batch_lane_dirty";
13398
+ if (facts.active_claim_task_id !== null || input.bound_paths.includes(facts.real_path))
13399
+ return "batch_lane_occupied";
13400
+ return null;
13401
+ }
13402
+ function laneGitOf(input) {
13403
+ return input.git?.lane ?? createDefaultLaneGitPort();
13404
+ }
13405
+ function serialGitOf(input) {
13406
+ return input.git ?? defaultSerialGit();
13407
+ }
13408
+ function releaseHandoffs(input, record, lanes) {
13409
+ const lineage = classifyBatchLineage({
13410
+ root: input.root,
13411
+ branch: record.branch ?? "",
13412
+ expectedHead: expectedBatchHead(record),
13413
+ childCommits: record.commits,
13414
+ batchId: record.batch_id
13415
+ });
13416
+ if (lineage.kind === "broken")
13417
+ return [];
13418
+ const handoffs = [];
13419
+ for (const child of record.children) {
13420
+ if (child.state !== "integrated" || !child.lane)
13421
+ continue;
13422
+ const facts = lanes.inspectLane(input.root, child.lane.path);
13423
+ if (!facts.exists || !facts.same_repository || !facts.is_worktree_root || !facts.clean || facts.branch !== child.lane.branch || facts.active_claim_task_id !== null)
13424
+ continue;
13425
+ if (!lanes.auditReachable({ root: input.root, head: lineage.head, task_id: child.task_id }))
13426
+ continue;
13427
+ handoffs.push({ role: "lane-steward", action: "release", task_id: child.task_id, lane_branch: child.lane.branch });
13428
+ }
13429
+ return handoffs;
13430
+ }
13431
+ function defaultSerialGit() {
13432
+ return createDefaultBatchGitPort();
13433
+ }
13434
+ var IN_FLIGHT = new Set(["lane_admitted", "enrolled", "settled", "lane_committed"]);
13435
+ function dependentsOf(record, taskId) {
13436
+ return record.children.filter((child) => child.blocked_by.includes(taskId));
13437
+ }
13438
+ function skipDependents(record, taskId, reason) {
13439
+ const skip = new Set([taskId]);
13440
+ const queue = [taskId];
13441
+ while (queue.length > 0) {
13442
+ const current = queue.shift();
13443
+ for (const dependent of dependentsOf(record, current)) {
13444
+ if (skip.has(dependent.task_id))
13445
+ continue;
13446
+ skip.add(dependent.task_id);
13447
+ queue.push(dependent.task_id);
13448
+ }
13449
+ }
13450
+ record.children = record.children.map((child) => skip.has(child.task_id) && child.state === "pending" ? { ...child, state: "skipped_blocked", reason } : child);
13451
+ }
13452
+ function consumedSlots(record) {
13453
+ return record.children.filter((child) => child.state !== "pending" && child.state !== "skipped_blocked").length;
13454
+ }
13455
+ function scopeOfChild(root, child) {
13456
+ try {
13457
+ return [...readTaskIntent(root, child.task_id, child.intent_path ?? undefined).intent.scope_hint];
13458
+ } catch {
13459
+ return ["**"];
13460
+ }
13461
+ }
13462
+ function scheduleView(input, record) {
13463
+ return record.children.map((child) => ({
13464
+ task_id: child.task_id,
13465
+ state: child.state,
13466
+ blocked_by: child.blocked_by,
13467
+ scope_hint: scopeOfChild(input.root, input.children.find((c) => c.task_id === child.task_id) ?? { task_id: child.task_id, intent_path: null })
13468
+ }));
13469
+ }
13470
+ 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.";
13471
+ var TERMINAL_NEXT_ACTIONS = {
13472
+ completed: "The batch integrated every enrollable child; review the commits and the tracker.",
13473
+ budget_stopped: "The child budget stopped new enrollments; re-confirm to continue under a new authorization.",
13474
+ failed: "A lineage failure stopped the batch; inspect the failing child and the branch state.",
13475
+ rejected: "The batch was rejected before any enrollment; correct the stated reason and re-confirm.",
13476
+ superseded: "The plan moved past this record and an explicit disposition retired it; it grants no handoff and no child delivery.",
13477
+ needs_human: LANE_NEEDS_HUMAN_NEXT_ACTION,
13478
+ running: "The batch is still running; no terminal report is due yet.",
13479
+ prepared: "The batch is prepared but not started."
13480
+ };
13481
+ function laneReport(record, reason, nextAction, extra = {}) {
13482
+ return {
13483
+ contract: "assurance_kernel/batch_run_report/v1",
13484
+ batch_id: record.batch_id,
13485
+ initiative_slug: record.initiative_slug,
13486
+ batch_state: record.batch_state,
13487
+ max_parallel: record.max_parallel,
13488
+ children: record.children,
13489
+ commits: record.commits,
13490
+ reason,
13491
+ handoffs: extra.handoffs ?? [],
13492
+ ...extra.refusals?.length ? { lane_refusals: extra.refusals } : {},
13493
+ next_action: nextAction || (TERMINAL_NEXT_ACTIONS[record.batch_state] ?? "Inspect the batch run state."),
13494
+ created_at: record.updated_at
13495
+ };
13496
+ }
13497
+ function laneRejectionReport(input, record, reason, nextAction) {
13498
+ const handoffs = releaseHandoffs(input, record, laneGitOf(input));
13499
+ return laneReport({ ...record, batch_state: "rejected" }, reason, handoffs.length ? `${nextAction}. Check each release handoff with the Lane Steward.` : nextAction, { handoffs });
13500
+ }
13501
+ function finalizeLane(root, record, reason, nextAction, extra = {}) {
13502
+ const report = laneReport(record, reason, nextAction, extra);
13503
+ if (isTerminalBatchState(record.batch_state) || record.batch_state === "needs_human")
13504
+ writeBatchRunReport(root, report);
13505
+ return report;
13506
+ }
13507
+ function refuse2(input, persisted, code, detail) {
13508
+ const base = persisted && isLaneBatchRecord(persisted) ? persisted : prepareBatchLaneRunState({
13509
+ batch_id: input.batch_id,
13510
+ initiative_slug: input.initiative_slug,
13511
+ children: persisted ? [] : input.children,
13512
+ plan_digest: input.plan_digest,
13513
+ base_head: input.base_head,
13514
+ confirmation_time: input.confirmation_time,
13515
+ budget: input.budget,
13516
+ max_parallel: input.max_parallel ?? 1,
13517
+ now: input.now
13518
+ });
13519
+ return laneReport({ ...base, batch_state: "rejected" }, `${code}: ${detail}`, "Correct the lane-mode parameters and call start_unattended_batch again.");
13520
+ }
13521
+ function assertPlanMatches(input, record) {
13522
+ const plan = input.registry.children(input.capability);
13523
+ if (record.plan_digest !== computeBatchPlanDigest(plan) || record.children.length !== plan.length || record.children.some((child, index) => {
13524
+ const expected = plan[index];
13525
+ return child.task_id !== expected.task_id || JSON.stringify(child.blocked_by) !== JSON.stringify(expected.blocked_by);
13526
+ }))
13527
+ throw new Error("plan_digest mismatch: persisted children do not match the authorized plan");
13528
+ }
13529
+ function validateNewAuthorization(input) {
13530
+ try {
13531
+ const authorized = input.kernel.validateBatchAuthorization({
13532
+ registry: input.registry,
13533
+ capability: input.capability,
13534
+ binding: {
13535
+ batch_id: input.batch_id,
13536
+ plan_digest: input.plan_digest,
13537
+ base_head: input.base_head,
13538
+ initiative_slug: input.initiative_slug,
13539
+ budget: input.budget
13540
+ }
13541
+ });
13542
+ if (authorized.issued_at !== input.confirmation_time)
13543
+ return "the parked batch requires a fresh literal-user confirmation";
13544
+ const children = input.children.map((child) => {
13545
+ const { intent_path, intent_revision, intent_content_hash } = child;
13546
+ if (intent_path === null || intent_revision === null || intent_content_hash === null)
13547
+ throw new Error(`batch child ${child.task_id} has no complete intent identity`);
13548
+ return { ...child, intent_path, intent_revision, intent_content_hash };
13549
+ });
13550
+ if (computeBatchPlanDigest(children) !== authorized.plan_digest || input.plan_digest !== authorized.plan_digest || input.base_head !== authorized.base_head)
13551
+ return "batch run input does not match the authorized plan or base_head";
13552
+ return null;
13553
+ } catch (error) {
13554
+ return error instanceof Error ? error.message : String(error);
13555
+ }
13556
+ }
13557
+ function rehydrateConsumption(input, record) {
13558
+ const { issued_at: _issuedAt, ...binding } = input.kernel.validateBatchAuthorization({
13559
+ registry: input.registry,
13560
+ capability: input.capability,
13561
+ binding: {
13562
+ batch_id: record.batch_id,
13563
+ plan_digest: record.plan_digest,
13564
+ base_head: record.base_head,
13565
+ initiative_slug: record.initiative_slug,
13566
+ budget: record.budget
13567
+ }
13568
+ });
13569
+ for (const child of record.children) {
13570
+ if (child.state === "pending" || child.state === "skipped_blocked" || child.state === "lane_admitted")
13571
+ continue;
13572
+ if (!input.registry.isChildConsumed(input.capability, child.task_id))
13573
+ input.registry.consumeChild(input.capability, binding, child.task_id);
13574
+ }
13575
+ }
13576
+ function failLineage(root, record, message) {
13577
+ const next = {
13578
+ ...record,
13579
+ batch_state: "failed",
13580
+ children: record.children.map((child) => IN_FLIGHT.has(child.state) ? { ...child, state: "needs_human", reason: message } : child)
13581
+ };
13582
+ for (const child of next.children)
13583
+ if (child.state === "needs_human")
13584
+ skipDependents(next, child.task_id, `dependency ${child.task_id} parked`);
13585
+ return finalizeLane(root, writeBatchLaneRunState(root, next), message, "");
13586
+ }
13587
+ function park(record, taskId, reason) {
13588
+ record.children = record.children.map((child) => child.task_id === taskId ? { ...child, state: "needs_human", reason } : child);
13589
+ skipDependents(record, taskId, `dependency ${taskId} parked`);
13590
+ }
13591
+ function isLineageBreak(message) {
13592
+ return message.includes("batch_head_lineage_broken");
13593
+ }
13594
+ async function runLaneBatch(input, persisted) {
13595
+ if (persisted && !isLaneBatchRecord(persisted))
13596
+ return refuse2(input, persisted, PARALLEL_MISMATCH, "the recorded batch runs serially; max_parallel cannot be added on a resume");
13597
+ if (persisted && input.max_parallel !== undefined && input.max_parallel !== persisted.max_parallel)
13598
+ return refuse2(input, persisted, PARALLEL_MISMATCH, `the recorded batch runs with max_parallel ${persisted.max_parallel}`);
13599
+ const limit = persisted?.max_parallel ?? input.max_parallel ?? 1;
13600
+ if (!input.children.length)
13601
+ return refuse2(input, persisted, "batch_plan_empty", "batch plan is empty");
13602
+ const git = serialGitOf(input);
13603
+ const lanes = laneGitOf(input);
13604
+ let record;
13605
+ if (persisted) {
13606
+ record = persisted;
13607
+ assertPlanMatches(input, record);
13608
+ if (isTerminalBatchState(record.batch_state))
13609
+ return finalizeLane(input.root, record, `terminal state already reached: ${record.batch_state}`, "", {
13610
+ handoffs: record.batch_state === "completed" ? releaseHandoffs(input, record, lanes) : []
13611
+ });
13612
+ if (record.batch_state === "needs_human") {
13613
+ const invalid = validateNewAuthorization(input);
13614
+ if (invalid || Date.parse(input.confirmation_time) <= Date.parse(record.confirmation_time))
13615
+ return laneReport(record, invalid ?? "the parked batch requires a fresh literal-user confirmation", LANE_NEEDS_HUMAN_NEXT_ACTION);
13616
+ const resumed = [];
13617
+ for (const child of record.children) {
13618
+ if (child.state !== "needs_human") {
13619
+ resumed.push(child);
13620
+ continue;
13621
+ }
13622
+ if (!child.lane?.run_id)
13623
+ return laneReport(record, "parked Lane has no recorded run identity; inspect it without re-enrolling", LANE_NEEDS_HUMAN_NEXT_ACTION);
13624
+ const facts = lanes.inspectLane(input.root, child.lane.path);
13625
+ const fresh = await input.kernel.projectTask(child.lane.path, child.task_id);
13626
+ if (!facts.exists || !facts.same_repository || !facts.is_worktree_root || facts.branch !== child.lane.branch || fresh.error !== null || fresh.projection.run_id !== child.lane.run_id || fresh.projection.open_user_decision_count > 0 || fresh.projection.replan_required_ids.length > 0)
13627
+ return laneReport(record, "parked Lane identity or decision is unresolved", LANE_NEEDS_HUMAN_NEXT_ACTION);
13628
+ const state = fresh.projection.lifecycle === "done" && fresh.projection.completion_ready ? child.lane.lane_commit ? "lane_committed" : "settled" : fresh.projection.lifecycle === "active" && fresh.claim?.task_id === child.task_id && (facts.active_claim_task_id === null || facts.active_claim_task_id === child.task_id) ? "enrolled" : null;
13629
+ if (!state)
13630
+ return laneReport(record, "parked child has no recoverable owned run", LANE_NEEDS_HUMAN_NEXT_ACTION);
13631
+ resumed.push({ ...child, state, reason: null, qa_failures: 0 });
13632
+ }
13633
+ record = {
13634
+ ...record,
13635
+ batch_state: "running",
13636
+ confirmation_time: input.confirmation_time,
13637
+ children: resumed.map((child) => child.state === "skipped_blocked" ? { ...child, state: "pending", reason: null } : child)
13638
+ };
13639
+ }
13640
+ } else {
13641
+ const invalid = validateNewAuthorization(input);
13642
+ if (invalid)
13643
+ return refuse2(input, null, "batch_authorization_invalid", invalid);
13644
+ const preflight = await git.preflight({
13645
+ root: input.root,
13646
+ initiative_slug: input.initiative_slug,
13647
+ base_head: input.base_head
13648
+ });
13649
+ if (!preflight.ok)
13650
+ return refuse2(input, null, preflight.reason, preflight.message || "Correct the preflight condition and re-confirm.");
13651
+ record = prepareBatchLaneRunState({
13652
+ batch_id: input.batch_id,
13653
+ initiative_slug: input.initiative_slug,
13654
+ children: input.children,
13655
+ plan_digest: input.plan_digest,
13656
+ base_head: input.base_head,
13657
+ confirmation_time: input.confirmation_time,
13658
+ budget: input.budget,
13659
+ max_parallel: limit,
13660
+ now: input.now
13661
+ });
13662
+ }
13663
+ const persist = () => {
13664
+ record = writeBatchLaneRunState(input.root, record);
13665
+ };
13666
+ const resumedPark = persisted?.batch_state === "needs_human" && record.batch_state === "running";
13667
+ if (record.batch_state === "prepared") {
13668
+ record.batch_state = "running";
13669
+ persist();
13670
+ }
13671
+ for (const child of record.children) {
13672
+ if (child.state !== "lane_committed" || !child.lane?.lane_commit)
13673
+ continue;
13674
+ const found = lanes.findIntegrated({
13675
+ root: input.root,
13676
+ task_id: child.task_id,
13677
+ batch_id: record.batch_id,
13678
+ from_head: expectedBatchHead(record),
13679
+ lane_base: child.lane.base_head,
13680
+ lane_commit: child.lane.lane_commit
13681
+ });
13682
+ if (!found)
13683
+ continue;
13684
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "integrated", commit: found } : c);
13685
+ record.commits = [...record.commits, found];
13686
+ persist();
13687
+ }
13688
+ for (const child of record.children) {
13689
+ if (child.state !== "integrated" || !child.lane)
13690
+ continue;
13691
+ if (lanes.inspectLane(input.root, child.lane.path).exists)
13692
+ continue;
13693
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "released" } : c);
13694
+ persist();
13695
+ }
13696
+ const lineage = existsSync10(join15(input.root, ".git")) ? classifyBatchLineage({
13697
+ root: input.root,
13698
+ branch: record.branch ?? "",
13699
+ expectedHead: expectedBatchHead(record),
13700
+ childCommits: record.commits,
13701
+ batchId: record.batch_id
13702
+ }) : { kind: "equal", head: expectedBatchHead(record) };
13703
+ if (lineage.kind === "broken")
13704
+ return failLineage(input.root, record, lineage.message);
13705
+ if (lineage.kind === "fast_forward") {
13706
+ record = writeBatchLaneRunState(input.root, {
13707
+ ...record,
13708
+ adopted_heads: [...record.adopted_heads ?? [], { from: expectedBatchHead(record), to: lineage.head }]
13709
+ });
13710
+ }
13711
+ try {
13712
+ rehydrateConsumption(input, record);
13713
+ } catch (error) {
13714
+ return failLineage(input.root, record, error instanceof Error ? error.message : String(error));
13715
+ }
13716
+ if (resumedPark)
13717
+ persist();
13718
+ const refusals = [];
13719
+ let parkedMessage = null;
13720
+ const reviewOpen = [];
13721
+ const parkChild = (taskId, reason) => {
13722
+ park(record, taskId, reason);
13723
+ parkedMessage ??= reason;
13724
+ persist();
13725
+ };
13726
+ const enrollInLane = async (taskId) => {
13727
+ const lane = record.children.find((c) => c.task_id === taskId).lane;
13728
+ try {
13729
+ await input.kernel.enrollTask({
13730
+ root: lane.path,
13731
+ task_id: taskId,
13732
+ batch: { registry: input.registry, capability: input.capability, binding: { batch_id: record.batch_id, expected_head: lane.base_head } }
13733
+ });
13734
+ const fresh = await input.kernel.projectTask(lane.path, taskId);
13735
+ if (fresh.error !== null || !fresh.projection.run_id || fresh.claim?.task_id !== taskId)
13736
+ throw new Error("enrolled Lane has no observable owned run");
13737
+ record.children = record.children.map((c) => c.task_id === taskId ? { ...c, state: "enrolled", reason: null, lane: { ...lane, run_id: fresh.projection.run_id } } : c);
13738
+ persist();
13739
+ return null;
13740
+ } catch (error) {
13741
+ const message = error instanceof Error ? error.message : String(error);
13742
+ parkChild(taskId, message);
13743
+ if (!isLineageBreak(message))
13744
+ return null;
13745
+ record.batch_state = "failed";
13746
+ persist();
13747
+ return finalizeLane(input.root, record, message, "", { refusals });
13748
+ }
13749
+ };
13750
+ for (const child of [...record.children]) {
13751
+ if (child.state !== "lane_admitted" && child.state !== "enrolled" || !child.lane)
13752
+ continue;
13753
+ let fresh;
13754
+ try {
13755
+ fresh = await input.kernel.projectTask(child.lane.path, child.task_id);
13756
+ } catch {
13757
+ fresh = { error: "unreadable" };
13758
+ }
13759
+ if (fresh.error !== null) {
13760
+ parkChild(child.task_id, "batch_lane_lost");
13761
+ continue;
13762
+ }
13763
+ if (child.lane.run_id && fresh.projection.run_id !== child.lane.run_id) {
13764
+ parkChild(child.task_id, "batch_lane_run_mismatch");
13765
+ continue;
13766
+ }
13767
+ const holdsClaim = fresh.claim !== null && fresh.claim.task_id === child.task_id;
13768
+ if (child.state === "lane_admitted") {
13769
+ if (holdsClaim) {
13770
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "enrolled", reason: "adopted existing lane claim after interruption", lane: { ...child.lane, run_id: fresh.projection.run_id } } : c);
13771
+ persist();
13772
+ } else {
13773
+ const failed = await enrollInLane(child.task_id);
13774
+ if (failed)
13775
+ return failed;
13776
+ }
13777
+ continue;
13778
+ }
13779
+ if (fresh.projection.lifecycle === "done" && fresh.projection.completion_ready) {
13780
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "settled", reason: null } : c);
13781
+ persist();
13782
+ continue;
13783
+ }
13784
+ if (!holdsClaim) {
13785
+ parkChild(child.task_id, "batch_lane_lost");
13786
+ continue;
13787
+ }
13788
+ if (!input.kernel.ownsTaskClaim(child.task_id)) {
13789
+ parkChild(child.task_id, "claim held by another batch");
13790
+ continue;
13791
+ }
13792
+ if (fresh.projection.next_obligation === "run_qa" || fresh.projection.artifact_state !== "frozen")
13793
+ continue;
13794
+ const terminal = await input.kernel.advanceTask(child.lane.path, child.task_id);
13795
+ if (terminal.state === "completed") {
13796
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "settled", reason: null, qa_failures: 0 } : c);
13797
+ persist();
13798
+ } else if (terminal.state === "review_ready") {
13799
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, reason: `review reservation ${terminal.operation_id} open` } : c);
13800
+ persist();
13801
+ reviewOpen.push(child.task_id);
13802
+ } else if (terminal.state === "rework" && terminal.operation === "qa") {
13803
+ const failures = child.qa_failures + 1;
13804
+ if (failures >= record.budget.qa_failure_limit) {
13805
+ const reason = "QA failure limit reached";
13806
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason, qa_failures: failures } : c);
13807
+ skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
13808
+ parkedMessage ??= reason;
13809
+ persist();
13810
+ continue;
13811
+ }
13812
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, qa_failures: failures } : c);
13813
+ persist();
13814
+ } else if (terminal.state === "rework" || terminal.environment_failure || terminal.state === "review_preparation_failed" || terminal.recovery?.category === "repair") {
13815
+ continue;
13816
+ } else {
13817
+ const reason = terminal.state === "stopped" ? "Kernel reported the child stopped" : terminal.reason;
13818
+ parkChild(child.task_id, reason);
13819
+ }
13820
+ }
13821
+ for (const child of [...record.children]) {
13822
+ if (child.state !== "settled" || !child.lane)
13823
+ continue;
13824
+ const planChild = input.children.find((c) => c.task_id === child.task_id);
13825
+ let laneCommit;
13826
+ try {
13827
+ const existing = await git.lookupBatchCommit(child.lane.path, child.task_id, record.batch_id, child.lane.base_head, child.lane.branch);
13828
+ 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;
13829
+ } catch (error) {
13830
+ const message = error instanceof Error ? error.message : String(error);
13831
+ parkChild(child.task_id, message);
13832
+ if (!isLineageBreak(message))
13833
+ continue;
13834
+ record.batch_state = "failed";
13835
+ persist();
13836
+ return finalizeLane(input.root, record, message, "");
13837
+ }
13838
+ 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);
13839
+ persist();
13840
+ }
13841
+ for (const child of [...record.children]) {
13842
+ if (child.state !== "lane_committed" || !child.lane?.lane_commit)
13843
+ continue;
13844
+ const intentPathOf = (taskId) => input.children.find((c) => c.task_id === taskId)?.intent_path ?? null;
13845
+ try {
13846
+ const { commit } = await lanes.integrate({
13847
+ root: input.root,
13848
+ branch: record.branch ?? "",
13849
+ batch_head: expectedBatchHead(record),
13850
+ lane_base: child.lane.base_head,
13851
+ lane_commit: child.lane.lane_commit,
13852
+ child: { task_id: child.task_id, intent_path: intentPathOf(child.task_id) },
13853
+ 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 }))
13854
+ });
13855
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "integrated", commit, reason: null } : c);
13856
+ record.commits = [...record.commits, commit];
13857
+ persist();
13858
+ } catch (error) {
13859
+ const message = error instanceof Error ? error.message : String(error);
13860
+ const reason = error instanceof BatchIntegrationError ? error.reason : "batch_integration_conflict";
13861
+ parkChild(child.task_id, isLineageBreak(message) || message.startsWith(`${reason}:`) ? message : `${reason}: ${message}`);
13862
+ if (isLineageBreak(message)) {
13863
+ record.batch_state = "failed";
13864
+ persist();
13865
+ return finalizeLane(input.root, record, message, "");
13866
+ }
13867
+ }
13868
+ }
13869
+ const coordinatorReal = lanes.resolveRoot(input.root);
13870
+ for (const offer of input.lane_offers ?? []) {
13871
+ const startable = new Set(startableChildren(scheduleView(input, record), limit));
13872
+ const child = record.children.find((c) => c.task_id === offer.task_id);
13873
+ if (!child || child.state !== "pending" || !startable.has(child.task_id)) {
13874
+ refusals.push({ ...offer, reason: "batch_lane_unknown_child" });
13875
+ continue;
13876
+ }
13877
+ const head = expectedBatchHead(record);
13878
+ const facts = lanes.inspectLane(input.root, offer.path);
13879
+ const reason = decideLaneAdmission({
13880
+ facts,
13881
+ coordinator_real_path: coordinatorReal,
13882
+ expected_branch: laneBranchName(record.initiative_slug, child.task_id),
13883
+ batch_head: head,
13884
+ bound_paths: record.children.flatMap((c) => c.lane && c.state !== "released" ? [c.lane.path] : [])
13885
+ });
13886
+ if (reason) {
13887
+ refusals.push({ ...offer, reason });
13888
+ continue;
13889
+ }
13890
+ if (consumedSlots(record) >= record.budget.max_children)
13891
+ break;
13892
+ const lane = { path: facts.real_path, branch: laneBranchName(record.initiative_slug, child.task_id), base_head: head, lane_commit: null };
13893
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "lane_admitted", lane } : c);
13894
+ persist();
13895
+ const failed = await enrollInLane(child.task_id);
13896
+ if (failed)
13897
+ return failed;
13898
+ }
13899
+ if (record.children.every((c) => c.state === "integrated" || c.state === "released")) {
13900
+ record.batch_state = "completed";
13901
+ persist();
13902
+ return finalizeLane(input.root, record, "all enrollable children integrated", "", {
13903
+ handoffs: releaseHandoffs(input, record, lanes),
13904
+ refusals
13905
+ });
13906
+ }
13907
+ const handoffs = [];
13908
+ for (const child of record.children) {
13909
+ if (child.state !== "enrolled" || !child.lane)
13910
+ continue;
13911
+ const fresh = await input.kernel.projectTask(child.lane.path, child.task_id);
13912
+ if (fresh.error !== null)
13913
+ continue;
13914
+ handoffs.push({
13915
+ role: "executor",
13916
+ task_id: child.task_id,
13917
+ run_id: fresh.projection.run_id ?? null,
13918
+ record_revision: fresh.projection.record_revision,
13919
+ next_obligation: fresh.projection.next_obligation,
13920
+ lane_branch: child.lane.branch,
13921
+ lane_path: child.lane.path
13922
+ });
13923
+ }
13924
+ handoffs.push(...releaseHandoffs(input, record, lanes));
13925
+ const inFlight = record.children.filter((c) => IN_FLIGHT.has(c.state));
13926
+ const startable = startableChildren(scheduleView(input, record), limit);
13927
+ const overBudget = consumedSlots(record) >= record.budget.max_children;
13928
+ if (!inFlight.length && (!startable.length || overBudget)) {
13929
+ const parked = record.children.some((c) => c.state === "needs_human" || c.state === "skipped_blocked");
13930
+ record.batch_state = parked ? "needs_human" : overBudget ? "budget_stopped" : "needs_human";
13931
+ persist();
13932
+ 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 });
13933
+ }
13934
+ if (!overBudget) {
13935
+ for (const taskId of startable) {
13936
+ handoffs.push({
13937
+ role: "lane-steward",
13938
+ action: "provision",
13939
+ task_id: taskId,
13940
+ lane_branch: laneBranchName(record.initiative_slug, taskId),
13941
+ base_head: expectedBatchHead(record),
13942
+ executor_hosts: LANE_EXECUTOR_HOSTS
13943
+ });
13944
+ }
13945
+ }
13946
+ if (reviewOpen.length > 0) {
13947
+ 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 });
13948
+ }
13949
+ 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 });
13950
+ }
13951
+
13952
+ // plugins/immune-brain/runtime/unattended/batch_runner.ts
13953
+ function batchQaFailureFacts(record) {
13954
+ const failed = record.history.filter((event) => event.type === "request_rework" && event.authority?.authority_kind === "qa");
13955
+ return {
13956
+ qa_failure_count: failed.length,
13957
+ last_qa_failure_at: failed.at(-1)?.at ?? null,
13958
+ recovery_findings: record.findings.filter((f) => f.status === "open").map((f) => ({ id: f.id, kind: f.kind, status: f.status, acceptance_id: f.acceptance_id }))
13959
+ };
13960
+ }
13961
+ function batchGitPortOf(input) {
13962
+ return input.git ?? createDefaultBatchGitPort();
13963
+ }
13964
+ function dependentsOf2(record, taskId) {
13965
+ return record.children.filter((child) => child.blocked_by.includes(taskId));
13966
+ }
13967
+ function skipDependents2(record, taskId, reason) {
13968
+ const skip = new Set([taskId]);
13969
+ const queue = [taskId];
13970
+ while (queue.length > 0) {
13971
+ const current = queue.shift();
13972
+ for (const dependent of dependentsOf2(record, current)) {
13973
+ if (skip.has(dependent.task_id))
13974
+ continue;
13975
+ skip.add(dependent.task_id);
13976
+ queue.push(dependent.task_id);
13977
+ }
13978
+ }
13979
+ record.children = record.children.map((child) => skip.has(child.task_id) && child.state === "pending" ? { ...child, state: "skipped_blocked", reason } : child);
13980
+ }
13981
+ function budgetStopReason(record) {
13982
+ const enrolledCount = record.children.filter((child) => child.state !== "pending" && child.state !== "skipped_blocked").length;
13983
+ if (enrolledCount >= record.budget.max_children)
13984
+ return `max_children budget exhausted (${record.budget.max_children})`;
13985
+ return null;
12555
13986
  }
12556
13987
  function requireFreshProjection(result, taskId) {
12557
13988
  if (result.error !== null)
@@ -12561,11 +13992,12 @@ function requireFreshProjection(result, taskId) {
12561
13992
  function nextEnrollableChild(record) {
12562
13993
  return record.children.find((child) => child.state === "pending" && record.children.every((other) => !child.blocked_by.includes(other.task_id) || other.state === "committed")) ?? null;
12563
13994
  }
12564
- var TERMINAL_NEXT_ACTIONS = {
13995
+ var TERMINAL_NEXT_ACTIONS2 = {
12565
13996
  completed: "The batch settled every enrollable child; review the commits and the tracker.",
12566
13997
  budget_stopped: "The child budget stopped new enrollments; re-confirm to continue under a new authorization.",
12567
13998
  failed: "A commit or lineage failure stopped the batch; inspect the failing child and the branch state.",
12568
13999
  rejected: "The batch was rejected before any enrollment; correct the stated reason and re-confirm.",
14000
+ superseded: "The plan moved past this record and an explicit disposition retired it; it grants no handoff and no child delivery.",
12569
14001
  needs_human: "A parked child needs a human decision; resolve it, then re-confirm to continue.",
12570
14002
  running: "The batch is still running; no terminal report is due yet.",
12571
14003
  prepared: "The batch is prepared but not started."
@@ -12579,7 +14011,7 @@ function reportFor(record, reason, nextAction) {
12579
14011
  children: record.children,
12580
14012
  commits: record.commits,
12581
14013
  reason,
12582
- next_action: nextAction || (TERMINAL_NEXT_ACTIONS[record.batch_state] ?? "Inspect the batch run state."),
14014
+ next_action: nextAction || (TERMINAL_NEXT_ACTIONS2[record.batch_state] ?? "Inspect the batch run state."),
12583
14015
  created_at: record.updated_at
12584
14016
  };
12585
14017
  }
@@ -12613,13 +14045,13 @@ function failPersistedLineage(root, existing, message) {
12613
14045
  const record = { ...existing, batch_state: "failed", children };
12614
14046
  for (const child of record.children) {
12615
14047
  if (child.state === "needs_human") {
12616
- skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
14048
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
12617
14049
  }
12618
14050
  }
12619
14051
  return writeBatchRunState(root, record);
12620
14052
  }
12621
14053
  function reconcileLineage(root, record) {
12622
- if (!existsSync10(join15(root, ".git")))
14054
+ if (!existsSync11(join16(root, ".git")))
12623
14055
  return { record, failure: null };
12624
14056
  const expected = expectedBatchHead(record);
12625
14057
  const lineage = classifyBatchLineage({
@@ -12642,8 +14074,8 @@ function reconcileLineage(root, record) {
12642
14074
  };
12643
14075
  }
12644
14076
  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" });
14077
+ if (!record.commits.length && record.children[0]?.state === "settled" && existsSync11(join16(input.root, ".git"))) {
14078
+ const head = spawnSync11("git", ["-C", input.root, "rev-parse", "HEAD"], { encoding: "utf8" });
12647
14079
  const live = head.stdout.trim();
12648
14080
  const expected = expectedBatchHead(record);
12649
14081
  const adoptable = () => classifyBatchLineage({
@@ -12672,8 +14104,8 @@ async function validatePersistedRun(input, record) {
12672
14104
  const git = batchGitPortOf(input);
12673
14105
  const evidence = await git.lookupBatchCommit(input.root, child.task_id, record.batch_id, undefined, record.branch);
12674
14106
  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"] });
14107
+ if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync11(join16(input.root, ".git"))) {
14108
+ const reach = spawnSync11("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
12677
14109
  if (reach.status !== 0) {
12678
14110
  throw new Error(`batch_head_lineage_broken: recorded commit ${child.commit} for ${child.task_id} is no longer reachable from HEAD`);
12679
14111
  }
@@ -12758,17 +14190,22 @@ async function startBatch(input) {
12758
14190
  function rejectionReport(input, reason) {
12759
14191
  const persisted = (() => {
12760
14192
  try {
12761
- return readBatchRunState(input.root, input.batch_id);
14193
+ return readAnyBatchRunState(input.root, input.batch_id);
12762
14194
  } catch {
12763
14195
  return null;
12764
14196
  }
12765
14197
  })();
14198
+ if (persisted !== null && isLaneBatchRecord(persisted))
14199
+ return laneRejectionReport(input, persisted, reason, "settle the reported kernel store condition and retry in the current Host");
12766
14200
  return reportFor({
12767
14201
  ...persisted ?? prepareBatchRunState({ ...input, children: [], now: input.now }),
12768
14202
  batch_state: "rejected"
12769
14203
  }, reason, "settle the reported kernel store condition and retry in the current Host");
12770
14204
  }
12771
14205
  async function startBatchLocked(input) {
14206
+ const recorded = readAnyBatchRunState(input.root, input.batch_id);
14207
+ if (input.max_parallel !== undefined || recorded !== null && isLaneBatchRecord(recorded))
14208
+ return await runLaneBatch(input, recorded);
12772
14209
  if (!input.children.length) {
12773
14210
  const rejected = prepareBatchRunState({ ...input, children: [], now: input.now });
12774
14211
  return reportFor({ ...rejected, batch_state: "rejected" }, "batch plan is empty", "Provide a non-empty enrollable child plan.");
@@ -12818,7 +14255,7 @@ async function startBatchLocked(input) {
12818
14255
  const reparked = { ...existing, children: remapped };
12819
14256
  for (const parked of remapped) {
12820
14257
  if (parked.state === "needs_human")
12821
- skipDependents(reparked, parked.task_id, `dependency ${parked.task_id} parked`);
14258
+ skipDependents2(reparked, parked.task_id, `dependency ${parked.task_id} parked`);
12822
14259
  }
12823
14260
  existing = writeBatchRunState(input.root, {
12824
14261
  ...reparked,
@@ -12901,7 +14338,7 @@ async function startBatchLocked(input) {
12901
14338
  adoptedClaim = true;
12902
14339
  } else if (claimState === "foreign") {
12903
14340
  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`);
14341
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
12905
14342
  record.batch_state = "needs_human";
12906
14343
  persist();
12907
14344
  return finalize(input.root, record, "claim held by another batch", "needs-human-attention");
@@ -12947,13 +14384,13 @@ async function startBatchLocked(input) {
12947
14384
  const message = error instanceof Error ? error.message : String(error);
12948
14385
  if (isLineageBreakError(message)) {
12949
14386
  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`);
14387
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
12951
14388
  record.batch_state = "failed";
12952
14389
  persist();
12953
14390
  break;
12954
14391
  }
12955
14392
  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`);
14393
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
12957
14394
  record.batch_state = "needs_human";
12958
14395
  persist();
12959
14396
  break;
@@ -13011,7 +14448,7 @@ async function resumeBatch(input, projection) {
13011
14448
  const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, driven.task_id), driven.task_id);
13012
14449
  const holdsClaim = fresh.claim !== null && fresh.claim.task_id === driven.task_id && input.kernel.ownsTaskClaim(driven.task_id);
13013
14450
  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`);
14451
+ skipDependents2(existing, driven.task_id, `dependency ${driven.task_id} parked`);
13015
14452
  existing = writeBatchRunState(input.root, {
13016
14453
  ...existing,
13017
14454
  batch_state: "needs_human",
@@ -13023,7 +14460,7 @@ async function resumeBatch(input, projection) {
13023
14460
  const failures = Math.max(existing.consecutive_qa_failures, fresh.qa_failure_count ?? 0);
13024
14461
  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
14462
  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`);
14463
+ skipDependents2(existing, driven.task_id, `dependency ${driven.task_id} parked`);
13027
14464
  existing = writeBatchRunState(input.root, {
13028
14465
  ...existing,
13029
14466
  batch_state: "needs_human",
@@ -13094,7 +14531,7 @@ async function driveInterruptedChild(input, child) {
13094
14531
  } catch (error) {
13095
14532
  const message = error instanceof Error ? error.message : String(error);
13096
14533
  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`);
14534
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} failed to commit`);
13098
14535
  const isLineageError = message.includes("batch_head_lineage_broken") || message.includes("lineage");
13099
14536
  record.batch_state = isLineageError ? "failed" : "needs_human";
13100
14537
  persist();
@@ -13107,12 +14544,12 @@ async function driveInterruptedChild(input, child) {
13107
14544
  const planChild = input.children.find((c) => c.task_id === child.task_id);
13108
14545
  const intentPath = planChild?.intent_path ?? undefined;
13109
14546
  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))
14547
+ if (existing && !record.commits.length && existsSync11(join16(input.root, ".git")) && !ownUnpersistedBatchHead(input.root, record, existing.commit))
13111
14548
  throw new Error("first unpersisted batch commit provenance is invalid");
13112
14549
  const adopted = existing ?? await doCommit().catch((error) => {
13113
14550
  const message = error instanceof Error ? error.message : String(error);
13114
14551
  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`);
14552
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} failed to commit`);
13116
14553
  record.batch_state = "failed";
13117
14554
  persist();
13118
14555
  throw new BatchCommitAbortError(message);
@@ -13142,7 +14579,7 @@ async function driveInterruptedChild(input, child) {
13142
14579
  const reason = "QA failure limit reached";
13143
14580
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
13144
14581
  record.batch_state = "needs_human";
13145
- skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
14582
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
13146
14583
  persist();
13147
14584
  const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
13148
14585
  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 +14594,7 @@ async function driveInterruptedChild(input, child) {
13157
14594
  const reason = terminal.state === "stopped" ? "Kernel reported the child stopped" : terminal.reason;
13158
14595
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
13159
14596
  record.batch_state = "needs_human";
13160
- skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
14597
+ skipDependents2(record, child.task_id, `dependency ${child.task_id} parked`);
13161
14598
  persist();
13162
14599
  return finalize(input.root, record, reason, "");
13163
14600
  }
@@ -13220,22 +14657,126 @@ function createBatchKernelPort(input) {
13220
14657
  };
13221
14658
  }
13222
14659
 
13223
- // plugins/immune-brain/runtime/claude/kernel_ports.ts
13224
- function diffSnapshotOf(root, record) {
13225
- return taskDeliveryIdentity(root, record);
14660
+ // plugins/immune-brain/runtime/unattended/batch_disposition.ts
14661
+ var SERIAL_IN_FLIGHT = new Set(["enrolled", "settled"]);
14662
+ var LANE_IN_FLIGHT = new Set(["lane_admitted", "enrolled", "settled", "lane_committed"]);
14663
+ var RETIRE_REASON = "Retired by an explicit literal-user disposition: the batch plan moved past this record, so its authorization could neither reconfirm nor continue. " + "Recorded child commits, child states and the batch branch are preserved as evidence. " + "Children this batch never executed carry no commit, no batch trailer and no approval; nothing infers their delivery from this record.";
14664
+ var RETIRE_NEXT_ACTION = "The Initiative has no active batch. Run each remaining child with imm-run under its own authorization, or start a new batch for this Initiative after checking out its batch branch.";
14665
+ function childEvidenceOf(record) {
14666
+ if (record.contract === "assurance_kernel/batch_run_state/v2")
14667
+ return record.children.map((child) => ({
14668
+ task_id: child.task_id,
14669
+ slice_id: child.slice_id,
14670
+ state: child.state,
14671
+ commit: child.commit,
14672
+ lane_branch: child.lane?.branch ?? null
14673
+ }));
14674
+ return record.children.map((child) => ({
14675
+ task_id: child.task_id,
14676
+ slice_id: child.slice_id,
14677
+ state: child.state,
14678
+ commit: child.commit,
14679
+ lane_branch: null
14680
+ }));
13226
14681
  }
13227
- function diffHashOf(root, record) {
13228
- return diffSnapshotOf(root, record).diff_hash;
14682
+ function inFlightChildOf(record) {
14683
+ const inFlight = record.contract === "assurance_kernel/batch_run_state/v2" ? LANE_IN_FLIGHT : SERIAL_IN_FLIGHT;
14684
+ return record.children.find((child) => inFlight.has(child.state)) ?? null;
13229
14685
  }
13230
- function readTaskIntentForRecord(root, taskId) {
13231
- recoverKernelStoreFollowUps(root, taskId);
13232
- const currentPath = readTaskRecordRaw(root, taskId).record?.intent_ref?.path;
13233
- return readTaskIntent(root, taskId, currentPath);
14686
+ function readStaleBatchLookup(root, initiativeSlug) {
14687
+ const found = findExistingActiveBatch(root, initiativeSlug);
14688
+ if (found === null) {
14689
+ const settled = findSettledBatchRecord(root, initiativeSlug);
14690
+ if (settled)
14691
+ return { kind: "terminal", batch_id: settled.batch_id };
14692
+ return { kind: "none" };
14693
+ }
14694
+ if (found.corrupt)
14695
+ return { kind: "corrupt", path: found.path };
14696
+ const record = found.record;
14697
+ if (isTerminalBatchState(record.batch_state))
14698
+ return { kind: "terminal", batch_id: record.batch_id };
14699
+ const inFlight = inFlightChildOf(record);
14700
+ if (inFlight)
14701
+ return { kind: "in_flight", batch_id: record.batch_id, task_id: inFlight.task_id };
14702
+ return {
14703
+ kind: "retirable",
14704
+ record,
14705
+ facts: {
14706
+ batch_id: record.batch_id,
14707
+ initiative_slug: record.initiative_slug,
14708
+ batch_state: record.batch_state,
14709
+ plan_digest: record.plan_digest,
14710
+ branch: record.branch ?? `imm/${record.initiative_slug}`,
14711
+ base_head: record.base_head,
14712
+ confirmation_time: record.confirmation_time,
14713
+ recorded_commits: [...record.commits],
14714
+ children: childEvidenceOf(record)
14715
+ }
14716
+ };
14717
+ }
14718
+ async function retireStaleBatch(options) {
14719
+ const { root, initiative_slug: initiativeSlug, now } = options;
14720
+ const lookup = readStaleBatchLookup(root, initiativeSlug);
14721
+ if (lookup.kind === "none")
14722
+ return { outcome: "rejected", rejection: batchReason("stale_batch_absent", `Initiative ${initiativeSlug} has no active batch`) };
14723
+ if (lookup.kind === "corrupt")
14724
+ return { outcome: "rejected", rejection: batchReason("batch_state_unreadable", lookup.path) };
14725
+ if (lookup.kind === "terminal")
14726
+ return {
14727
+ outcome: "rejected",
14728
+ rejection: batchReason("stale_batch_absent", `batch ${lookup.batch_id} already reached a terminal state`)
14729
+ };
14730
+ if (lookup.kind === "in_flight")
14731
+ return {
14732
+ outcome: "rejected",
14733
+ rejection: batchReason("stale_batch_in_flight", `batch ${lookup.batch_id}, child ${lookup.task_id}`)
14734
+ };
14735
+ const { record, facts } = lookup;
14736
+ const expected = readSecureProjectBytes(root, batchStatePath(record.batch_id));
14737
+ const decision = await options.gate(facts);
14738
+ if (decision.kind === "host_rejection")
14739
+ return { outcome: "host_rejection", value: decision.value };
14740
+ if (!readSecureProjectBytes(root, batchStatePath(record.batch_id)).equals(expected))
14741
+ return { outcome: "rejected", rejection: batchReason("plan_changed", record.batch_id) };
14742
+ const retired = { ...record, batch_state: "superseded" };
14743
+ const stored = replaceAnyBatchRunState(root, expected, retired, () => {
14744
+ if (!readSecureProjectBytes(root, batchStatePath(record.batch_id)).equals(expected))
14745
+ throw new Error("batch state changed before the disposition was applied");
14746
+ });
14747
+ const report = stored.contract === "assurance_kernel/batch_run_state/v2" ? writeBatchRunReport(root, {
14748
+ contract: "assurance_kernel/batch_run_report/v1",
14749
+ batch_id: stored.batch_id,
14750
+ initiative_slug: stored.initiative_slug,
14751
+ batch_state: stored.batch_state,
14752
+ max_parallel: stored.max_parallel,
14753
+ children: stored.children,
14754
+ commits: stored.commits,
14755
+ reason: RETIRE_REASON,
14756
+ handoffs: [],
14757
+ next_action: RETIRE_NEXT_ACTION,
14758
+ created_at: now
14759
+ }) : writeBatchRunReport(root, {
14760
+ contract: "assurance_kernel/batch_run_report/v1",
14761
+ batch_id: stored.batch_id,
14762
+ initiative_slug: stored.initiative_slug,
14763
+ batch_state: stored.batch_state,
14764
+ children: stored.children,
14765
+ commits: stored.commits,
14766
+ reason: RETIRE_REASON,
14767
+ next_action: RETIRE_NEXT_ACTION,
14768
+ created_at: now
14769
+ });
14770
+ return { outcome: "retired", batch_id: stored.batch_id, record: stored, report };
13234
14771
  }
14772
+
14773
+ // plugins/immune-brain/runtime/assurance/review_mediation.ts
14774
+ import { createHash as createHash21 } from "node:crypto";
13235
14775
  function extractVerdictJson(input) {
13236
14776
  if (typeof input === "string") {
13237
- const cleaned = input.split(`
13238
- `).map((line) => line.trim()).filter((line) => line.startsWith("{") && line.endsWith("}")).join("");
14777
+ const first = input.indexOf("{");
14778
+ const last = input.lastIndexOf("}");
14779
+ const cleaned = first >= 0 && last >= first ? input.slice(first, last + 1) : "";
13239
14780
  if (!cleaned)
13240
14781
  return null;
13241
14782
  try {
@@ -13259,36 +14800,72 @@ function verdictFingerprint(raw) {
13259
14800
  findings: raw.findings ?? null
13260
14801
  });
13261
14802
  }
14803
+ function digestOfReviewerBytes(bytes) {
14804
+ return `sha256:${createHash21("sha256").update(bytes).digest("hex")}`;
14805
+ }
13262
14806
  var RELEASED_REVIEW_RECOVERY = "Call advance_assurance to obtain a new Review reservation, then dispatch one fresh reviewer with the returned envelope unchanged";
13263
14807
  var RETAINED_REVIEW_RECOVERY = "Wait for the dispatched reviewer to finish, then call submit_review again with its verdict; do not dispatch or continue another reviewer";
13264
- var MISMATCH_REVIEW_RECOVERY = "Resubmit the reviewer's verdict exactly as the reviewer returned it";
14808
+ var MISMATCH_REVIEW_RECOVERY = "Resubmit without a verdict to apply the observed reviewer receipt, or resubmit the reviewer's verdict exactly as the reviewer returned it";
13265
14809
  function withReviewRecovery(result, recovery_action) {
13266
14810
  if (result.state !== "blocked" || result.code === "verdict_invalid")
13267
14811
  return result;
13268
14812
  return { ...result, recovery_action };
13269
14813
  }
13270
- async function submitClaudeReview(host, coordinator, ctx, taskId, verdictInput) {
13271
- if (verdictInput === undefined)
13272
- throw new Error("verdict is required");
13273
- const observed = host.inspectReviewForTask(taskId);
14814
+ async function submitMediatedReview(coordinator, ctx, taskId, verdictInput, inspect) {
14815
+ if (coordinator.active(taskId)?.state === "settlement_unknown") {
14816
+ return coordinator.submitReview(taskId, ctx, verdictInput);
14817
+ }
14818
+ const observed = inspect();
13274
14819
  if (!observed.ok) {
13275
14820
  if (observed.release)
13276
14821
  return withReviewRecovery(coordinator.abandonReview(taskId, observed.reason), RELEASED_REVIEW_RECOVERY);
13277
14822
  return { state: "blocked", reason: observed.reason, recovery_action: RETAINED_REVIEW_RECOVERY };
13278
14823
  }
14824
+ const receiptBytes = observed.receipt.result;
14825
+ const options = {
14826
+ receiptRecoveryAction: RETAINED_REVIEW_RECOVERY,
14827
+ reviewer_verdict_sha256: digestOfReviewerBytes(receiptBytes),
14828
+ validateReceipt: () => {
14829
+ const current = inspect();
14830
+ if (!current.ok)
14831
+ return current.reason;
14832
+ return current.receipt.actorId === observed.receipt.actorId && current.receipt.result === receiptBytes ? null : "reviewer receipt changed during submission";
14833
+ }
14834
+ };
14835
+ if (verdictInput === undefined) {
14836
+ if (!coordinator.isReviewVerdictValid(taskId, receiptBytes)) {
14837
+ return withReviewRecovery(coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict"), RELEASED_REVIEW_RECOVERY);
14838
+ }
14839
+ return coordinator.submitReview(taskId, ctx, receiptBytes, options);
14840
+ }
13279
14841
  const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
13280
14842
  if (!parentValid)
13281
14843
  return coordinator.submitReview(taskId, ctx, verdictInput);
13282
- const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
13283
- if (!receiptValid) {
14844
+ if (!coordinator.isReviewVerdictValid(taskId, receiptBytes)) {
13284
14845
  return withReviewRecovery(coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict"), RELEASED_REVIEW_RECOVERY);
13285
14846
  }
13286
14847
  const parentJson = extractVerdictJson(verdictInput);
13287
- const receiptJson = extractVerdictJson(observed.receipt.result);
13288
- if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
14848
+ const receiptJson = extractVerdictJson(receiptBytes);
14849
+ if (!parentJson || !receiptJson || verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
13289
14850
  return { state: "blocked", reason: "parent verdict does not match reviewer receipt", recovery_action: MISMATCH_REVIEW_RECOVERY };
13290
14851
  }
13291
- return coordinator.submitReview(taskId, ctx, verdictInput);
14852
+ return coordinator.submitReview(taskId, ctx, verdictInput, options);
14853
+ }
14854
+
14855
+ // plugins/immune-brain/runtime/claude/kernel_ports.ts
14856
+ function diffSnapshotOf(root, record) {
14857
+ return taskDeliveryIdentity(root, record);
14858
+ }
14859
+ function diffHashOf(root, record) {
14860
+ return diffSnapshotOf(root, record).diff_hash;
14861
+ }
14862
+ function readTaskIntentForRecord(root, taskId) {
14863
+ recoverKernelStoreFollowUps(root, taskId);
14864
+ const currentPath = readTaskRecordRaw(root, taskId).record?.intent_ref?.path;
14865
+ return readTaskIntent(root, taskId, currentPath);
14866
+ }
14867
+ async function submitClaudeReview(host, coordinator, ctx, taskId, verdictInput) {
14868
+ return submitMediatedReview(coordinator, ctx, taskId, verdictInput, () => host.inspectReviewForTask(taskId));
13292
14869
  }
13293
14870
  function settledKernelResult(result) {
13294
14871
  const state = result.state;
@@ -13496,7 +15073,7 @@ class ClaudeRuntime {
13496
15073
  if (!slug)
13497
15074
  return result;
13498
15075
  try {
13499
- if (existsSync11(join16(this.cwd, ".imm", "audit", taskId)))
15076
+ if (existsSync12(join17(this.cwd, ".imm", "audit", taskId)))
13500
15077
  execFileSync8("git", ["-C", this.cwd, "add", "--", `.imm/audit/${taskId}`], { stdio: "ignore" });
13501
15078
  const batch = await this.startUnattendedBatch(slug, meta, { reuseOnly: true });
13502
15079
  return { ...result, batch };
@@ -13593,7 +15170,7 @@ class ClaudeRuntime {
13593
15170
  throw new Error("approve_breaking_intent_revision requires next_intent");
13594
15171
  const nextIntentHash = nextIntent ? canonicalIntentHash(nextIntent) : undefined;
13595
15172
  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;
15173
+ const sidecar = nextIntent ? join17(this.cwd, priorIntent.intent_ref.path) : undefined;
13597
15174
  const stagedSnapshot = sidecar ? captureStagedIntent(this.cwd, priorIntent.intent_ref.path) : undefined;
13598
15175
  const restoreStagedIntent2 = () => {
13599
15176
  if (!stagedSnapshot)
@@ -13702,8 +15279,8 @@ class ClaudeRuntime {
13702
15279
  const { app } = await this.authority();
13703
15280
  const operation = input.operation.op === "revise_intent" ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) } : input.operation;
13704
15281
  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;
15282
+ const sidecar = join17(ctx.cwd, priorIntent.intent_ref.path);
15283
+ const priorBytes = operation.op === "revise_intent" ? readFileSync15(sidecar) : null;
13707
15284
  const priorStaged = priorBytes !== null ? captureStagedIntent(ctx.cwd, priorIntent.intent_ref.path) : null;
13708
15285
  try {
13709
15286
  if (priorBytes) {
@@ -13750,7 +15327,7 @@ class ClaudeRuntime {
13750
15327
  root: this.cwd,
13751
15328
  initiative_slug: initiativeSlug,
13752
15329
  now,
13753
- readInitiative: this.readInitiative ?? observeGithubInitiative
15330
+ readInitiative: this.readInitiative ?? observeInitiative
13754
15331
  });
13755
15332
  if (!preflight.ok) {
13756
15333
  return {
@@ -13770,7 +15347,7 @@ class ClaudeRuntime {
13770
15347
  initiative_slug: initiativeSlug,
13771
15348
  now,
13772
15349
  projection: preflight.projection,
13773
- readInitiative: this.readInitiative ?? observeGithubInitiative,
15350
+ readInitiative: this.readInitiative ?? observeInitiative,
13774
15351
  nonce: enrollmentNonce(),
13775
15352
  gate: async (facts) => {
13776
15353
  if (reuseOnly)
@@ -13803,6 +15380,13 @@ class ClaudeRuntime {
13803
15380
  reason: child.reason
13804
15381
  })),
13805
15382
  budget: facts.budget,
15383
+ ...options.max_parallel !== undefined ? {
15384
+ lane_mode: {
15385
+ max_parallel: options.max_parallel,
15386
+ parallel_groups: facts.parallel_groups,
15387
+ serialized: facts.scope_conflicts
15388
+ }
15389
+ } : {},
13806
15390
  ...facts.reuse_blockers.length > 0 ? {
13807
15391
  re_confirmation_required: facts.reuse_blockers,
13808
15392
  recovery: "confirm to issue a fresh authorization bound to the current plan and HEAD"
@@ -13902,7 +15486,9 @@ class ClaudeRuntime {
13902
15486
  budget,
13903
15487
  now,
13904
15488
  kernel: kernelPort,
13905
- git: this.batchGit
15489
+ git: this.batchGit,
15490
+ ...options.max_parallel !== undefined ? { max_parallel: options.max_parallel } : {},
15491
+ ...options.lane_offers !== undefined ? { lane_offers: options.lane_offers } : {}
13906
15492
  });
13907
15493
  if (report.batch_state === "rejected")
13908
15494
  return batchReason("batch_run_rejected", report.reason ?? "");
@@ -13912,6 +15498,81 @@ class ClaudeRuntime {
13912
15498
  report
13913
15499
  };
13914
15500
  }
15501
+ async retireStaleBatch(initiativeSlug, meta) {
15502
+ throwIfCancelled(meta.signal);
15503
+ const probe = probeHost(this.env, process.platform, this.hostVersion);
15504
+ if (!probe.ok)
15505
+ throw new NativeAuthorityError("unsupported_host", probe.reason);
15506
+ if (!this.requestConfirmation)
15507
+ throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
15508
+ const outcome = await retireStaleBatch({
15509
+ root: this.cwd,
15510
+ initiative_slug: initiativeSlug,
15511
+ now: new Date().toISOString(),
15512
+ gate: async (facts) => {
15513
+ let confirmationResult;
15514
+ try {
15515
+ confirmationResult = await this.requestConfirmation({
15516
+ operation: "retire_stale_batch",
15517
+ initiativeSlug,
15518
+ toolCallId: meta.toolCallId,
15519
+ batchDisposition: {
15520
+ batch_id: facts.batch_id,
15521
+ batch_state: facts.batch_state,
15522
+ batch_branch: facts.branch,
15523
+ plan_digest: facts.plan_digest,
15524
+ recorded_commits: facts.recorded_commits.length,
15525
+ children: facts.children.map((child) => ({
15526
+ task_id: child.task_id,
15527
+ slice_id: child.slice_id,
15528
+ state: child.state,
15529
+ commit: child.commit,
15530
+ lane_branch: child.lane_branch
15531
+ }))
15532
+ },
15533
+ signal: meta.signal
15534
+ });
15535
+ } catch (err) {
15536
+ if (meta.signal?.aborted)
15537
+ return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
15538
+ if (err instanceof NativeAuthorityError) {
15539
+ if (err.reasonCode === "unsupported_host")
15540
+ throw err;
15541
+ if (err.reasonCode === "user_cancelled") {
15542
+ return {
15543
+ kind: "host_rejection",
15544
+ value: { state: "cancelled", reason: err.message, recovery_action: err.recoveryAction }
15545
+ };
15546
+ }
15547
+ return {
15548
+ kind: "host_rejection",
15549
+ value: { state: "rejected", reason: err.message, recovery_action: err.recoveryAction }
15550
+ };
15551
+ }
15552
+ return {
15553
+ kind: "host_rejection",
15554
+ value: batchReason("confirmation_failed", err instanceof Error ? err.message : String(err))
15555
+ };
15556
+ }
15557
+ if (confirmationResult.decision === "cancel" && meta.signal?.aborted)
15558
+ return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
15559
+ if (meta.signal?.aborted)
15560
+ return { kind: "host_rejection", value: batchReason("cancelled_before_execution") };
15561
+ if (confirmationResult.decision === "decline")
15562
+ return { kind: "host_rejection", value: batchReason("confirmation_declined") };
15563
+ if (confirmationResult.decision === "cancel")
15564
+ return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
15565
+ if (confirmationResult.decision !== "accept")
15566
+ return { kind: "host_rejection", value: batchReason("confirmation_no_decision") };
15567
+ return { kind: "confirmed", request_id: confirmationResult.requestId };
15568
+ }
15569
+ });
15570
+ if (outcome.outcome === "host_rejection")
15571
+ return outcome.value;
15572
+ if (outcome.outcome === "rejected")
15573
+ return outcome.rejection;
15574
+ return { state: "retired", batch_id: outcome.batch_id, report: outcome.report };
15575
+ }
13915
15576
  }
13916
15577
 
13917
15578
  // plugins/immune-brain/runtime/claude/mcp_server.ts
@@ -13920,12 +15581,13 @@ var TOOLS = [
13920
15581
  { name: "status", description: "Read the Kernel Assurance Projection for an exact task.", privileged: false },
13921
15582
  { name: "enroll", description: "Enroll a Git-tracked TaskIntent after native confirmation.", privileged: true },
13922
15583
  { name: "advance_assurance", description: "Advance frozen Assurance through deterministic QA and Review reservation.", privileged: false },
13923
- { name: "submit_review", description: "Submit the Parent-mediated Review verdict bound to the correlated receipt.", privileged: false },
15584
+ { name: "submit_review", description: "Submit the Review verdict; omit the verdict to apply the correlated reviewer receipt, or relay the reviewer's structured verdict for fingerprint comparison.", privileged: false },
13924
15585
  { name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
13925
15586
  { name: "revise_intent", description: "Apply a compatible TaskIntent revision.", privileged: false },
13926
15587
  { name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
13927
15588
  { name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
13928
15589
  { name: "start_unattended_batch", description: "Start an unattended serial batch run for an Initiative after native confirmation.", privileged: true },
15590
+ { name: "retire_stale_batch", description: "Retire an Initiative's parked or drifted batch record to the terminal superseded state after native confirmation.", privileged: true },
13929
15591
  { name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false },
13930
15592
  { name: "resolve_finding", description: "Resolve one open blocking or advisory finding whose cause is fixed and verified.", privileged: false },
13931
15593
  { name: "refute_finding", description: "Refute one open finding by binding the fresh passing QA attestation that contradicts it.", privileged: false }
@@ -13937,7 +15599,21 @@ function listMcpTools() {
13937
15599
  inputSchema: {
13938
15600
  type: "object",
13939
15601
  properties: {
13940
- ...tool.name === "start_unattended_batch" ? { initiative_slug: { type: "string" } } : {
15602
+ ...tool.name === "start_unattended_batch" || tool.name === "retire_stale_batch" ? {
15603
+ initiative_slug: { type: "string" },
15604
+ ...tool.name === "start_unattended_batch" ? {
15605
+ max_parallel: { type: "integer", minimum: 1 },
15606
+ lane_offers: {
15607
+ type: "array",
15608
+ items: {
15609
+ type: "object",
15610
+ properties: { task_id: { type: "string" }, path: { type: "string" } },
15611
+ required: ["task_id", "path"],
15612
+ additionalProperties: false
15613
+ }
15614
+ }
15615
+ } : {}
15616
+ } : {
13941
15617
  task_id: { type: "string" },
13942
15618
  ...tool.name === "approve_breaking_intent_revision" || tool.name === "revise_intent" ? { next_intent: { type: "object" } } : {},
13943
15619
  ...tool.name === "stop" ? { reason: { type: "string" } } : {},
@@ -13946,7 +15622,7 @@ function listMcpTools() {
13946
15622
  ...tool.name === "refute_finding" ? { finding_id: { type: "string" }, attestation_id: { type: "string" } } : {}
13947
15623
  }
13948
15624
  },
13949
- required: tool.name === "start_unattended_batch" ? ["initiative_slug"] : tool.name === "submit_review" ? ["task_id", "verdict"] : tool.name === "resolve_finding" ? ["task_id", "finding_id"] : tool.name === "refute_finding" ? ["task_id", "finding_id", "attestation_id"] : tool.name === "revise_intent" ? ["task_id", "next_intent"] : ["task_id"]
15625
+ required: tool.name === "start_unattended_batch" || tool.name === "retire_stale_batch" ? ["initiative_slug"] : tool.name === "resolve_finding" ? ["task_id", "finding_id"] : tool.name === "refute_finding" ? ["task_id", "finding_id", "attestation_id"] : tool.name === "revise_intent" ? ["task_id", "next_intent"] : ["task_id"]
13950
15626
  },
13951
15627
  annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" }
13952
15628
  }));
@@ -14008,7 +15684,36 @@ function createMcpRuntime(options = {}) {
14008
15684
  interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
14009
15685
  signal: meta.signal
14010
15686
  };
14011
- return runtime.startUnattendedBatch(initiativeSlug, toolMeta);
15687
+ const laneOffers = parseLaneOffers(args.lane_offers);
15688
+ const maxParallel = resolveLaneParallel(options.cwd ?? process.cwd(), initiativeSlug, parseMaxParallel(args.max_parallel), laneOffers);
15689
+ return runtime.startUnattendedBatch(initiativeSlug, toolMeta, {
15690
+ ...maxParallel !== undefined ? { max_parallel: maxParallel } : {},
15691
+ ...laneOffers !== undefined ? { lane_offers: laneOffers } : {}
15692
+ });
15693
+ }
15694
+ if (name === "retire_stale_batch") {
15695
+ const initiativeSlug = String(args.initiative_slug ?? "");
15696
+ if (!initiativeSlug)
15697
+ throw new Error("initiative_slug is required");
15698
+ if ("native_decision" in args)
15699
+ throw new Error("native_decision cannot be supplied in tool arguments");
15700
+ if (!negotiatedVersion)
15701
+ throw new NativeAuthorityError("unsupported_host", "Claude Code version is unavailable");
15702
+ if (!negotiatedInteractive) {
15703
+ throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
15704
+ }
15705
+ const probe = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
15706
+ if (!probe.ok)
15707
+ throw new NativeAuthorityError("unsupported_host", probe.reason);
15708
+ const toolMeta = {
15709
+ sessionId: meta.sessionId ?? connectionId,
15710
+ toolCallId: meta.toolCallId ?? `call-${name}`,
15711
+ taskId: initiativeSlug,
15712
+ initiativeSlug,
15713
+ interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
15714
+ signal: meta.signal
15715
+ };
15716
+ return runtime.retireStaleBatch(initiativeSlug, toolMeta);
14012
15717
  }
14013
15718
  const taskId = String(args.task_id ?? "");
14014
15719
  if (!taskId)
@@ -14037,9 +15742,7 @@ function createMcpRuntime(options = {}) {
14037
15742
  if (name === "advance_assurance")
14038
15743
  return runtime.advance(taskId, toolMeta.signal, toolMeta);
14039
15744
  if (name === "submit_review") {
14040
- if (!Object.hasOwn(args, "verdict"))
14041
- throw new Error("verdict is required");
14042
- return runtime.submitReview(taskId, args.verdict, toolMeta);
15745
+ return runtime.submitReview(taskId, Object.hasOwn(args, "verdict") ? args.verdict : undefined, toolMeta);
14043
15746
  }
14044
15747
  if (name === "revise_intent") {
14045
15748
  if (!Object.hasOwn(args, "next_intent"))
@@ -14222,6 +15925,32 @@ async function writeReply(output, reply) {
14222
15925
  });
14223
15926
  }
14224
15927
  function elicitationParams(input) {
15928
+ if (input.operation === "retire_stale_batch") {
15929
+ const d = input.batchDisposition;
15930
+ const details = [
15931
+ `Operation: retire_stale_batch`,
15932
+ `Initiative: ${input.initiativeSlug}`,
15933
+ d ? `Batch: ${d.batch_id}` : null,
15934
+ d ? `Batch state: ${d.batch_state}` : null,
15935
+ d ? `Batch branch: ${d.batch_branch}` : null,
15936
+ d ? `Plan digest: ${d.plan_digest}` : null,
15937
+ d ? `Recorded child commits: ${d.recorded_commits}` : null,
15938
+ d ? `Children (${d.children.length}):
15939
+ ${d.children.map((child) => ` - ${child.task_id} (${child.slice_id}) [state: ${child.state}]${child.commit ? ` [commit: ${child.commit}]` : " [no commit]"}${child.lane_branch ? ` [lane: ${child.lane_branch}]` : ""}`).join(`
15940
+ `)}` : null,
15941
+ "Retiring writes this record to the terminal superseded state. Child states, recorded commits and the batch branch are preserved as evidence; nothing is deleted and no batch trailer or approval is manufactured.",
15942
+ "A child without a commit was never executed by this batch: it carries no delivery evidence, and the retired authorization is never reused for a revised plan.",
15943
+ "After retiring, run each remaining child with imm-run under its own authorization, or start a new batch for this Initiative."
15944
+ ].filter(Boolean);
15945
+ return {
15946
+ mode: "form",
15947
+ message: `Retire this stale batch record for ${input.initiativeSlug}?
15948
+
15949
+ ${details.join(`
15950
+ `)}`,
15951
+ requestedSchema: { type: "object", properties: {} }
15952
+ };
15953
+ }
14225
15954
  if (input.operation === "start_unattended_batch") {
14226
15955
  const b = input.batchDetails;
14227
15956
  const details = [
@@ -14235,7 +15964,14 @@ ${b.children.map((c) => ` - ${c.task_id} (${c.slice_id}) [risk: ${c.risk ?? "un
14235
15964
  b?.excluded && b.excluded.length > 0 ? `Excluded children (${b.excluded.length}):
14236
15965
  ${b.excluded.map((e) => ` - ${e.task_id} (${e.slice_id}): ${e.reason}`).join(`
14237
15966
  `)}` : null,
14238
- b?.budget ? `Budget: max_children=${b.budget.max_children}, qa_failure_limit=${b.budget.qa_failure_limit}` : null
15967
+ b?.budget ? `Budget: max_children=${b.budget.max_children}, qa_failure_limit=${b.budget.qa_failure_limit}` : null,
15968
+ b?.lane_mode ? `Lane mode: max_parallel=${b.lane_mode.max_parallel}` : null,
15969
+ b?.lane_mode ? `Parallel groups (${b.lane_mode.parallel_groups.length}):
15970
+ ${b.lane_mode.parallel_groups.map((group) => ` - ${group.join(", ")}`).join(`
15971
+ `)}` : null,
15972
+ b?.lane_mode && b.lane_mode.serialized.length > 0 ? `Serialized by overlapping scope (${b.lane_mode.serialized.length}):
15973
+ ${b.lane_mode.serialized.map((c) => ` - ${c.task_id} after ${c.overlaps_with.join(", ")}`).join(`
15974
+ `)}` : null
14239
15975
  ].filter(Boolean);
14240
15976
  return {
14241
15977
  mode: "form",