immune-brain 4.4.1 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +3 -2
  2. package/package.json +1 -1
  3. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  4. package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +0 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +47 -2
  6. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +23 -18
  7. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -3
  8. package/plugins/immune-brain/dist/BASELINE.md +18 -1
  9. package/plugins/immune-brain/dist/claude/mcp-server.mjs +1401 -1101
  10. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -0
  11. package/plugins/immune-brain/dist/imm-brainstorm.md +21 -0
  12. package/plugins/immune-brain/dist/imm-loop.md +52 -6
  13. package/plugins/immune-brain/dist/imm-planner.md +102 -13
  14. package/plugins/immune-brain/dist/role-prompts/code-review.md +19 -0
  15. package/plugins/immune-brain/dist/role-prompts/qa.md +13 -1
  16. package/plugins/immune-brain/runtime/assurance/coordinator.ts +55 -2
  17. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +4 -0
  18. package/plugins/immune-brain/runtime/claude/interaction.ts +1 -2
  19. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +73 -27
  20. package/plugins/immune-brain/runtime/claude/mcp_server.ts +3 -4
  21. package/plugins/immune-brain/runtime/claude/review_host.ts +15 -0
  22. package/plugins/immune-brain/runtime/github_issue_tracker.ts +956 -634
  23. package/plugins/immune-brain/runtime/kernel/application.ts +2 -5
  24. package/plugins/immune-brain/runtime/kernel/authority_port.ts +5 -14
  25. package/plugins/immune-brain/runtime/kernel/batch_authority.ts +30 -49
  26. package/plugins/immune-brain/runtime/kernel/canary_application.ts +1 -2
  27. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +0 -1
  28. package/plugins/immune-brain/runtime/kernel/capability_registry.ts +7 -9
  29. package/plugins/immune-brain/runtime/kernel/enrollment.ts +21 -12
  30. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +5 -11
  31. package/plugins/immune-brain/runtime/kernel/storage.ts +8 -3
  32. package/plugins/immune-brain/runtime/kernel/types.ts +2 -1
  33. package/plugins/immune-brain/runtime/kernel/validation.ts +3 -1
  34. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  35. package/plugins/immune-brain/runtime/prompts/code-review.md +19 -0
  36. package/plugins/immune-brain/runtime/prompts/qa.md +13 -1
  37. package/plugins/immune-brain/runtime/unattended/batch_git.ts +9 -1
  38. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +3 -9
  39. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +152 -46
  40. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
  41. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +193 -0
  42. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +112 -122
  43. package/plugins/immune-brain/runtime/unattended/batch_state.ts +57 -16
  44. package/plugins/immune-brain/runtime/unattended/types.ts +0 -2
  45. package/plugins/immune-brain/runtime/v4_runtime.ts +54 -0
  46. package/plugins/immune-brain/runtime/workspace_scope.ts +75 -12
  47. package/plugins/immune-brain/skills/BASELINE.md +18 -1
  48. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +5 -0
  49. package/plugins/immune-brain/skills/imm-loop/SKILL.md +4 -2
  50. package/plugins/immune-brain/skills/imm-planner/SKILL.md +10 -1
  51. package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +0 -33
@@ -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 = "4.4.1";
42
+ var PLUGIN_VERSION = "4.6.0";
43
43
 
44
44
  // plugins/immune-brain/runtime/claude/interaction.ts
45
45
  import { createHash, randomUUID } from "node:crypto";
@@ -220,6 +220,7 @@ function parseAsyncAgentLaunch(result) {
220
220
  return null;
221
221
  return { agentId, outputFile };
222
222
  }
223
+ var HANDBACK_TOOL = "SubagentHandback";
223
224
  function readAgentTranscriptResult(transcript, agentId) {
224
225
  let last = null;
225
226
  for (const line of transcript.split(`
@@ -247,6 +248,13 @@ function readAgentTranscriptResult(transcript, agentId) {
247
248
  const part = block;
248
249
  if (part.type === "text" && typeof part.text === "string")
249
250
  text += part.text;
251
+ if (part.type === "tool_use" && part.name === HANDBACK_TOOL && part.input && typeof part.input === "object") {
252
+ const report = part.input.message;
253
+ if (typeof report === "string" && report.trim()) {
254
+ last = report;
255
+ text = "";
256
+ }
257
+ }
250
258
  }
251
259
  if (text.trim())
252
260
  last = text;
@@ -1448,6 +1456,7 @@ var QA_MIN_JOB_TIMEOUT_SECONDS = 15 * 60;
1448
1456
  var QA_MAX_JOB_TIMEOUT_SECONDS = 60 * 60;
1449
1457
  var QA_JOB_OVERHEAD_SECONDS = 2 * 60;
1450
1458
  var QA_JOB_TIMEOUT_SECONDS = QA_MIN_JOB_TIMEOUT_SECONDS;
1459
+ var QA_MAX_IDENTICAL_FAILURES = 2;
1451
1460
  var REVIEW_TIMING_PROFILES = {
1452
1461
  quick: { softDeadlineSeconds: 5 * 60, stopThresholdSeconds: 15 * 60 },
1453
1462
  standard: { softDeadlineSeconds: 10 * 60, stopThresholdSeconds: 30 * 60 },
@@ -1682,6 +1691,8 @@ class AssuranceCoordinator {
1682
1691
  operationControllers = new Map;
1683
1692
  reviewReservations = new Map;
1684
1693
  rejectedReviewOperations = new Map;
1694
+ qaFailureStreaks = new Map;
1695
+ qaAttemptKeys = new Map;
1685
1696
  unknownOperations = new Map;
1686
1697
  sessionInvocations = new Set;
1687
1698
  sessionActive = true;
@@ -1710,6 +1721,8 @@ class AssuranceCoordinator {
1710
1721
  this.removeEvidence(reservation);
1711
1722
  }
1712
1723
  this.reviewReservations.clear();
1724
+ this.qaFailureStreaks.clear();
1725
+ this.qaAttemptKeys.clear();
1713
1726
  for (const invocation of [...this.sessionInvocations])
1714
1727
  this.closeSessionInvocation(invocation);
1715
1728
  }
@@ -1756,7 +1769,21 @@ class AssuranceCoordinator {
1756
1769
  }
1757
1770
  async advance(taskId, ctx, signal, onUpdate) {
1758
1771
  const checks = new Map;
1772
+ const started = performance.now();
1773
+ const stageMs = {};
1774
+ let stage = "preparing";
1775
+ let stageStarted = started;
1776
+ const closeStage = (now) => {
1777
+ stageMs[stage] = (stageMs[stage] ?? 0) + Math.round(now - stageStarted);
1778
+ };
1759
1779
  const result = await this.advanceOnce(taskId, ctx, signal, (update) => {
1780
+ const next = update.details.stage;
1781
+ if (typeof next === "string" && next !== stage) {
1782
+ const now = performance.now();
1783
+ closeStage(now);
1784
+ stage = next;
1785
+ stageStarted = now;
1786
+ }
1760
1787
  const diagnostic = update.details.diagnostic;
1761
1788
  if (diagnostic) {
1762
1789
  checks.set(diagnostic.descriptor_ref, diagnostic);
@@ -1765,8 +1792,26 @@ class AssuranceCoordinator {
1765
1792
  }
1766
1793
  onUpdate?.(update);
1767
1794
  });
1795
+ this.recordQaAttempt(taskId, result);
1768
1796
  const diagnostics = result.diagnostics ?? [...checks.values()];
1769
- return this.withRecovery(taskId, ctx, diagnostics.length ? { ...result, diagnostics } : result);
1797
+ const enriched = diagnostics.length ? { ...result, diagnostics } : result;
1798
+ if (!(this.ports.reportTimings ?? process.env.IMM_ASSURANCE_TIMINGS === "1"))
1799
+ return this.withRecovery(taskId, ctx, enriched);
1800
+ const finished = performance.now();
1801
+ closeStage(finished);
1802
+ return this.withRecovery(taskId, ctx, { ...enriched, timings: { total_ms: Math.round(finished - started), stage_ms: stageMs } });
1803
+ }
1804
+ recordQaAttempt(taskId, result) {
1805
+ const key = this.qaAttemptKeys.get(taskId);
1806
+ this.qaAttemptKeys.delete(taskId);
1807
+ if (!key || result.state === "cancelled")
1808
+ return;
1809
+ if (result.state !== "failed" || result.operation !== "qa") {
1810
+ this.qaFailureStreaks.delete(taskId);
1811
+ return;
1812
+ }
1813
+ const streak = this.qaFailureStreaks.get(taskId);
1814
+ this.qaFailureStreaks.set(taskId, { key, count: streak?.key === key ? streak.count + 1 : 1 });
1770
1815
  }
1771
1816
  async withRecovery(taskId, ctx, enriched) {
1772
1817
  if (enriched.recovery_error || !["failed", "blocked", "rework", "review_preparation_failed"].includes(enriched.state))
@@ -1933,6 +1978,12 @@ class AssuranceCoordinator {
1933
1978
  ensureOperationLive();
1934
1979
  const assurance = await this.ports.buildAssurance(ctx.cwd, taskId, "qa", projection);
1935
1980
  ensureOperationLive();
1981
+ const qaKey = `${assurance.snapshot.record_revision}|${assurance.snapshot.intent_content_hash}|${assurance.snapshot.diff_hash}`;
1982
+ const streak = this.qaFailureStreaks.get(taskId);
1983
+ const maxFailures = this.ports.qaMaxIdenticalFailures ?? QA_MAX_IDENTICAL_FAILURES;
1984
+ if (streak?.key === qaKey && streak.count >= maxFailures)
1985
+ return { state: "blocked", reason: `deterministic QA already failed ${streak.count} times on this exact snapshot; change the workspace diff or intent, or restart the session after fixing the environment, before retrying` };
1986
+ this.qaAttemptKeys.set(taskId, qaKey);
1936
1987
  const declaredQaJobMs = deriveQaJobTimeoutMs(assurance.descriptors.values());
1937
1988
  const qaJobTimeoutMs = Math.min(declaredQaJobMs, this.ports.qaJobTimeoutMs ?? declaredQaJobMs);
1938
1989
  const qaJobDeadline = setTimeout(() => {
@@ -2323,11 +2374,13 @@ import {
2323
2374
  } from "node:fs";
2324
2375
  import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
2325
2376
  function git(root, args) {
2326
- const result = spawnSync("git", ["-C", root, ...args], {
2377
+ const diff = args[0] === "diff";
2378
+ const command = diff ? ["diff", "--exit-code", ...args.slice(1)] : args;
2379
+ const result = spawnSync("git", ["--no-optional-locks", "-c", "diff.autoRefreshIndex=false", "-C", root, ...command], {
2327
2380
  encoding: "utf8",
2328
2381
  stdio: ["ignore", "pipe", "pipe"]
2329
2382
  });
2330
- return result.status === 0 ? result.stdout : null;
2383
+ return result.status === 0 || diff && result.status === 1 ? result.stdout : null;
2331
2384
  }
2332
2385
  function splitNull(value) {
2333
2386
  return value.split("\x00").filter((entry) => entry.length > 0);
@@ -2407,7 +2460,7 @@ function fileFingerprint(root, relativePath) {
2407
2460
  return `file:${stat.mode}:${createHash7("sha256").update(readFileSync3(absolutePath)).digest("hex")}`;
2408
2461
  }
2409
2462
  function dirtyPaths(root) {
2410
- const tracked = git(root, ["diff", "--name-only", "-z", "HEAD", "--"]);
2463
+ const tracked = git(root, ["diff", "--name-only", "--no-renames", "-z", "HEAD", "--"]);
2411
2464
  const untracked = git(root, [
2412
2465
  "ls-files",
2413
2466
  "--others",
@@ -2446,12 +2499,14 @@ var portablePathCollator = new Intl.Collator("und", {
2446
2499
  });
2447
2500
  var gitTaskSnapshotTestHook;
2448
2501
  function gitBytes(root, args) {
2449
- const result = spawnSync("git", ["-C", root, ...args], {
2502
+ const diff = args[0] === "diff";
2503
+ const command = diff ? ["diff", "--exit-code", ...args.slice(1)] : args;
2504
+ const result = spawnSync("git", ["--no-optional-locks", "-c", "diff.autoRefreshIndex=false", "-C", root, ...command], {
2450
2505
  encoding: null,
2451
2506
  stdio: ["ignore", "pipe", "pipe"],
2452
2507
  maxBuffer: 8 * 1024 * 1024
2453
2508
  });
2454
- if (result.status !== 0 || !Buffer.isBuffer(result.stdout)) {
2509
+ if (result.status !== 0 && !(diff && result.status === 1) || !Buffer.isBuffer(result.stdout)) {
2455
2510
  throw new Error(`Git task snapshot command failed: git ${args.join(" ")}`);
2456
2511
  }
2457
2512
  return result.stdout;
@@ -2580,6 +2635,25 @@ function headEntry(root, head, path) {
2580
2635
  function taskPathMatchesScope(path, scope) {
2581
2636
  return scope.some((scopePath) => pathMatchesScope(path, scopePath));
2582
2637
  }
2638
+ function assertNoPreEnrollmentScopeChanges(root, scopedStagedPaths, taskId) {
2639
+ const baseline = readEnrollmentBaseline(root);
2640
+ if (!baseline)
2641
+ return;
2642
+ const current = captureGitWorkspaceSnapshot(root);
2643
+ if (!current)
2644
+ throw new Error("enrollment baseline cannot be compared because Git is unavailable");
2645
+ const withheld = [...new Set(scopedStagedPaths)].filter((path) => !isOwnPlanningSidecar(path, taskId)).filter((path) => baseline.dirty_files[path] !== undefined && baseline.dirty_files[path] === current.dirty_files[path]).sort(comparePaths);
2646
+ if (withheld.length > 0)
2647
+ throw new Error(`task scope contains staged changes that predate Enrollment and cannot become task work: ${withheld.join(", ")}; stop the task and re-enroll from a clean scope`);
2648
+ }
2649
+ function dirtyScopePaths(projectRoot, scopeHint, taskId) {
2650
+ const root = realpathSync3(resolve2(projectRoot));
2651
+ const scope = assertCanonicalTaskScope(scopeHint);
2652
+ const paths = dirtyPaths(root);
2653
+ if (!paths)
2654
+ throw new Error("cannot inspect the task scope outside a committed Git workspace");
2655
+ return paths.filter((path) => !isNonDeliveryPath(path) && !isOwnPlanningSidecar(path, taskId) && taskPathMatchesScope(path, scope));
2656
+ }
2583
2657
  function taskSnapshotOnce(root, scope, taskId) {
2584
2658
  const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
2585
2659
  const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
@@ -2602,9 +2676,8 @@ function taskSnapshotOnce(root, scope, taskId) {
2602
2676
  const uncommittedInScope = [...new Set([...unstagedPaths, ...untrackedPaths])].filter((path) => taskPathMatchesScope(path, scope)).sort(comparePaths);
2603
2677
  if (uncommittedInScope.length > 0)
2604
2678
  throw new Error(`task scope contains unstaged or untracked changes: ${uncommittedInScope.join(", ")}`);
2605
- const baseline = readEnrollmentBaseline(root);
2606
- const current = baseline ? captureGitWorkspaceSnapshot(root) : null;
2607
- const taskPaths = [...new Set(stagedPaths)].filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope)).filter((path) => !(baseline && current && baseline.dirty_files[path] === current.dirty_files[path] && baseline.dirty_files[path] !== undefined)).sort(comparePaths);
2679
+ const taskPaths = [...new Set(stagedPaths)].filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope)).sort(comparePaths);
2680
+ assertNoPreEnrollmentScopeChanges(root, taskPaths, taskId);
2608
2681
  const stagedFiles = {};
2609
2682
  for (const path of taskPaths) {
2610
2683
  const current = indexEntry(root, path);
@@ -2682,9 +2755,8 @@ function taskRevisionSnapshotOnce(root, scope, baseHead, taskId) {
2682
2755
  const unstagedPaths = decodeNullPaths(gitBytes(root, ["diff", "--no-renames", "--name-only", "-z", "--"]), "unstaged task revision paths");
2683
2756
  const untrackedPaths = decodeNullPaths(gitBytes(root, ["ls-files", "--others", "--exclude-standard", "-z", "--"]), "untracked task revision paths");
2684
2757
  assertNoEnvelopeEscape(root, stagedPaths, scope, taskId);
2685
- const baseline = readEnrollmentBaseline(root);
2686
- const current = baseline ? captureGitWorkspaceSnapshot(root) : null;
2687
- const scopedStagedPaths = stagedPaths.filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope)).filter((path) => !(baseline && current && baseline.dirty_files[path] === current.dirty_files[path] && baseline.dirty_files[path] !== undefined));
2758
+ const scopedStagedPaths = stagedPaths.filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope));
2759
+ assertNoPreEnrollmentScopeChanges(root, scopedStagedPaths, taskId);
2688
2760
  const scopedUnstagedPaths = unstagedPaths.filter((path) => taskPathMatchesScope(path, scope));
2689
2761
  const scopedUntrackedPaths = untrackedPaths.filter((path) => taskPathMatchesScope(path, scope));
2690
2762
  assertNoCaseFoldCollisions([...scopedStagedPaths, ...scopedUnstagedPaths, ...scopedUntrackedPaths], "Git task revision paths");
@@ -2741,6 +2813,38 @@ function taskRevisionIdentity(projectRoot, scopeHint, baseHead, taskId) {
2741
2813
  changed_paths: Object.keys(snapshot.changed_paths).sort(comparePaths)
2742
2814
  };
2743
2815
  }
2816
+ function taskCommitRevisionIdentity(projectRoot, scopeHint, baseHead, commit) {
2817
+ const requested = resolve2(projectRoot);
2818
+ if (lstatSync2(requested).isSymbolicLink())
2819
+ throw new Error("task commit root must be a real directory");
2820
+ const root = realpathSync3(requested), scope = assertCanonicalTaskScope(scopeHint);
2821
+ if (!GIT_OBJECT_ID.test(baseHead) || !GIT_OBJECT_ID.test(commit))
2822
+ throw new Error("invalid task commit identity");
2823
+ if (gitRequired(root, ["cat-file", "-t", baseHead], "unreadable task base") !== "commit" || gitRequired(root, ["cat-file", "-t", commit], "unreadable task commit") !== "commit" || git(root, ["merge-base", "--is-ancestor", baseHead, commit]) === null)
2824
+ throw new Error("invalid task commit ancestry");
2825
+ const paths = decodeNullPaths(gitBytes(root, ["diff", "--no-renames", "--name-only", "-z", baseHead, commit, "--"]), "task commit paths").filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope)).sort(comparePaths);
2826
+ assertNoCaseFoldCollisions(paths, "task commit paths");
2827
+ const changed_paths = {};
2828
+ for (const path of paths) {
2829
+ const current = headEntry(root, commit, path), base = headEntry(root, baseHead, path);
2830
+ changed_paths[path] = {
2831
+ status: !base ? "added" : !current ? "deleted" : "modified",
2832
+ mode: current?.mode ?? null,
2833
+ oid: current?.oid ?? null,
2834
+ base_mode: base?.mode ?? null,
2835
+ base_oid: base?.oid ?? null
2836
+ };
2837
+ }
2838
+ const snapshot = {
2839
+ kind: "git-task-revision-v1",
2840
+ repository_root: root,
2841
+ base_head: baseHead,
2842
+ base_tree: gitRequired(root, ["rev-parse", `${baseHead}^{tree}`], "unreadable task base tree"),
2843
+ scope,
2844
+ changed_paths
2845
+ };
2846
+ return { diff_hash: hashTaskSnapshot(snapshot), changed_paths: paths };
2847
+ }
2744
2848
  function isGitWorkspaceSnapshot(value) {
2745
2849
  return typeof value === "object" && value !== null && !Array.isArray(value) && value.kind === "git-workspace-v1" && typeof value.repository_root === "string" && typeof value.head === "string" && typeof value.dirty_files === "object" && value.dirty_files !== null;
2746
2850
  }
@@ -4160,7 +4264,7 @@ function parseHistoryV3(value, index, violations) {
4160
4264
  actor_id: stringAt(auth.actor_id, `${path}.authority.actor_id`, violations),
4161
4265
  confirmation_ref: stringAt(auth.confirmation_ref, `${path}.authority.confirmation_ref`, violations),
4162
4266
  issued_at: stringAt(auth.issued_at, `${path}.authority.issued_at`, violations),
4163
- expires_at: stringAt(auth.expires_at, `${path}.authority.expires_at`, violations)
4267
+ ...auth.expires_at !== undefined ? { expires_at: stringAt(auth.expires_at, `${path}.authority.expires_at`, violations) } : {}
4164
4268
  };
4165
4269
  }
4166
4270
  return {
@@ -5728,6 +5832,9 @@ function ensureSecureDirectory(root, relativePath) {
5728
5832
  return target.path;
5729
5833
  }
5730
5834
  function readSecureProjectFile(root, relativePath) {
5835
+ return readSecureProjectBytes(root, relativePath).toString("utf8");
5836
+ }
5837
+ function readSecureProjectBytes(root, relativePath) {
5731
5838
  const candidate = safeCandidate(root, relativePath);
5732
5839
  assertNoSymlinkSegments(candidate.root, candidate.path);
5733
5840
  const before = pathStatOrNull(candidate.path);
@@ -5743,7 +5850,7 @@ function readSecureProjectFile(root, relativePath) {
5743
5850
  const opened = fstatSync4(fd);
5744
5851
  if (opened.dev !== before.dev || opened.ino !== before.ino)
5745
5852
  throw new KernelStoreSecurityError(`source identity changed: ${relativePath}`);
5746
- const content = readFileSync8(fd, "utf8");
5853
+ const content = readFileSync8(fd);
5747
5854
  const after = lstatSync7(candidate.path);
5748
5855
  if (after.dev !== opened.dev || after.ino !== opened.ino)
5749
5856
  throw new KernelStoreSecurityError(`source identity changed: ${relativePath}`);
@@ -6679,6 +6786,8 @@ function publishInput(root, input) {
6679
6786
  const recomputed = `sha256:${createHash13("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
6680
6787
  if (recomputed !== input.expectedDiffHash)
6681
6788
  throw new Error("review task revision does not match assurance snapshot");
6789
+ if (Object.keys(snapshot.changed_paths).length === 0)
6790
+ throw new Error("review revision carries no task change; stage the task's in-scope work before Review");
6682
6791
  return { snapshot, revision: publishReviewRevision(snapshot.repository_root, snapshot, recomputed, input.taskId) };
6683
6792
  }
6684
6793
  function captureReviewManifest(root, input) {
@@ -6968,7 +7077,7 @@ async function projectAssurance(root, taskId, diffProvider) {
6968
7077
 
6969
7078
  // plugins/immune-brain/runtime/kernel/application.ts
6970
7079
  function applyTaskAction(input) {
6971
- const { root, task_id, prior_intent_token, registry, capability, diffProvider, now } = input;
7080
+ const { root, task_id, prior_intent_token, registry, capability, diffProvider } = input;
6972
7081
  return withKernelStoreLock(root, () => {
6973
7082
  const current = readTaskRecordRaw(root, task_id);
6974
7083
  if (!current.record)
@@ -7019,7 +7128,7 @@ function applyTaskAction(input) {
7019
7128
  diff_hash: trustedDiff.diff_hash,
7020
7129
  ...action.type === "request_rework" ? { findings_digest: findingsDigestV2(action.findings) } : {}
7021
7130
  } : null;
7022
- const inspectedAudit = expectedAuthority ? registry.inspect(capability, expectedAuthority, now) : null;
7131
+ const inspectedAudit = expectedAuthority ? registry.inspect(capability, expectedAuthority) : null;
7023
7132
  const authorityRunId = inspectedAudit?.run_id;
7024
7133
  const mutation = reduceTask(current.record, action, inspectedAudit ? inspectedAudit.audit : null, trustedDiff.changed_paths);
7025
7134
  if (!isReducedMutation(mutation))
@@ -7045,7 +7154,7 @@ function applyTaskAction(input) {
7045
7154
  consumeIntentToken(prior_intent_token);
7046
7155
  consumeIntentToken(freshRead.token);
7047
7156
  if (expectedAuthority) {
7048
- registry.consume(capability, expectedAuthority, now);
7157
+ registry.consume(capability, expectedAuthority);
7049
7158
  }
7050
7159
  const nextWorking = mutation.next_workspace_working;
7051
7160
  const nextRecord = input.artifact_transition ? {
@@ -7371,7 +7480,6 @@ function createCanaryApplication(registry) {
7371
7480
  registry,
7372
7481
  capability,
7373
7482
  diffProvider: input.diffProvider,
7374
- now: Date.parse(now),
7375
7483
  ...operation.op === "complete" || operation.op === "stop" ? { terminal: { terminalized_at: now } } : {},
7376
7484
  ...artifactTransition ? { artifact_transition: artifactTransition } : {}
7377
7485
  });
@@ -7419,7 +7527,7 @@ function createCanaryApplication(registry) {
7419
7527
  intent_revision: current.record.intent_snapshot.revision,
7420
7528
  intent_content_hash: current.record.intent_ref.content_hash,
7421
7529
  diff_hash: "sha256:" + "0".repeat(64)
7422
- }, Date.parse(now));
7530
+ });
7423
7531
  const nextClaim = {
7424
7532
  ...claim,
7425
7533
  lifecycle_status: "draining",
@@ -7460,7 +7568,7 @@ function createCapabilityRegistry(capabilityBrand, hooks, domainLabel) {
7460
7568
  return {
7461
7569
  brand,
7462
7570
  issue(binding, issuedAt = new Date().toISOString()) {
7463
- hooks.validateBinding(binding, issuedAt);
7571
+ hooks.validateBinding(binding);
7464
7572
  const capability = Object.freeze(Object.defineProperties({}, {
7465
7573
  [capabilityBrand]: { value: true, enumerable: false, writable: false, configurable: false },
7466
7574
  [brand]: { value: true, enumerable: false, writable: false, configurable: false }
@@ -7468,16 +7576,16 @@ function createCapabilityRegistry(capabilityBrand, hooks, domainLabel) {
7468
7576
  states.set(capability, { binding: { ...binding }, issued_at: issuedAt, consumed: false });
7469
7577
  return capability;
7470
7578
  },
7471
- inspect(capability, expected, now = Date.now()) {
7579
+ inspect(capability, expected) {
7472
7580
  if (!isCapability(capability))
7473
7581
  throw new Error(`${domainLabel} capability is not recognized by this registry`);
7474
7582
  const state = stateOf(capability);
7475
7583
  if (state.consumed)
7476
7584
  throw new Error(`${domainLabel} capability already consumed`);
7477
- return hooks.validateAndProject({ ...state.binding, issued_at: state.issued_at }, expected, now);
7585
+ return hooks.validateAndProject({ ...state.binding, issued_at: state.issued_at }, expected);
7478
7586
  },
7479
- consume(capability, expected, now = Date.now()) {
7480
- const validated = this.inspect(capability, expected, now);
7587
+ consume(capability, expected) {
7588
+ const validated = this.inspect(capability, expected);
7481
7589
  stateOf(capability).consumed = true;
7482
7590
  return validated;
7483
7591
  },
@@ -7494,7 +7602,7 @@ function digestOfAction(action) {
7494
7602
  }
7495
7603
  function createMutationAuthorityRegistry() {
7496
7604
  const inner = createCapabilityRegistry(MUTATION_AUTHORITY_CAPABILITY_BRAND, {
7497
- validateBinding(binding, issuedAt) {
7605
+ validateBinding(binding) {
7498
7606
  const missing = [];
7499
7607
  if (binding.run_id !== undefined && binding.run_id.length === 0)
7500
7608
  throw new Error("authority capability run_id must not be empty");
@@ -7508,12 +7616,8 @@ function createMutationAuthorityRegistry() {
7508
7616
  throw new Error(`authority capability binding is incomplete: ${missing.join(", ")}`);
7509
7617
  if (binding.findings_digest !== null && !/^sha256:[a-f0-9]{64}$/.test(binding.findings_digest))
7510
7618
  throw new Error("authority capability findings_digest must be a canonical sha256 hash");
7511
- if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
7512
- throw new Error("authority capability must have a future expiry");
7513
7619
  },
7514
- validateAndProject(state, expected, now) {
7515
- if (Date.parse(state.expires_at) <= now)
7516
- throw new Error("authority capability has expired");
7620
+ validateAndProject(state, expected) {
7517
7621
  const actionDigest = digestOfAction(expected.action);
7518
7622
  if (state.action_digest !== actionDigest)
7519
7623
  throw new Error("authority capability action digest mismatch");
@@ -7540,8 +7644,7 @@ function createMutationAuthorityRegistry() {
7540
7644
  authority_kind: state.authority_kind,
7541
7645
  actor_id: canonicalActorId(state.actor_id),
7542
7646
  confirmation_ref: state.confirmation_ref,
7543
- issued_at: state.issued_at,
7544
- expires_at: state.expires_at
7647
+ issued_at: state.issued_at
7545
7648
  },
7546
7649
  action_digest: actionDigest,
7547
7650
  ...state.run_id !== undefined ? { run_id: state.run_id } : {},
@@ -7556,20 +7659,20 @@ function createMutationAuthorityRegistry() {
7556
7659
  return {
7557
7660
  brand: inner.brand,
7558
7661
  issue: inner.issue.bind(inner),
7559
- inspect(capability, expected, now = Date.now()) {
7662
+ inspect(capability, expected) {
7560
7663
  if (!capability)
7561
7664
  throw new Error("privileged action requires an opaque authority capability");
7562
7665
  try {
7563
- return inner.inspect(capability, expected, now);
7666
+ return inner.inspect(capability, expected);
7564
7667
  } catch (err) {
7565
7668
  if (err instanceof Error && err.message.includes("not recognized by this registry"))
7566
7669
  throw new Error("privileged action requires an opaque authority capability");
7567
7670
  throw err;
7568
7671
  }
7569
7672
  },
7570
- consume(capability, expected, now = Date.now()) {
7673
+ consume(capability, expected) {
7571
7674
  try {
7572
- return inner.consume(capability, expected, now);
7675
+ return inner.consume(capability, expected);
7573
7676
  } catch (err) {
7574
7677
  if (err instanceof Error && err.message.includes("not recognized by this registry"))
7575
7678
  throw new Error("privileged action requires an opaque authority capability");
@@ -7584,7 +7687,7 @@ function createMutationAuthorityRegistry() {
7584
7687
  var ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollment-capability-brand");
7585
7688
  function createEnrollmentAuthorityRegistry() {
7586
7689
  return createCapabilityRegistry(ENROLLMENT_CAPABILITY_BRAND, {
7587
- validateBinding(binding, issuedAt) {
7690
+ validateBinding(binding) {
7588
7691
  const missing = [];
7589
7692
  for (const [key, value] of Object.entries(binding)) {
7590
7693
  if (value === undefined || value === null || value === "")
@@ -7592,12 +7695,8 @@ function createEnrollmentAuthorityRegistry() {
7592
7695
  }
7593
7696
  if (missing.length > 0)
7594
7697
  throw new Error(`enrollment capability binding is incomplete: ${missing.join(", ")}`);
7595
- if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
7596
- throw new Error("enrollment capability must have a future expiry");
7597
7698
  },
7598
- validateAndProject(state, expected, now) {
7599
- if (Date.parse(state.expires_at) <= now)
7600
- throw new Error("enrollment capability has expired");
7699
+ validateAndProject(state, expected) {
7601
7700
  for (const key of Object.keys(expected)) {
7602
7701
  if (state[key] !== expected[key])
7603
7702
  throw new Error(`enrollment capability ${key} mismatch`);
@@ -7611,13 +7710,16 @@ function createEnrollmentAuthorityRegistry() {
7611
7710
  actor_id: state.actor_id,
7612
7711
  confirmation_ref: state.confirmation_ref,
7613
7712
  issued_at: state.issued_at,
7614
- expires_at: state.expires_at,
7615
7713
  nonce: state.nonce
7616
7714
  };
7617
7715
  }
7618
7716
  }, "enrollment");
7619
7717
  }
7620
7718
 
7719
+ // plugins/immune-brain/runtime/kernel/batch_authority.ts
7720
+ import { createHash as createHash16 } from "node:crypto";
7721
+ import { spawnSync as spawnSync4 } from "node:child_process";
7722
+
7621
7723
  // plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts
7622
7724
  import { createHash as createHash15 } from "node:crypto";
7623
7725
  import { spawnSync as spawnSync3 } from "node:child_process";
@@ -7719,6 +7821,238 @@ function revalidatePiCanary(root, input, previous) {
7719
7821
  return { unchanged: current.digest === previous.digest, current };
7720
7822
  }
7721
7823
 
7824
+ // plugins/immune-brain/runtime/kernel/batch_authority.ts
7825
+ var BATCH_AUTHORITY_CAPABILITY_BRAND = Symbol.for("assurance-kernel.batch-authority-capability-brand");
7826
+ var GIT_COMMIT_ID2 = /^[a-f0-9]{40}$/;
7827
+ function sha256Hex3(bytes) {
7828
+ return createHash16("sha256").update(bytes).digest("hex");
7829
+ }
7830
+ function stableStringify3(value) {
7831
+ if (value === null || typeof value !== "object")
7832
+ return JSON.stringify(value);
7833
+ if (Array.isArray(value))
7834
+ return `[${value.map((entry) => stableStringify3(entry)).join(",")}]`;
7835
+ const record = value;
7836
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify3(record[key])}`).join(",")}}`;
7837
+ }
7838
+ function computeBatchPlanDigest(children) {
7839
+ const canonical = children.map((child) => ({
7840
+ blocked_by: [...child.blocked_by],
7841
+ intent_content_hash: child.intent_content_hash,
7842
+ intent_path: child.intent_path,
7843
+ intent_revision: child.intent_revision,
7844
+ task_id: child.task_id
7845
+ }));
7846
+ return `sha256:${sha256Hex3(stableStringify3(canonical))}`;
7847
+ }
7848
+ function requireNonEmpty(binding) {
7849
+ const missing = [];
7850
+ for (const key of [
7851
+ "batch_id",
7852
+ "initiative_slug",
7853
+ "plan_digest",
7854
+ "branch",
7855
+ "base_head",
7856
+ "actor_id",
7857
+ "confirmation_ref",
7858
+ "nonce"
7859
+ ]) {
7860
+ const value = binding[key];
7861
+ if (value === undefined || value === null || value === "")
7862
+ missing.push(key);
7863
+ }
7864
+ if (!binding.budget || typeof binding.budget !== "object")
7865
+ missing.push("budget");
7866
+ if (missing.length > 0)
7867
+ throw new Error(`batch authorization binding is incomplete: ${missing.join(", ")}`);
7868
+ }
7869
+ function validateBudget(budget) {
7870
+ if (!Number.isInteger(budget.max_children) || budget.max_children <= 0)
7871
+ throw new Error("batch budget max_children must be a positive integer");
7872
+ if (!Number.isInteger(budget.qa_failure_limit) || budget.qa_failure_limit <= 0)
7873
+ throw new Error("batch budget qa_failure_limit must be a positive integer");
7874
+ }
7875
+ function validateChildren(children, planDigest) {
7876
+ if (!Array.isArray(children) || children.length === 0)
7877
+ throw new Error("batch authorization requires a non-empty child plan");
7878
+ const seen = new Set;
7879
+ for (const child of children) {
7880
+ if (!child || typeof child !== "object")
7881
+ throw new Error("batch plan child must be an object");
7882
+ for (const key of ["task_id", "intent_path", "intent_content_hash"]) {
7883
+ if (typeof child[key] !== "string" || child[key] === "")
7884
+ throw new Error(`batch plan child ${key} must be a non-empty string`);
7885
+ }
7886
+ if (!Number.isInteger(child.intent_revision) || child.intent_revision <= 0)
7887
+ throw new Error("batch plan child intent_revision must be a positive integer");
7888
+ if (!Array.isArray(child.blocked_by) || child.blocked_by.some((id) => typeof id !== "string" || id === ""))
7889
+ throw new Error("batch plan child blocked_by must be an array of task ids");
7890
+ if (seen.has(child.task_id))
7891
+ throw new Error(`batch plan child ${child.task_id} appears more than once`);
7892
+ seen.add(child.task_id);
7893
+ }
7894
+ for (const child of children) {
7895
+ for (const blocker of child.blocked_by) {
7896
+ if (!seen.has(blocker))
7897
+ throw new Error(`batch plan child ${child.task_id} is blocked by ${blocker}, which is not in the confirmed plan`);
7898
+ }
7899
+ }
7900
+ const dependencies = new Map(children.map((child) => [child.task_id, child.blocked_by]));
7901
+ const visiting = new Set;
7902
+ const visited = new Set;
7903
+ function visit(taskId) {
7904
+ if (visiting.has(taskId))
7905
+ throw new Error(`batch plan dependency cycle at ${taskId}`);
7906
+ if (visited.has(taskId))
7907
+ return;
7908
+ visiting.add(taskId);
7909
+ for (const blocker of dependencies.get(taskId))
7910
+ visit(blocker);
7911
+ visiting.delete(taskId);
7912
+ visited.add(taskId);
7913
+ }
7914
+ for (const child of children)
7915
+ visit(child.task_id);
7916
+ if (computeBatchPlanDigest(children) !== planDigest)
7917
+ throw new Error("batch plan digest does not match the confirmed child plan");
7918
+ }
7919
+ function createBatchAuthorityRegistry() {
7920
+ const inner = createCapabilityRegistry(BATCH_AUTHORITY_CAPABILITY_BRAND, {
7921
+ validateBinding(binding) {
7922
+ requireNonEmpty(binding);
7923
+ if (!isLiteralUserActor(binding.actor_id))
7924
+ throw new Error("batch authorization requires a literal-user actor_id");
7925
+ if (!GIT_COMMIT_ID2.test(binding.base_head))
7926
+ throw new Error("batch authorization base_head must be a committed 40-hex commit id");
7927
+ validateBudget(binding.budget);
7928
+ },
7929
+ validateAndProject(state, expected) {
7930
+ for (const key of Object.keys(expected)) {
7931
+ if (key === "budget") {
7932
+ const a = state.budget ?? {};
7933
+ const b = expected.budget ?? {};
7934
+ if (a.max_children !== b.max_children || a.qa_failure_limit !== b.qa_failure_limit)
7935
+ throw new Error("batch authorization budget mismatch");
7936
+ continue;
7937
+ }
7938
+ if (state[key] !== expected[key])
7939
+ throw new Error(`batch authorization ${key} mismatch`);
7940
+ }
7941
+ return {
7942
+ batch_id: state.batch_id,
7943
+ initiative_slug: state.initiative_slug,
7944
+ plan_digest: state.plan_digest,
7945
+ branch: state.branch,
7946
+ base_head: state.base_head,
7947
+ budget: { ...state.budget },
7948
+ actor_id: state.actor_id,
7949
+ confirmation_ref: state.confirmation_ref,
7950
+ issued_at: state.issued_at,
7951
+ nonce: state.nonce
7952
+ };
7953
+ }
7954
+ }, "batch authorization");
7955
+ const plans = new WeakMap;
7956
+ const consumed = new WeakMap;
7957
+ function planOf(capability) {
7958
+ const plan = plans.get(capability);
7959
+ if (!plan)
7960
+ throw new Error("batch authorization capability is not recognized by this registry");
7961
+ return plan;
7962
+ }
7963
+ function slotsOf(capability) {
7964
+ const slots = consumed.get(capability);
7965
+ if (!slots)
7966
+ throw new Error("batch authorization capability is not recognized by this registry");
7967
+ return slots;
7968
+ }
7969
+ return {
7970
+ brand: inner.brand,
7971
+ issue(binding, children, issuedAt = new Date().toISOString()) {
7972
+ requireNonEmpty(binding);
7973
+ validateChildren(children, binding.plan_digest);
7974
+ const capability = inner.issue(binding, issuedAt);
7975
+ plans.set(capability, children.map((child) => ({ ...child, blocked_by: [...child.blocked_by] })));
7976
+ consumed.set(capability, new Set);
7977
+ return capability;
7978
+ },
7979
+ inspect(capability, expected) {
7980
+ planOf(capability);
7981
+ return inner.inspect(capability, expected);
7982
+ },
7983
+ children(capability) {
7984
+ return planOf(capability).map((child) => ({ ...child, blocked_by: [...child.blocked_by] }));
7985
+ },
7986
+ consumedChildren(capability) {
7987
+ return [...slotsOf(capability)];
7988
+ },
7989
+ isChildConsumed(capability, taskId) {
7990
+ return slotsOf(capability).has(taskId);
7991
+ },
7992
+ consumeChild(capability, expected, taskId) {
7993
+ const validated = this.inspect(capability, expected);
7994
+ const plan = planOf(capability);
7995
+ if (!plan.some((child) => child.task_id === taskId))
7996
+ throw new Error(`batch_child_not_in_plan: ${taskId}`);
7997
+ const slots = slotsOf(capability);
7998
+ if (slots.has(taskId))
7999
+ throw new Error(`batch_child_slot_consumed: ${taskId}`);
8000
+ slots.add(taskId);
8001
+ return validated;
8002
+ },
8003
+ releaseChild(capability, taskId) {
8004
+ slotsOf(capability).delete(taskId);
8005
+ },
8006
+ isExhausted(capability) {
8007
+ return slotsOf(capability).size >= planOf(capability).length;
8008
+ }
8009
+ };
8010
+ }
8011
+ function assertBatchLineageOrigin(root, baseHead, expectedHead) {
8012
+ if (expectedHead === baseHead)
8013
+ return;
8014
+ const descends = spawnSync4("git", ["-C", root, "merge-base", "--is-ancestor", baseHead, expectedHead], {
8015
+ stdio: ["ignore", "ignore", "ignore"]
8016
+ }).status === 0;
8017
+ if (!descends)
8018
+ throw new Error(`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${baseHead} or a fast-forward of it, not ${expectedHead}`);
8019
+ }
8020
+ function deriveChildEnrollment(root, registry, input) {
8021
+ const validated = registry.inspect(input.capability, input.binding);
8022
+ const child = registry.children(input.capability).find((entry) => entry.task_id === input.task_id);
8023
+ if (!child)
8024
+ throw new Error(`batch_child_not_in_plan: ${input.task_id}`);
8025
+ if (registry.isChildConsumed(input.capability, input.task_id))
8026
+ throw new Error(`batch_child_slot_consumed: ${input.task_id}`);
8027
+ if (!GIT_COMMIT_ID2.test(input.expected_head))
8028
+ throw new Error("batch_head_lineage_broken: expected_head is not a commit id");
8029
+ const preparation = preparePiCanary(root, { task_id: input.task_id, now: input.now });
8030
+ if (!preparation.git_base_head)
8031
+ throw new Error(preparation.git_error ?? "enrollment requires a committed Git HEAD");
8032
+ if (preparation.git_base_head !== input.expected_head)
8033
+ throw new Error(`batch_head_lineage_broken: expected ${input.expected_head}, found ${preparation.git_base_head}`);
8034
+ if (!preparation.intent)
8035
+ throw new Error(`batch_child_intent_changed: ${input.task_id} intent sidecar is unreadable`);
8036
+ if (preparation.intent.path !== child.intent_path || preparation.intent.revision !== child.intent_revision || preparation.intent.content_hash !== child.intent_content_hash)
8037
+ throw new Error(`batch_child_intent_changed: ${input.task_id}`);
8038
+ if (registry.consumedChildren(input.capability).length === 0)
8039
+ assertBatchLineageOrigin(root, validated.base_head, input.expected_head);
8040
+ return {
8041
+ child,
8042
+ preparation,
8043
+ binding: {
8044
+ task_id: child.task_id,
8045
+ intent_path: child.intent_path,
8046
+ intent_revision: child.intent_revision,
8047
+ intent_content_hash: child.intent_content_hash,
8048
+ preparation_digest: preparation.digest,
8049
+ actor_id: validated.actor_id,
8050
+ confirmation_ref: validated.confirmation_ref,
8051
+ nonce: `${validated.nonce}:${child.task_id}`
8052
+ }
8053
+ };
8054
+ }
8055
+
7722
8056
  // plugins/immune-brain/runtime/kernel/enrollment.ts
7723
8057
  function runEnrollmentPreconditionChecks(root, input, capability, registry, mode, beforeLock, onReady) {
7724
8058
  const blockers = [];
@@ -7770,10 +8104,21 @@ function runEnrollmentPreconditionChecks(root, input, capability, registry, mode
7770
8104
  } catch (error) {
7771
8105
  fail(`git base: ${error instanceof Error ? error.message : String(error)}`, error);
7772
8106
  }
7773
- const state = {
7774
- validated,
7775
- current,
7776
- workspace,
8107
+ if (intent && gitBaseHead) {
8108
+ try {
8109
+ const dirty = dirtyScopePaths(root, intent.intent.scope_hint, input.task_id);
8110
+ if (dirty.length > 0) {
8111
+ const message = `task scope is already dirty before Enrollment: ${dirty.join(", ")}; commit, stash or revert these paths first`;
8112
+ fail(message, new Error(message));
8113
+ }
8114
+ } catch (error) {
8115
+ fail(`scope: ${error instanceof Error ? error.message : String(error)}`, error);
8116
+ }
8117
+ }
8118
+ const state = {
8119
+ validated,
8120
+ current,
8121
+ workspace,
7777
8122
  intent,
7778
8123
  gitBaseHead
7779
8124
  };
@@ -7866,16 +8211,16 @@ function enrollCanaryTask(root, input, registry) {
7866
8211
  if (checks.gitBaseHead !== gitBaseHead)
7867
8212
  throw new Error("Git HEAD moved after the enrollment confirmation");
7868
8213
  if (input.batch) {
7869
- const batch = input.batch.registry.inspect(input.batch.capability, input.batch.binding, Date.parse(input.now));
7870
- if (input.batch.registry.consumedChildren(input.batch.capability).length === 0 && input.batch.expected_head !== batch.base_head)
7871
- throw new Error(`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${batch.base_head}, not ${input.batch.expected_head}`);
8214
+ const batch = input.batch.registry.inspect(input.batch.capability, input.batch.binding);
8215
+ if (input.batch.registry.consumedChildren(input.batch.capability).length === 0)
8216
+ assertBatchLineageOrigin(root, batch.base_head, input.batch.expected_head);
7872
8217
  if (checks.gitBaseHead !== input.batch.expected_head)
7873
8218
  throw new Error(`batch_head_lineage_broken: expected ${input.batch.expected_head}, found ${checks.gitBaseHead}`);
7874
8219
  }
7875
8220
  registry.consume(input.capability, input.capability_binding);
7876
8221
  consumed = true;
7877
8222
  if (input.batch)
7878
- input.batch.registry.consumeChild(input.batch.capability, input.batch.binding, input.task_id, Date.parse(input.now));
8223
+ input.batch.registry.consumeChild(input.batch.capability, input.batch.binding, input.task_id);
7879
8224
  if (!gitBaseHead)
7880
8225
  throw new Error("enrollment requires a committed Git HEAD");
7881
8226
  const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
@@ -7929,7 +8274,7 @@ function enrollCanaryTask(root, input, registry) {
7929
8274
  }
7930
8275
 
7931
8276
  // plugins/immune-brain/runtime/assurance/enrollment_git_base.ts
7932
- import { execFileSync as execFileSync4, spawnSync as spawnSync4 } from "node:child_process";
8277
+ import { execFileSync as execFileSync4, spawnSync as spawnSync5 } from "node:child_process";
7933
8278
  function git2(root, args, input, env = process.env) {
7934
8279
  return execFileSync4("git", args, { cwd: root, encoding: "utf8", input, env, stdio: ["pipe", "pipe", "pipe"] }).trim();
7935
8280
  }
@@ -7942,8 +8287,8 @@ function inspectEnrollmentGitBase(root) {
7942
8287
  const branch = git2(root, ["symbolic-ref", "--no-recurse", "HEAD"]);
7943
8288
  if (!branch.startsWith("refs/heads/"))
7944
8289
  throw new Error("Enrollment HEAD must name a local branch");
7945
- const ref = spawnSync4("git", ["show-ref", "--verify", "--quiet", branch], { cwd: root });
7946
- const symbolic = spawnSync4("git", ["symbolic-ref", "-q", branch], { cwd: root });
8290
+ const ref = spawnSync5("git", ["show-ref", "--verify", "--quiet", branch], { cwd: root });
8291
+ const symbolic = spawnSync5("git", ["symbolic-ref", "-q", branch], { cwd: root });
7947
8292
  if (ref.status !== 1 || symbolic.status !== 1)
7948
8293
  throw new Error("Enrollment Git HEAD is invalid; only an absent branch can be initialized");
7949
8294
  return { state: "unborn", branch };
@@ -7953,7 +8298,7 @@ function enrollmentGitBaseNotice(base) {
7953
8298
  }
7954
8299
  function identityEnv(root) {
7955
8300
  const configured = (key) => {
7956
- const result = spawnSync4("git", ["config", "--get", key], { cwd: root, encoding: "utf8" });
8301
+ const result = spawnSync5("git", ["config", "--get", key], { cwd: root, encoding: "utf8" });
7957
8302
  if (result.status !== 0 && result.status !== 1)
7958
8303
  throw new Error(`Cannot read Git identity: ${key}`);
7959
8304
  return result.stdout?.trim() ?? "";
@@ -8002,10 +8347,10 @@ function initializeEnrollmentGitBase(root, input, previous, base, signal) {
8002
8347
  }
8003
8348
 
8004
8349
  // plugins/immune-brain/runtime/assurance/qa.ts
8005
- import { createHash as createHash17 } from "node:crypto";
8350
+ import { createHash as createHash18 } from "node:crypto";
8006
8351
  // plugins/immune-brain/runtime/assurance/delivery_workspace.ts
8007
8352
  import { execFileSync as execFileSync5 } from "node:child_process";
8008
- import { createHash as createHash16 } from "node:crypto";
8353
+ import { createHash as createHash17 } from "node:crypto";
8009
8354
  import { chmodSync as chmodSync2, lstatSync as lstatSync8, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync10, readlinkSync as readlinkSync2, realpathSync as realpathSync9, rmSync as rmSync5 } from "node:fs";
8010
8355
  import { tmpdir as tmpdir3 } from "node:os";
8011
8356
  import { dirname as dirname6, isAbsolute as isAbsolute5, join as join8, parse, relative as relative4, resolve as resolve9, sep as sep5 } from "node:path";
@@ -8155,7 +8500,7 @@ function workspaceFiles(root, dir = root, result = Object.create(null)) {
8155
8500
  result[rel] = `${stat.mode & 4095}:directory`;
8156
8501
  workspaceFiles(root, path, result);
8157
8502
  } else if (stat.isFile())
8158
- result[rel] = `${stat.mode & 4095}:${createHash16("sha256").update(readFileSync10(path)).digest("hex")}`;
8503
+ result[rel] = `${stat.mode & 4095}:${createHash17("sha256").update(readFileSync10(path)).digest("hex")}`;
8159
8504
  else
8160
8505
  throw new DeliveryWorkspaceError("delivery contains an unsupported filesystem entry");
8161
8506
  }
@@ -8257,7 +8602,7 @@ function deliveryTreeForSnapshot(snapshot, writeTree) {
8257
8602
  if (!record || record.contract !== "assurance_kernel/task_record/v4" || !record.git_base_head)
8258
8603
  throw new Error("QA delivery requires a TaskRecord v4 git_base_head");
8259
8604
  const captured = captureGitTaskRevisionSnapshot(snapshot.root, record.intent_snapshot.scope_hint, record.git_base_head, snapshot.task_id);
8260
- const digest = `sha256:${createHash17("sha256").update(JSON.stringify(captured)).digest("hex")}`;
8605
+ const digest = `sha256:${createHash18("sha256").update(JSON.stringify(captured)).digest("hex")}`;
8261
8606
  if (digest !== snapshot.diff_hash)
8262
8607
  throw new Error("QA delivery identity does not match the frozen snapshot");
8263
8608
  return writeDeliveryTree(snapshot.root, captured);
@@ -8292,7 +8637,7 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
8292
8637
  const diagnostics = (stage, outcome, affected = group, commandStage = "check", elapsed = 0, result) => affected.map((item) => ({
8293
8638
  acceptance_id: item.id,
8294
8639
  descriptor_ref: `acceptance/${item.index}/verification/${commandStage === "prepare" ? "environment/prepare" : "command"}`,
8295
- descriptor_digest: `sha256:${createHash17("sha256").update(JSON.stringify(item.descriptor)).digest("hex")}`,
8640
+ descriptor_digest: `sha256:${createHash18("sha256").update(JSON.stringify(item.descriptor)).digest("hex")}`,
8296
8641
  stage,
8297
8642
  outcome,
8298
8643
  elapsed_ms: elapsed,
@@ -8366,8 +8711,8 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
8366
8711
  }
8367
8712
  evidence.push({
8368
8713
  stage,
8369
- environment: createHash17("sha256").update(environmentKey).digest("hex"),
8370
- command: createHash17("sha256").update(JSON.stringify(command)).digest("hex"),
8714
+ environment: createHash18("sha256").update(environmentKey).digest("hex"),
8715
+ command: createHash18("sha256").update(JSON.stringify(command)).digest("hex"),
8371
8716
  entry: frozen?.entry?.content_hash ?? null,
8372
8717
  interpreter: frozen?.interpreter?.content_hash ?? null,
8373
8718
  exit_code: result.exit_code,
@@ -8445,7 +8790,7 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
8445
8790
  };
8446
8791
  if (findings.length)
8447
8792
  return { ...base, decision: "rework", findings };
8448
- const executionDigest = createHash17("sha256").update(JSON.stringify({
8793
+ const executionDigest = createHash18("sha256").update(JSON.stringify({
8449
8794
  tree,
8450
8795
  platform: process.platform,
8451
8796
  arch: process.arch,
@@ -8530,11 +8875,6 @@ var BATCH_REASONS = Object.freeze({
8530
8875
  reason: "native confirmation port is unavailable",
8531
8876
  recovery_action: "retry through a fresh native gate in the current Host"
8532
8877
  },
8533
- confirmation_timed_out: {
8534
- state: "rejected",
8535
- reason: "native confirmation timed out waiting for user interaction",
8536
- recovery_action: "retry through a fresh native gate in the current Host"
8537
- },
8538
8878
  confirmation_cancelled: {
8539
8879
  state: "cancelled",
8540
8880
  reason: "native interaction cancelled",
@@ -8600,23 +8940,6 @@ function batchReason(key, detail = "") {
8600
8940
  };
8601
8941
  }
8602
8942
 
8603
- // plugins/immune-brain/runtime/unattended/confirmation_deadline.ts
8604
- var CONFIRMATION_TIMEOUT_DEFAULT_MS = 60000;
8605
- var CONFIRMATION_TIMEOUT_ENV = "IMMUNE_BRAIN_BATCH_TIMEOUT_MS";
8606
- function startConfirmationDeadline(input) {
8607
- const configured = Number(input.env?.[CONFIRMATION_TIMEOUT_ENV]);
8608
- const timeoutMs = Number.isFinite(configured) && configured > 0 ? configured : CONFIRMATION_TIMEOUT_DEFAULT_MS;
8609
- const controller = new AbortController;
8610
- const timer = setTimeout(() => {
8611
- controller.abort(new Error("native confirmation timed out waiting for user interaction"));
8612
- }, timeoutMs);
8613
- return {
8614
- signal: input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal,
8615
- timedOut: () => controller.signal.aborted && !input.signal?.aborted,
8616
- clear: () => clearTimeout(timer)
8617
- };
8618
- }
8619
-
8620
8943
  // plugins/immune-brain/runtime/authorization_operation.ts
8621
8944
  function deriveAuthorizationOperation(input) {
8622
8945
  if (input.readiness.state === "resolve_user_decision")
@@ -8846,6 +9169,17 @@ function createGhTransport(binary = "gh") {
8846
9169
  return new Promise((complete) => {
8847
9170
  let stdout = Buffer.alloc(0);
8848
9171
  let stderr = Buffer.alloc(0);
9172
+ if (options.signal?.aborted) {
9173
+ complete({
9174
+ exit_code: 1,
9175
+ stdout: "",
9176
+ stderr: "operation cancelled by the caller",
9177
+ timed_out: true,
9178
+ output_exceeded: false
9179
+ });
9180
+ return;
9181
+ }
9182
+ const remaining = GH_TIMEOUT_MS;
8849
9183
  let timedOut = false;
8850
9184
  let outputExceeded = false;
8851
9185
  let timer;
@@ -8903,7 +9237,7 @@ function createGhTransport(binary = "gh") {
8903
9237
  timer = setTimeout(() => {
8904
9238
  timedOut = true;
8905
9239
  child.kill("SIGKILL");
8906
- }, GH_TIMEOUT_MS);
9240
+ }, remaining);
8907
9241
  child.once("close", (code) => {
8908
9242
  finish(code ?? 1);
8909
9243
  });
@@ -9007,6 +9341,33 @@ function initiativeLookup(issues, repositoryId, initiativeId) {
9007
9341
  const initiative = marker("initiative-id", initiativeId);
9008
9342
  return findIssue(issues, [initiative, KIND_INITIATIVE_MARKER], [marker("repo-id", repositoryId), initiative]);
9009
9343
  }
9344
+ function publishedIntentHashMarkers(body) {
9345
+ return [...body.matchAll(/<!-- immune-brain:intent-hash=([A-Za-z0-9][A-Za-z0-9._:-]{0,127}) -->/g)].map((match) => match[1]);
9346
+ }
9347
+ function publishedIntentIdentity(body) {
9348
+ const hashes = publishedIntentHashMarkers(body);
9349
+ if (hashes.length === 0)
9350
+ return { kind: "absent" };
9351
+ if (hashes.length === 1)
9352
+ return { kind: "bound", hash: hashes[0] };
9353
+ return { kind: "ambiguous", hashes };
9354
+ }
9355
+ function publishedIntentMismatch(body, taskId, expected) {
9356
+ const identity = publishedIntentIdentity(body);
9357
+ if (identity.kind === "ambiguous")
9358
+ return `published Task ${taskId} carries ${identity.hashes.length} conflicting intent-hash markers; its published Intent identity is ambiguous`;
9359
+ if (identity.kind === "absent" || expected === undefined)
9360
+ return null;
9361
+ if (identity.hash !== expected)
9362
+ return `published Task ${taskId} was published from a different TaskIntent revision; the published identity no longer matches the Intent being published`;
9363
+ return null;
9364
+ }
9365
+ function sameTrackedBody(current, expected) {
9366
+ return stripPublishedIntentHash(current) === stripPublishedIntentHash(expected);
9367
+ }
9368
+ function stripPublishedIntentHash(body) {
9369
+ return body.replace(/<!-- immune-brain:intent-hash=[A-Za-z0-9][A-Za-z0-9._:-]{0,127} -->\n?/g, "");
9370
+ }
9010
9371
  function ownershipMarkerValue(body, name) {
9011
9372
  const values = [...body.matchAll(new RegExp(`<!-- immune-brain:${name}=([A-Za-z0-9][A-Za-z0-9._-]{0,127}) -->`, "g"))];
9012
9373
  return values.length === 1 ? values[0][1] : null;
@@ -9206,7 +9567,7 @@ ${provenance}## How to use this Issue
9206
9567
 
9207
9568
  - Edit planning prose and Slice ordering directly after creation.
9208
9569
  - Keep each Slice marker attached to exactly one stable Slice entry.
9209
- - The tracker never rewrites or closes this Parent after creation; the tracker never changes or closes it automatically.
9570
+ - The tracker never rewrites this Parent after creation; the tracker closes it as completed once every Slice Child is completed, and never closes it otherwise.
9210
9571
 
9211
9572
  ## Problem
9212
9573
 
@@ -9307,7 +9668,8 @@ function childBody(repository, operation, parent) {
9307
9668
  marker("repo-id", repository.id),
9308
9669
  marker("initiative-id", operation.initiative_id),
9309
9670
  marker("slice-id", operation.slice_id),
9310
- marker("task-id", operation.task_id)
9671
+ marker("task-id", operation.task_id),
9672
+ ...operation.intent_hash ? [marker("intent-hash", operation.intent_hash)] : []
9311
9673
  ].join(`
9312
9674
  `)}
9313
9675
 
@@ -9354,7 +9716,7 @@ ${publicText(projection.agent_handoff ?? "Implement only the bounded TaskIntent
9354
9716
  ${ISSUE_FOOTER}
9355
9717
  `;
9356
9718
  }
9357
- async function upsertTask(root, gh, operation, source, pendingBinding = null, amendmentContext = undefined) {
9719
+ async function upsertTask(root, gh, operation, source) {
9358
9720
  const labelFailure = await labelAvailabilityFailure(root, gh, source.repository, desiredTaskLabels(operation));
9359
9721
  if (labelFailure)
9360
9722
  return labelFailure;
@@ -9369,8 +9731,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
9369
9731
  const found = lookup(source.issues);
9370
9732
  if (found.kind === "ambiguous")
9371
9733
  return result(operation.op, "ambiguous_remote_state", found.message);
9372
- if (found.kind === "missing" && pendingBinding !== null && pendingBinding !== undefined)
9373
- return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} is bound to Issue #${pendingBinding.issue_number} but that Issue no longer holds its Task marker; amend the binding or restore the marker instead of recreating`, parent.issue);
9374
9734
  const blockerIds = operation.projection?.blocked_by ?? [];
9375
9735
  const blockers = [];
9376
9736
  for (const blockerId of blockerIds) {
@@ -9395,32 +9755,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
9395
9755
  let createdChild = false;
9396
9756
  let labelsConverged = false;
9397
9757
  if (found.kind === "missing") {
9398
- if (amendmentContext !== undefined) {
9399
- const reRead = await snapshot(root, gh, operation.op);
9400
- if ("contract" in reRead)
9401
- return reRead;
9402
- const reReadParent = initiativeLookup(reRead.issues, reRead.repository.id, operation.initiative_id);
9403
- if (reReadParent.kind !== "found")
9404
- return result(operation.op, "ambiguous_remote_state", reReadParent.kind === "ambiguous" ? reReadParent.message : "amendment Parent is not observable before creating a new Child", parent.issue);
9405
- if (reReadParent.issue.number !== amendmentContext.parentIssueNumber)
9406
- return result(operation.op, "ambiguous_remote_state", `amendment Parent is bound to Issue #${amendmentContext.parentIssueNumber} but observed Issue #${reReadParent.issue.number} before creating a new Child`, reReadParent.issue);
9407
- if (reReadParent.issue.state !== "open")
9408
- return result(operation.op, "ambiguous_remote_state", "amendment Parent is no longer open before creating a new Child", reReadParent.issue);
9409
- if (amendmentContext.parent.title !== reReadParent.issue.title || amendmentContext.parent.body !== reReadParent.issue.body)
9410
- return result(operation.op, "ambiguous_remote_state", "amendment Parent content changed before creating a new Child", reReadParent.issue);
9411
- if (sliceCount(reReadParent.issue.body, operation.slice_id) !== 1)
9412
- return result(operation.op, "ambiguous_remote_state", `Parent Issue #${reReadParent.issue.number} lost its exact Slice marker ${operation.slice_id} before creating a new Child`, reReadParent.issue);
9413
- const raced = lookup(reRead.issues);
9414
- if (raced.kind === "ambiguous")
9415
- return result(operation.op, "ambiguous_remote_state", raced.message);
9416
- if (raced.kind === "found") {
9417
- const approved = amendmentContext.pendingContent.get(operation.task_id);
9418
- const resumable = approved !== undefined && raced.issue.state === "open" && carriesApprovedContent(raced.issue.title, raced.issue.body, approved);
9419
- if (!resumable)
9420
- return result(operation.op, "ambiguous_remote_state", `new pending Task ${operation.task_id} is unbound but Issue #${raced.issue.number} already exists with divergent content; bind it to amend`, raced.issue);
9421
- return updatePendingChild(root, gh, reRead, operation, raced.issue, undefined, blockers, approved, amendmentContext);
9422
- }
9423
- }
9424
9758
  const mutation = await gh.run([
9425
9759
  "issue",
9426
9760
  "create",
@@ -9443,11 +9777,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
9443
9777
  }
9444
9778
  if (created.issue.body !== body || created.issue.title !== title)
9445
9779
  return result(operation.op, "retryable_failure", "Task Issue did not converge to the requested title and body", created.issue);
9446
- if (amendmentContext !== undefined) {
9447
- if (created.issue.state !== "open")
9448
- return result(operation.op, "ambiguous_remote_state", `new pending Task ${operation.task_id} (Issue #${created.issue.number}) is not open after creation`, created.issue);
9449
- return updatePendingChild(root, gh, source, operation, created.issue, undefined, blockers, amendmentContext.pendingContent.get(operation.task_id), amendmentContext);
9450
- }
9451
9780
  child = created.issue;
9452
9781
  createdChild = true;
9453
9782
  } else {
@@ -9455,29 +9784,11 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
9455
9784
  if (owned.kind !== "found")
9456
9785
  return result(operation.op, "ambiguous_remote_state", owned.kind === "ambiguous" ? owned.message : "Task Issue ownership changed during publication", found.issue);
9457
9786
  child = owned.issue;
9458
- if (pendingBinding !== null) {
9459
- if (child.state !== "open")
9460
- return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} is closed and cannot be amended as pending work`, found.issue);
9461
- if (pendingBinding !== undefined && pendingBinding.issue_number !== child.number)
9462
- return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} is bound to Issue #${pendingBinding.issue_number} but observed Issue #${child.number}`, found.issue);
9463
- const approvedFinal = amendmentContext?.pendingContent.get(operation.task_id);
9464
- const isFinal = approvedFinal !== undefined && carriesApprovedContent(found.issue.title, found.issue.body, approvedFinal);
9465
- if (!isFinal) {
9466
- const baselineMatches = pendingBinding !== undefined && pendingBinding.title === found.issue.title && carriesApprovedContent(found.issue.title, found.issue.body, pendingBinding);
9467
- if (!baselineMatches)
9468
- return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} changed since the approved amendment baseline`, found.issue);
9469
- }
9470
- return updatePendingChild(root, gh, source, operation, child, pendingBinding?.issue_number, blockers, approvedFinal, amendmentContext);
9471
- }
9472
- if (amendmentContext !== undefined) {
9473
- const approvedFinal = amendmentContext.pendingContent.get(operation.task_id);
9474
- const resumable = approvedFinal !== undefined && child.state === "open" && carriesApprovedContent(found.issue.title, found.issue.body, approvedFinal);
9475
- if (!resumable)
9476
- return result(operation.op, "ambiguous_remote_state", `new pending Task ${operation.task_id} is unbound but Issue #${child.number} already exists with divergent content; bind it to amend`, found.issue);
9477
- return updatePendingChild(root, gh, source, operation, child, undefined, blockers, approvedFinal, amendmentContext);
9478
- }
9479
- if (found.issue.body !== body || found.issue.title !== title)
9787
+ if (!sameTrackedBody(found.issue.body, body) || found.issue.title !== title)
9480
9788
  return result(operation.op, "permanent_failure", "Task Issue already exists with a different title or Agent Brief; edit the GitHub source or retry the original projection before changing native relations", found.issue);
9789
+ const intentMismatch = publishedIntentMismatch(found.issue.body, operation.task_id, operation.intent_hash);
9790
+ if (intentMismatch)
9791
+ return result(operation.op, "ambiguous_remote_state", intentMismatch, found.issue);
9481
9792
  const observedLabelArgs = labelMutationArgs(child.labels, desiredTaskLabels(operation));
9482
9793
  if (observedLabelArgs.length) {
9483
9794
  const edited = await gh.run([
@@ -9513,8 +9824,11 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
9513
9824
  if ("contract" in finalSource)
9514
9825
  return finalSource;
9515
9826
  const finalChild = ownedTaskLookup(finalSource.issues, finalSource.repository.id, operation.task_id, operation.initiative_id, operation.slice_id);
9516
- if (finalChild.kind !== "found" || finalChild.issue.id !== child.id || finalChild.issue.title !== title || finalChild.issue.body !== body)
9827
+ if (finalChild.kind !== "found" || finalChild.issue.id !== child.id || finalChild.issue.title !== title || !sameTrackedBody(finalChild.issue.body, body))
9517
9828
  return result(operation.op, "ambiguous_remote_state", "Task Issue changed identity, title, or body during dependency publication", child);
9829
+ const finalIdentityMismatch = publishedIntentMismatch(finalChild.issue.body, operation.task_id, operation.intent_hash);
9830
+ if (finalIdentityMismatch)
9831
+ return result(operation.op, "ambiguous_remote_state", finalIdentityMismatch, child);
9518
9832
  const finalChildOwnership = await confirmTerminalOwnership(root, gh, operation.op, finalSource, finalChild.issue);
9519
9833
  if (!("owned" in finalChildOwnership))
9520
9834
  return finalChildOwnership;
@@ -9576,7 +9890,7 @@ async function closeTerminalIssue(root, gh, operation, source, issue, lookup) {
9576
9890
  return result(operation.op, "updated", "terminal Task Issue closure confirmed", found.issue);
9577
9891
  return close.exit_code !== 0 ? ghFailure(operation.op, close, "terminal Issue closure failed") : result(operation.op, "retryable_failure", "terminal Issue closure did not converge", found.issue);
9578
9892
  }
9579
- async function markTerminal(root, gh, operation, source) {
9893
+ async function markChildTerminal(root, gh, operation, source) {
9580
9894
  const lookup = (issues) => taskLookup(issues, source.repository.id, operation.task_id);
9581
9895
  const found = lookup(source.issues);
9582
9896
  if (found.kind === "missing")
@@ -9626,6 +9940,69 @@ async function markTerminal(root, gh, operation, source) {
9626
9940
  return refreshedOwnership;
9627
9941
  return closeTerminalIssue(root, gh, operation, refreshed, reread.issue, lookup);
9628
9942
  }
9943
+ async function closeParentWhenComplete(root, gh, operation, source, childResult) {
9944
+ const child = taskLookup(source.issues, source.repository.id, operation.task_id);
9945
+ if (child.kind !== "found")
9946
+ return childResult;
9947
+ const initiativeId = ownershipMarkerValue(child.issue.body, "initiative-id");
9948
+ if (!initiativeId)
9949
+ return childResult;
9950
+ const parentLookup = initiativeLookup(source.issues, source.repository.id, initiativeId);
9951
+ if (parentLookup.kind !== "found")
9952
+ return childResult;
9953
+ const parentNumber = parentLookup.issue.number;
9954
+ const retry = (message, status = "retryable_failure") => result(operation.op, status, `Parent #${parentNumber}: ${message}; the Task closure is unchanged, retry the same mark-terminal`, child.issue);
9955
+ const endpoint = `repos/${source.repository.name_with_owner}/issues/${parentNumber}`;
9956
+ const readParent = async () => {
9957
+ const read = await gh.run(["api", endpoint], { cwd: root });
9958
+ if (read.exit_code !== 0 || read.output_exceeded) {
9959
+ const failed = ghFailure(operation.op, read, "cannot read the Initiative Parent");
9960
+ return retry(failed.message, failed.status);
9961
+ }
9962
+ try {
9963
+ const [issue] = parseIssues(`[${read.stdout}]`);
9964
+ return issue ?? retry("gh returned a malformed Issue", "permanent_failure");
9965
+ } catch (error) {
9966
+ return retry(error instanceof Error ? error.message : String(error), "permanent_failure");
9967
+ }
9968
+ };
9969
+ const parent = await readParent();
9970
+ if ("contract" in parent)
9971
+ return parent;
9972
+ if (parent.state === "closed")
9973
+ return childResult;
9974
+ const listed = await gh.run(["api", "--paginate", "--slurp", `${endpoint}/sub_issues?per_page=100`], { cwd: root });
9975
+ if (listed.exit_code !== 0 || listed.output_exceeded) {
9976
+ const failed = ghFailure(operation.op, listed, "cannot read the Initiative Sub-issues");
9977
+ return retry(failed.message, failed.status);
9978
+ }
9979
+ let children;
9980
+ try {
9981
+ children = parseIssues(listed.stdout);
9982
+ } catch (error) {
9983
+ return retry(error instanceof Error ? error.message : String(error), "permanent_failure");
9984
+ }
9985
+ const sliceChildren = children.filter((candidate) => candidate.body.includes(marker("initiative-id", initiativeId)) && /<!-- immune-brain:slice-id=[^>]+ -->/.test(candidate.body));
9986
+ if (sliceChildren.length === 0 || !sliceChildren.every((candidate) => candidate.state === "closed" && candidate.state_reason === "completed"))
9987
+ return childResult;
9988
+ const close = await gh.run(["issue", "close", String(parentNumber), "--repo", source.repository.name_with_owner, "--reason", "completed"], { cwd: root });
9989
+ const confirmed = await readParent();
9990
+ if ("contract" in confirmed)
9991
+ return confirmed;
9992
+ if (confirmed.state === "closed" && confirmed.state_reason === "completed")
9993
+ return result(operation.op, "updated", `${childResult.message}; Initiative Parent #${parentNumber} closed as completed`, child.issue);
9994
+ if (close.exit_code !== 0 || close.output_exceeded) {
9995
+ const failed = ghFailure(operation.op, close, "Initiative Parent closure failed");
9996
+ return retry(failed.message, failed.status);
9997
+ }
9998
+ return retry("closure did not converge");
9999
+ }
10000
+ async function markTerminal(root, gh, operation, source) {
10001
+ const terminal = await markChildTerminal(root, gh, operation, source);
10002
+ if (operation.phase !== "done" || terminal.status !== "updated" && terminal.status !== "already_current" || !terminal.association_found)
10003
+ return terminal;
10004
+ return closeParentWhenComplete(root, gh, operation, source, terminal);
10005
+ }
9629
10006
  function normalizedList(value, name, max = 2000) {
9630
10007
  if (value === undefined)
9631
10008
  return;
@@ -9766,756 +10143,116 @@ async function runGithubTrackerOperation(root, input, gh = createGhTransport())
9766
10143
  return markTerminal(absoluteRoot, gh, operation, source);
9767
10144
  }
9768
10145
  }
9769
- function issueTerminalEventId(body) {
9770
- const suffixMatch = [...body.matchAll(/<!-- immune-brain:terminal-event=([A-Za-z0-9._:-]+) -->/g)];
9771
- if (suffixMatch.length === 0)
9772
- return null;
9773
- if (suffixMatch.length > 1)
9774
- return "multiple";
9775
- const eventId = suffixMatch[0][1];
9776
- if (eventId.length > MAX_TERMINAL_EVENT_ID || !body.endsWith(terminalSuffix(eventId)))
9777
- return "malformed";
9778
- return eventId;
9779
- }
9780
- function stripTerminalSuffixFromBytes(body) {
9781
- if (body === undefined)
9782
- return null;
9783
- const eventId = issueTerminalEventId(body);
9784
- if (eventId === null || eventId === "multiple" || eventId === "malformed")
9785
- return eventId;
9786
- const suffix = terminalSuffix(eventId);
9787
- if (!body.endsWith(suffix))
9788
- return null;
9789
- return body.slice(0, body.length - suffix.length);
9790
- }
9791
- function carriesApprovedContent(title, body, approved) {
9792
- if (title !== approved.title)
9793
- return false;
9794
- if (body === approved.body)
9795
- return true;
9796
- const stripped = stripTerminalSuffixFromBytes(body);
9797
- return typeof stripped === "string" && stripped.trimEnd() === approved.body.trimEnd();
9798
- }
9799
- async function updatePendingChild(root, gh, source, op, child, boundNumber, desiredBlockers, approvedFinal, amendmentContext = undefined) {
9800
- const parent = initiativeLookup(source.issues, source.repository.id, op.initiative_id);
9801
- const body = childBody(source.repository, op, parent.kind === "found" ? parent.issue : child);
9802
- const oversized = bodyLimitFailure(op.op, body, MAX_TERMINAL_SUFFIX_BYTES);
9803
- if (oversized)
9804
- return oversized;
9805
- const title = taskIssueTitle(op, sliceOrdinalFromChecklist(parent.kind === "found" ? parent.issue.body : child.body, op.slice_id, op.projection?.slice_ordinal ?? 1));
9806
- const parentApprovedContent = amendmentContext?.parent;
9807
- const parentBoundNumber = amendmentContext?.parentIssueNumber;
9808
- const baseBody = child.body;
9809
- const suffixEvent = issueTerminalEventId(baseBody);
9810
- if (suffixEvent === "multiple")
9811
- return result(op.op, "ambiguous_remote_state", "pending Task has multiple terminal markers", child);
9812
- if (suffixEvent === "malformed")
9813
- return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a malformed terminal marker (marker without its exact canonical suffix)`, child);
9814
- let finalBody = suffixEvent !== null ? `${body.trimEnd()}${terminalSuffix(suffixEvent)}` : body;
9815
- const approvedMatches = approvedFinal !== undefined && carriesApprovedContent(title, body, approvedFinal);
9816
- if (!approvedMatches)
9817
- return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a terminal suffix that diverges from the approved amendment content`, child);
9818
- const approvedNow = approvedFinal !== undefined && carriesApprovedContent(child.title, child.body, approvedFinal);
9819
- if (approvedNow) {
9820
- const currentParent = initiativeLookup(source.issues, source.repository.id, op.initiative_id);
9821
- if (currentParent.kind !== "found")
9822
- return result(op.op, "ambiguous_remote_state", "pending Task Parent is not observable before attachment convergence", child);
9823
- const revalidated = await revalidatePendingChildBeforeWrite(root, gh, child.number, op.task_id, approvedFinal, undefined, true, parentApprovedContent, parentBoundNumber);
9824
- if ("contract" in revalidated)
9825
- return revalidated;
9826
- const targetParentNumber = parentBoundNumber ?? currentParent.issue.number;
9827
- const attachment = await confirmAttachment(root, gh, op.op, source.repository, targetParentNumber, child.number);
9828
- if (!("attached" in attachment))
9829
- return attachment;
9830
- if (!attachment.attached) {
9831
- const attached = await attachSubIssue(root, gh, op.op, source.repository, targetParentNumber, child);
9832
- if (!("attached" in attached))
9833
- return attached;
9834
- }
9835
- }
9836
- let observed = child;
9837
- const desiredLabels = desiredTaskLabels(op);
9838
- if (child.title !== title || child.body !== finalBody || labelMutationArgs(child.labels, desiredLabels).length) {
9839
- const revalidated = await revalidatePendingChildBeforeWrite(root, gh, child.number, op.task_id, approvedFinal, { title: child.title, body: child.body }, false, parentApprovedContent, parentBoundNumber);
9840
- if ("contract" in revalidated)
9841
- return revalidated;
9842
- observed = revalidated;
9843
- const observedEvent = issueTerminalEventId(observed.body);
9844
- if (observedEvent === "malformed")
9845
- return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a malformed terminal marker (marker without its exact canonical suffix)`, child);
9846
- const writeBody = typeof observedEvent === "string" ? `${body.trimEnd()}${terminalSuffix(observedEvent)}` : finalBody;
9847
- const labelArgs = labelMutationArgs(observed.labels, desiredLabels);
9848
- if (observed.title !== title || observed.body !== writeBody || labelArgs.length) {
9849
- const edited = await gh.run([
9850
- "issue",
9851
- "edit",
9852
- String(child.number),
9853
- "--repo",
9854
- source.repository.name_with_owner,
9855
- "--title",
9856
- title,
9857
- "--body-file",
9858
- "-",
9859
- ...labelArgs
9860
- ], { cwd: root, stdin: writeBody });
9861
- if (edited.exit_code !== 0 || edited.output_exceeded)
9862
- return ghFailure(op.op, edited, `pending Task Issue #${child.number} update failed`);
9863
- finalBody = writeBody;
9864
- }
9865
- }
9866
- const dependencies = await convergePendingDependencies(root, gh, source, child.number, op.task_id, desiredBlockers, approvedFinal, parentApprovedContent, parentBoundNumber);
9867
- if (!("complete" in dependencies))
9868
- return dependencies;
9869
- const refreshed = await snapshot(root, gh, op.op);
9870
- if ("contract" in refreshed)
9871
- return refreshed;
9872
- const reread = ownedTaskLookup(refreshed.issues, refreshed.repository.id, op.task_id, op.initiative_id, op.slice_id);
9873
- if (reread.kind !== "found" || reread.issue.number !== child.number)
9874
- return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} changed identity during amendment`, child);
9875
- if (reread.issue.title !== title || reread.issue.body !== finalBody)
9876
- return result(op.op, "retryable_failure", `pending Task ${op.task_id} update did not converge`, reread.issue);
9877
- const labelsCurrent = !desiredLabels.some((label) => !reread.issue.labels.includes(label));
9878
- if (!labelsCurrent)
9879
- return result(op.op, "retryable_failure", `pending Task ${op.task_id} labels did not converge`, reread.issue);
9880
- const currentDependencies = await confirmBlockedBy(root, gh, op.op, refreshed.repository, reread.issue.number, desiredBlockers);
9881
- if (!("complete" in currentDependencies))
9882
- return currentDependencies;
9883
- if (!currentDependencies.complete)
9884
- return result(op.op, "retryable_failure", `pending Task ${op.task_id} dependencies did not converge`, reread.issue);
9885
- const contentCurrent = child.title === title && child.body === finalBody && labelMutationArgs(child.labels, desiredLabels).length === 0;
9886
- return contentCurrent ? result(op.op, "already_current", `pending Task ${op.task_id} already carries the approved amendment content`, reread.issue) : result(op.op, "updated", `pending Task ${op.task_id} Agent Brief updated with approved amendment content`, reread.issue);
9887
- }
9888
- async function convergePendingDependencies(root, gh, source, childNumber, childTaskId, requestedBlockers, approvedFinal, parentApprovedContent = undefined, parentBoundNumber = undefined) {
9889
- const expected = requestedBlockers.map((blocker) => blocker.id);
9890
- const existing = await readBlockedByIds(root, gh, "upsert-task", source.repository, childNumber);
9891
- if (!Array.isArray(existing))
9892
- return existing;
9893
- const removed = existing.filter((id) => !expected.includes(id));
9894
- for (const id of removed) {
9895
- const revalidated = await revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, undefined, false, parentApprovedContent, parentBoundNumber);
9896
- if ("contract" in revalidated)
9897
- return revalidated;
9898
- const mutation = await gh.run([
9899
- "api",
9900
- "--method",
9901
- "DELETE",
9902
- `repos/${source.repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by/${id}`
9903
- ], { cwd: root });
9904
- if (mutation.exit_code !== 0 || mutation.output_exceeded)
9905
- return ghFailure("upsert-task", mutation, `native blocked_by removal failed for Issue #${childNumber}`);
9906
- }
9907
- const additions = requestedBlockers.filter((blocker) => !existing.includes(blocker.id));
9908
- for (const blocker of additions) {
9909
- const revalidated = await revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, undefined, false, parentApprovedContent, parentBoundNumber);
9910
- if ("contract" in revalidated)
9911
- return revalidated;
9912
- const mutation = await gh.run([
9913
- "api",
9914
- "-F",
9915
- `issue_id=${blocker.id}`,
9916
- `repos/${source.repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by`
9917
- ], { cwd: root });
9918
- if (mutation.exit_code !== 0 || mutation.output_exceeded)
9919
- return ghFailure("upsert-task", mutation, `native blocked_by attachment failed for Issue #${blocker.number}`);
9920
- }
9921
- const confirm = await confirmBlockedBy(root, gh, "upsert-task", source.repository, childNumber, requestedBlockers);
9922
- if (!("complete" in confirm))
9923
- return confirm;
9924
- return confirm.complete ? { complete: true } : { complete: true };
9925
- }
9926
- async function revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, allowedBaseline = undefined, skipAttachmentCheck = false, parentApproved = undefined, parentExpectedNumber = undefined) {
9927
- const refreshed = await snapshot(root, gh, "upsert-task");
9928
- if ("contract" in refreshed)
9929
- return refreshed;
9930
- const resolved = taskLookup(refreshed.issues, refreshed.repository.id, childTaskId);
9931
- if (resolved.kind !== "found")
9932
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} is not uniquely owned before a write: ${resolved.kind}`);
9933
- const child = resolved.issue;
9934
- if (child.number !== childNumber)
9935
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} resolves to Issue #${child.number}, not the bound Issue #${childNumber}`, child);
9936
- if (child.state !== "open")
9937
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) is no longer open before a dependency write`, child);
9938
- const contentFinal = approvedFinal !== undefined && carriesApprovedContent(child.title, child.body, approvedFinal);
9939
- const contentBaseline = allowedBaseline !== undefined && child.title === allowedBaseline.title && child.body === allowedBaseline.body;
9940
- if (!contentFinal && !contentBaseline)
9941
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) content is no longer the approved final bytes before a dependency write`, child);
9942
- const initiativeId = [...child.body.matchAll(/<!-- immune-brain:initiative-id=([A-Za-z0-9._:-]+) -->/g)].map((match) => match[1])[0];
9943
- if (!initiativeId)
9944
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its Initiative marker before a dependency write`, child);
9945
- const parent = initiativeLookup(refreshed.issues, refreshed.repository.id, initiativeId);
9946
- if (parent.kind !== "found" || parent.issue.state !== "open")
9947
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its open Parent before a dependency write`, child);
9948
- if (parentExpectedNumber !== undefined && parent.issue.number !== parentExpectedNumber)
9949
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) Parent resolves to Issue #${parent.issue.number}, not the bound Parent Issue #${parentExpectedNumber} before a dependency write`, child);
9950
- if (parentApproved !== undefined && (parent.issue.title !== parentApproved.title || parent.issue.body !== parentApproved.body))
9951
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) Parent changed since the approved amendment content before a dependency write`, child);
9952
- if (!skipAttachmentCheck) {
9953
- const attachedNow = await readSubIssueNumbers(root, gh, "upsert-task", refreshed.repository, parent.issue.number);
9954
- if (!Array.isArray(attachedNow))
9955
- return attachedNow;
9956
- if (!attachedNow.includes(childNumber))
9957
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) is no longer attached to the Parent before a dependency write`, child);
9958
- }
9959
- const sliceId = ownershipMarkerValue(child.body, "slice-id");
9960
- if (sliceId && sliceCount(parent.issue.body, sliceId) !== 1)
9961
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its exact Slice in the Parent before a dependency write`, child);
9962
- return child;
9963
- }
9964
10146
 
9965
10147
  // plugins/immune-brain/runtime/unattended/batch_preflight.ts
9966
10148
  import { existsSync as existsSync7, readdirSync as readdirSync6, readFileSync as readFileSync13 } from "node:fs";
9967
10149
  import { randomUUID as randomUUID7 } from "node:crypto";
9968
10150
  import { join as join12 } from "node:path";
9969
- import { spawnSync as spawnSync5 } from "node:child_process";
10151
+ import { spawnSync as spawnSync7 } from "node:child_process";
9970
10152
 
9971
- // plugins/immune-brain/runtime/kernel/batch_authority.ts
9972
- import { createHash as createHash18 } from "node:crypto";
9973
- var BATCH_AUTHORITY_CAPABILITY_BRAND = Symbol.for("assurance-kernel.batch-authority-capability-brand");
9974
- var GIT_COMMIT_ID2 = /^[a-f0-9]{40}$/;
10153
+ // plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts
10154
+ import { spawnSync as spawnSync6 } from "node:child_process";
9975
10155
 
9976
- class BatchAuthorizationExpiryError extends Error {
9977
- constructor(message) {
9978
- super(message);
9979
- this.name = "BatchAuthorizationExpiryError";
9980
- }
9981
- }
9982
- function sha256Hex3(bytes) {
9983
- return createHash18("sha256").update(bytes).digest("hex");
10156
+ // plugins/immune-brain/runtime/unattended/batch_state.ts
10157
+ import { existsSync as existsSync6, 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";
10158
+ import { randomUUID as randomUUID6 } from "node:crypto";
10159
+ import { dirname as dirname7, join as join11 } from "node:path";
10160
+ var BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10161
+ var CHILD_RUN_STATES = new Set([
10162
+ "pending",
10163
+ "enrolled",
10164
+ "settled",
10165
+ "committed",
10166
+ "needs_human",
10167
+ "skipped_blocked"
10168
+ ]);
10169
+ var BATCH_RUN_STATES = new Set([
10170
+ "prepared",
10171
+ "running",
10172
+ "needs_human",
10173
+ "completed",
10174
+ "budget_stopped",
10175
+ "failed",
10176
+ "rejected"
10177
+ ]);
10178
+ var ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
10179
+ function isCanonicalTimestamp(value) {
10180
+ if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN.test(value))
10181
+ return false;
10182
+ const milliseconds = Date.parse(value);
10183
+ return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === value;
9984
10184
  }
9985
- function stableStringify3(value) {
9986
- if (value === null || typeof value !== "object")
9987
- return JSON.stringify(value);
9988
- if (Array.isArray(value))
9989
- return `[${value.map((entry) => stableStringify3(entry)).join(",")}]`;
9990
- const record = value;
9991
- return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify3(record[key])}`).join(",")}}`;
10185
+ function validateBatchId(batchId) {
10186
+ if (typeof batchId !== "string" || !BATCH_ID_PATTERN.test(batchId))
10187
+ throw new Error("batch id is not a safe file identity");
9992
10188
  }
9993
- function computeBatchPlanDigest(children) {
9994
- const canonical = children.map((child) => ({
9995
- blocked_by: [...child.blocked_by],
9996
- intent_content_hash: child.intent_content_hash,
9997
- intent_path: child.intent_path,
9998
- intent_revision: child.intent_revision,
9999
- task_id: child.task_id
10000
- }));
10001
- return `sha256:${sha256Hex3(stableStringify3(canonical))}`;
10189
+ function statePath(batchId) {
10190
+ validateBatchId(batchId);
10191
+ return join11(".imm", "state", "batches", `${batchId}.json`);
10002
10192
  }
10003
- function requireNonEmpty(binding) {
10004
- const missing = [];
10005
- for (const key of [
10006
- "batch_id",
10007
- "initiative_slug",
10008
- "plan_digest",
10009
- "branch",
10010
- "base_head",
10011
- "actor_id",
10012
- "confirmation_ref",
10013
- "expires_at",
10014
- "nonce"
10015
- ]) {
10016
- const value = binding[key];
10017
- if (value === undefined || value === null || value === "")
10018
- missing.push(key);
10019
- }
10020
- if (!binding.budget || typeof binding.budget !== "object")
10021
- missing.push("budget");
10022
- if (missing.length > 0)
10023
- throw new Error(`batch authorization binding is incomplete: ${missing.join(", ")}`);
10193
+ function withoutRetiredClock(record) {
10194
+ const { authorization_expires_at: _expiry, ...rest } = record;
10195
+ const { deadline_at: _deadline, ...budget } = rest.budget;
10196
+ return { ...rest, budget };
10024
10197
  }
10025
- function validateBudget(budget, issuedAt) {
10026
- if (!Number.isInteger(budget.max_children) || budget.max_children <= 0)
10027
- throw new Error("batch budget max_children must be a positive integer");
10028
- if (!Number.isInteger(budget.qa_failure_limit) || budget.qa_failure_limit <= 0)
10029
- throw new Error("batch budget qa_failure_limit must be a positive integer");
10030
- const deadline = Date.parse(budget.deadline_at);
10031
- if (Number.isNaN(deadline) || deadline <= Date.parse(issuedAt))
10032
- throw new Error("batch budget must have a future deadline_at");
10198
+ function canonicalBytes(record) {
10199
+ return `${JSON.stringify(withoutRetiredClock(record), null, 2)}
10200
+ `;
10033
10201
  }
10034
- function validateChildren(children, planDigest) {
10035
- if (!Array.isArray(children) || children.length === 0)
10036
- throw new Error("batch authorization requires a non-empty child plan");
10037
- const seen = new Set;
10038
- for (const child of children) {
10039
- if (!child || typeof child !== "object")
10040
- throw new Error("batch plan child must be an object");
10041
- for (const key of ["task_id", "intent_path", "intent_content_hash"]) {
10042
- if (typeof child[key] !== "string" || child[key] === "")
10043
- throw new Error(`batch plan child ${key} must be a non-empty string`);
10044
- }
10045
- if (!Number.isInteger(child.intent_revision) || child.intent_revision <= 0)
10046
- throw new Error("batch plan child intent_revision must be a positive integer");
10047
- if (!Array.isArray(child.blocked_by) || child.blocked_by.some((id) => typeof id !== "string" || id === ""))
10048
- throw new Error("batch plan child blocked_by must be an array of task ids");
10049
- if (seen.has(child.task_id))
10050
- throw new Error(`batch plan child ${child.task_id} appears more than once`);
10051
- seen.add(child.task_id);
10202
+ function validateRecordShape(value, batchId) {
10203
+ if (typeof value !== "object" || value === null)
10204
+ throw new Error(`batch run state ${batchId} is not an object`);
10205
+ const record = value;
10206
+ if (record.contract !== "assurance_kernel/batch_run_state/v1")
10207
+ throw new Error(`batch run state ${batchId} has an unknown contract`);
10208
+ if (record.batch_id !== batchId)
10209
+ throw new Error(`batch run state ${batchId} carries batch_id ${String(record.batch_id)}`);
10210
+ if (typeof record.plan_digest !== "string" || !record.plan_digest)
10211
+ throw new Error(`batch run state ${batchId} has an invalid plan_digest`);
10212
+ if (typeof record.base_head !== "string" || !record.base_head)
10213
+ throw new Error(`batch run state ${batchId} has an invalid base_head`);
10214
+ if (record.branch !== undefined && (typeof record.branch !== "string" || !record.branch))
10215
+ throw new Error(`batch run state ${batchId} has an invalid branch`);
10216
+ if (!isCanonicalTimestamp(record.confirmation_time))
10217
+ throw new Error(`batch run state ${batchId} has an invalid confirmation_time`);
10218
+ if (!isCanonicalTimestamp(record.created_at) || !isCanonicalTimestamp(record.updated_at))
10219
+ throw new Error(`batch run state ${batchId} has invalid state timestamps`);
10220
+ if (!Array.isArray(record.children) || record.children.length === 0)
10221
+ throw new Error(`batch run state ${batchId} has no children`);
10222
+ if (!BATCH_RUN_STATES.has(String(record.batch_state)))
10223
+ throw new Error(`batch run state ${batchId} has an invalid batch_state`);
10224
+ if (typeof record.consecutive_qa_failures !== "number" || !Number.isInteger(record.consecutive_qa_failures) || record.consecutive_qa_failures < 0)
10225
+ throw new Error(`batch run state ${batchId} has an invalid consecutive_qa_failures`);
10226
+ const budget = record.budget;
10227
+ 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)
10228
+ throw new Error(`batch run state ${batchId} has an invalid budget`);
10229
+ if (!Array.isArray(record.commits) || record.commits.some((c) => typeof c !== "string"))
10230
+ throw new Error(`batch run state ${batchId} has an invalid commits list`);
10231
+ 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)))
10232
+ throw new Error(`batch run state ${batchId} has an invalid adopted_heads list`);
10233
+ const seenTaskIds = new Set;
10234
+ for (const child of record.children) {
10235
+ if (typeof child !== "object" || child === null || typeof child.task_id !== "string" || !child.task_id || typeof child.slice_id !== "string" || !child.slice_id)
10236
+ throw new Error(`batch run state ${batchId} has an invalid child entry`);
10237
+ if (!CHILD_RUN_STATES.has(String(child.state)))
10238
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid state`);
10239
+ if (!Array.isArray(child.blocked_by) || child.blocked_by.some((b) => typeof b !== "string"))
10240
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid blocked_by`);
10241
+ if (child.reason !== null && typeof child.reason !== "string" || child.commit !== null && typeof child.commit !== "string")
10242
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has invalid terminal fields`);
10243
+ if (child.state === "committed" && child.commit === null)
10244
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is committed without a commit`);
10245
+ if (child.state !== "committed" && child.commit !== null)
10246
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has a commit but is not committed`);
10247
+ if ((child.state === "needs_human" || child.state === "skipped_blocked") && child.reason === null)
10248
+ throw new Error(`batch run state ${batchId} child ${child.task_id} needs a terminal reason`);
10249
+ if (seenTaskIds.has(child.task_id))
10250
+ throw new Error(`batch run state ${batchId} has a duplicate child ${child.task_id}`);
10251
+ seenTaskIds.add(child.task_id);
10052
10252
  }
10053
- for (const child of children) {
10054
- for (const blocker of child.blocked_by) {
10055
- if (!seen.has(blocker))
10056
- throw new Error(`batch plan child ${child.task_id} is blocked by ${blocker}, which is not in the confirmed plan`);
10057
- }
10058
- }
10059
- const dependencies = new Map(children.map((child) => [child.task_id, child.blocked_by]));
10060
- const visiting = new Set;
10061
- const visited = new Set;
10062
- function visit(taskId) {
10063
- if (visiting.has(taskId))
10064
- throw new Error(`batch plan dependency cycle at ${taskId}`);
10065
- if (visited.has(taskId))
10066
- return;
10067
- visiting.add(taskId);
10068
- for (const blocker of dependencies.get(taskId))
10069
- visit(blocker);
10070
- visiting.delete(taskId);
10071
- visited.add(taskId);
10072
- }
10073
- for (const child of children)
10074
- visit(child.task_id);
10075
- if (computeBatchPlanDigest(children) !== planDigest)
10076
- throw new Error("batch plan digest does not match the confirmed child plan");
10077
- }
10078
- function createBatchAuthorityRegistry() {
10079
- const inner = createCapabilityRegistry(BATCH_AUTHORITY_CAPABILITY_BRAND, {
10080
- validateBinding(binding, issuedAt) {
10081
- requireNonEmpty(binding);
10082
- if (!isLiteralUserActor(binding.actor_id))
10083
- throw new Error("batch authorization requires a literal-user actor_id");
10084
- if (!GIT_COMMIT_ID2.test(binding.base_head))
10085
- throw new Error("batch authorization base_head must be a committed 40-hex commit id");
10086
- const expires = Date.parse(binding.expires_at);
10087
- if (Number.isNaN(expires) || expires <= Date.parse(issuedAt))
10088
- throw new Error("batch authorization must have a future expiry");
10089
- validateBudget(binding.budget, issuedAt);
10090
- },
10091
- validateAndProject(state, expected, now) {
10092
- if (!Number.isFinite(now))
10093
- throw new Error("batch authorization requires a valid clock");
10094
- const expires = Date.parse(state.expires_at);
10095
- if (Number.isNaN(expires) || expires <= now)
10096
- throw new BatchAuthorizationExpiryError("batch authorization has expired");
10097
- if (Date.parse(state.budget.deadline_at) <= now)
10098
- throw new BatchAuthorizationExpiryError("batch authorization deadline has expired");
10099
- for (const key of Object.keys(expected)) {
10100
- if (key === "budget") {
10101
- const a = state.budget ?? {};
10102
- const b = expected.budget ?? {};
10103
- if (a.max_children !== b.max_children || a.deadline_at !== b.deadline_at || a.qa_failure_limit !== b.qa_failure_limit)
10104
- throw new Error("batch authorization budget mismatch");
10105
- continue;
10106
- }
10107
- if (state[key] !== expected[key])
10108
- throw new Error(`batch authorization ${key} mismatch`);
10109
- }
10110
- return {
10111
- batch_id: state.batch_id,
10112
- initiative_slug: state.initiative_slug,
10113
- plan_digest: state.plan_digest,
10114
- branch: state.branch,
10115
- base_head: state.base_head,
10116
- budget: { ...state.budget },
10117
- actor_id: state.actor_id,
10118
- confirmation_ref: state.confirmation_ref,
10119
- issued_at: state.issued_at,
10120
- expires_at: state.expires_at,
10121
- nonce: state.nonce
10122
- };
10123
- }
10124
- }, "batch authorization");
10125
- const plans = new WeakMap;
10126
- const consumed = new WeakMap;
10127
- function planOf(capability) {
10128
- const plan = plans.get(capability);
10129
- if (!plan)
10130
- throw new Error("batch authorization capability is not recognized by this registry");
10131
- return plan;
10132
- }
10133
- function slotsOf(capability) {
10134
- const slots = consumed.get(capability);
10135
- if (!slots)
10136
- throw new Error("batch authorization capability is not recognized by this registry");
10137
- return slots;
10138
- }
10139
- return {
10140
- brand: inner.brand,
10141
- issue(binding, children, issuedAt = new Date().toISOString()) {
10142
- requireNonEmpty(binding);
10143
- validateChildren(children, binding.plan_digest);
10144
- const capability = inner.issue(binding, issuedAt);
10145
- plans.set(capability, children.map((child) => ({ ...child, blocked_by: [...child.blocked_by] })));
10146
- consumed.set(capability, new Set);
10147
- return capability;
10148
- },
10149
- inspect(capability, expected, now = Date.now()) {
10150
- planOf(capability);
10151
- return inner.inspect(capability, expected, now);
10152
- },
10153
- children(capability) {
10154
- return planOf(capability).map((child) => ({ ...child, blocked_by: [...child.blocked_by] }));
10155
- },
10156
- consumedChildren(capability) {
10157
- return [...slotsOf(capability)];
10158
- },
10159
- isChildConsumed(capability, taskId) {
10160
- return slotsOf(capability).has(taskId);
10161
- },
10162
- consumeChild(capability, expected, taskId, now = Date.now()) {
10163
- const validated = this.inspect(capability, expected, now);
10164
- const plan = planOf(capability);
10165
- if (!plan.some((child) => child.task_id === taskId))
10166
- throw new Error(`batch_child_not_in_plan: ${taskId}`);
10167
- const slots = slotsOf(capability);
10168
- if (slots.has(taskId))
10169
- throw new Error(`batch_child_slot_consumed: ${taskId}`);
10170
- slots.add(taskId);
10171
- return validated;
10172
- },
10173
- releaseChild(capability, taskId) {
10174
- slotsOf(capability).delete(taskId);
10175
- },
10176
- isExhausted(capability) {
10177
- return slotsOf(capability).size >= planOf(capability).length;
10178
- }
10179
- };
10180
- }
10181
- function deriveChildEnrollment(root, registry, input) {
10182
- const validated = registry.inspect(input.capability, input.binding, Date.parse(input.now));
10183
- const child = registry.children(input.capability).find((entry) => entry.task_id === input.task_id);
10184
- if (!child)
10185
- throw new Error(`batch_child_not_in_plan: ${input.task_id}`);
10186
- if (registry.isChildConsumed(input.capability, input.task_id))
10187
- throw new Error(`batch_child_slot_consumed: ${input.task_id}`);
10188
- if (!GIT_COMMIT_ID2.test(input.expected_head))
10189
- throw new Error("batch_head_lineage_broken: expected_head is not a commit id");
10190
- const preparation = preparePiCanary(root, { task_id: input.task_id, now: input.now });
10191
- if (!preparation.git_base_head)
10192
- throw new Error(preparation.git_error ?? "enrollment requires a committed Git HEAD");
10193
- if (preparation.git_base_head !== input.expected_head)
10194
- throw new Error(`batch_head_lineage_broken: expected ${input.expected_head}, found ${preparation.git_base_head}`);
10195
- if (!preparation.intent)
10196
- throw new Error(`batch_child_intent_changed: ${input.task_id} intent sidecar is unreadable`);
10197
- if (preparation.intent.path !== child.intent_path || preparation.intent.revision !== child.intent_revision || preparation.intent.content_hash !== child.intent_content_hash)
10198
- throw new Error(`batch_child_intent_changed: ${input.task_id}`);
10199
- if (registry.consumedChildren(input.capability).length === 0 && input.expected_head !== validated.base_head)
10200
- throw new Error(`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${validated.base_head}, not ${input.expected_head}`);
10201
- return {
10202
- child,
10203
- preparation,
10204
- binding: {
10205
- task_id: child.task_id,
10206
- intent_path: child.intent_path,
10207
- intent_revision: child.intent_revision,
10208
- intent_content_hash: child.intent_content_hash,
10209
- preparation_digest: preparation.digest,
10210
- actor_id: validated.actor_id,
10211
- confirmation_ref: validated.confirmation_ref,
10212
- expires_at: validated.expires_at,
10213
- nonce: `${validated.nonce}:${child.task_id}`
10214
- }
10215
- };
10216
- }
10217
-
10218
- // plugins/immune-brain/runtime/unattended/batch_plan.ts
10219
- import { createHash as createHash19 } from "node:crypto";
10220
- var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10221
- var DEFAULT_DEADLINE_MS = 8 * 60 * 60 * 1000;
10222
- var DEFAULT_QA_FAILURE_LIMIT = 2;
10223
- function specBindingReason(inspection) {
10224
- if (inspection.missing.length > 0)
10225
- return `spec_binding_incomplete: ${inspection.missing.join(", ")}`;
10226
- return inspection.code === "binding_ambiguous" ? "spec_binding_ambiguous" : "spec_binding_missing";
10227
- }
10228
- function compareIds(left, right) {
10229
- return left < right ? -1 : left > right ? 1 : 0;
10230
- }
10231
- function positiveSafeInteger(value, name) {
10232
- if (!Number.isSafeInteger(value) || value <= 0)
10233
- throw new Error(`${name} must be a positive safe integer`);
10234
- return value;
10235
- }
10236
- var ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
10237
- function timestamp(value, name) {
10238
- if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN.test(value))
10239
- throw new Error(`${name} must be an ISO timestamp`);
10240
- const milliseconds = Date.parse(value);
10241
- if (!Number.isFinite(milliseconds))
10242
- throw new Error(`${name} must be an ISO timestamp`);
10243
- return { milliseconds, iso: new Date(milliseconds).toISOString() };
10244
- }
10245
- function normalizeObservation(value, initiativeSlug) {
10246
- if (!value || value.contract !== "immune_brain/github_initiative_observation/v1")
10247
- throw new Error("tracker returned an invalid Initiative observation contract");
10248
- if (value.initiative_id !== initiativeSlug)
10249
- throw new Error("tracker returned an observation for another Initiative");
10250
- if (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0)
10251
- throw new Error("tracker returned an invalid Initiative Issue number");
10252
- if (!Array.isArray(value.tasks))
10253
- throw new Error("tracker returned an invalid Initiative Task list");
10254
- const taskIds = new Set;
10255
- const sliceIds = new Set;
10256
- const tasks = value.tasks.map((task, index) => {
10257
- if (!task || typeof task !== "object")
10258
- throw new Error(`tracker Task ${index} is invalid`);
10259
- if (!ID_PATTERN2.test(task.task_id))
10260
- throw new Error(`tracker Task ${index} has an invalid task_id`);
10261
- if (!ID_PATTERN2.test(task.slice_id))
10262
- throw new Error(`tracker Task ${index} has an invalid slice_id`);
10263
- if (!Number.isSafeInteger(task.issue_number) || task.issue_number <= 0)
10264
- throw new Error(`tracker Task ${task.task_id} has an invalid Issue number`);
10265
- if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN2.test(id)))
10266
- throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
10267
- if (taskIds.has(task.task_id))
10268
- throw new Error(`tracker returned duplicate Task ${task.task_id}`);
10269
- if (sliceIds.has(task.slice_id))
10270
- throw new Error(`tracker returned duplicate Slice ${task.slice_id}`);
10271
- taskIds.add(task.task_id);
10272
- sliceIds.add(task.slice_id);
10273
- const blockedBy = [...task.blocked_by].sort();
10274
- if (new Set(blockedBy).size !== blockedBy.length || blockedBy.includes(task.task_id))
10275
- throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
10276
- return { ...task, blocked_by: blockedBy };
10277
- });
10278
- for (const task of tasks) {
10279
- const unknown = task.blocked_by.find((id) => !taskIds.has(id));
10280
- if (unknown)
10281
- throw new Error(`tracker Task ${task.task_id} depends on unknown Task ${unknown}`);
10282
- }
10283
- return { ...value, tasks: tasks.sort((left, right) => compareIds(left.task_id, right.task_id)) };
10284
- }
10285
- function dependencyOrder(observation) {
10286
- const remaining = new Map(observation.tasks.map((task) => [task.task_id, task]));
10287
- const done = new Set;
10288
- const order = [];
10289
- while (remaining.size) {
10290
- const ready = [...remaining.values()].filter((task) => task.blocked_by.every((id) => done.has(id))).sort((left, right) => compareIds(left.task_id, right.task_id));
10291
- if (!ready.length)
10292
- throw new Error("Initiative Task dependencies must form an acyclic graph");
10293
- for (const task of ready) {
10294
- remaining.delete(task.task_id);
10295
- done.add(task.task_id);
10296
- order.push(task);
10297
- }
10298
- }
10299
- return order;
10300
- }
10301
- function dependencyClosures(order) {
10302
- const closures = new Map;
10303
- for (const task of order) {
10304
- const closure = new Set;
10305
- for (const dependency of task.blocked_by) {
10306
- closure.add(dependency);
10307
- for (const transitive of closures.get(dependency) ?? [])
10308
- closure.add(transitive);
10309
- }
10310
- closures.set(task.task_id, [...closure].sort());
10311
- }
10312
- return closures;
10313
- }
10314
- function budget(input, enrollableCount, confirmationTime) {
10315
- const maxChildren = input.budget?.max_children === undefined ? positiveSafeInteger(enrollableCount, "budget.max_children") : positiveSafeInteger(input.budget.max_children, "budget.max_children");
10316
- if (maxChildren > enrollableCount)
10317
- throw new Error("budget.max_children exceeds the enrollable Child count");
10318
- const qaFailureLimit = input.budget?.qa_failure_limit === undefined ? DEFAULT_QA_FAILURE_LIMIT : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
10319
- const deadline = input.budget?.deadline_at === undefined ? new Date(confirmationTime.milliseconds + DEFAULT_DEADLINE_MS).toISOString() : timestamp(input.budget.deadline_at, "budget.deadline_at").iso;
10320
- if (Date.parse(deadline) <= confirmationTime.milliseconds)
10321
- throw new Error("budget.deadline_at must be later than confirmation_time");
10322
- return { max_children: maxChildren, deadline_at: deadline, qa_failure_limit: qaFailureLimit };
10323
- }
10324
- async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeGithubInitiative) {
10325
- if (!ID_PATTERN2.test(initiativeSlug))
10326
- throw new Error("initiative_slug is invalid");
10327
- if (!input || typeof input !== "object")
10328
- throw new Error("batch plan input is required");
10329
- const confirmationTime = timestamp(input.confirmation_time, "confirmation_time");
10330
- const trackerObservation = normalizeObservation(await readInitiative(root, initiativeSlug), initiativeSlug);
10331
- const order = dependencyOrder(trackerObservation);
10332
- const closures = dependencyClosures(order);
10333
- const children = [];
10334
- for (const task of order) {
10335
- const blockedBy = closures.get(task.task_id);
10336
- const child = { task_id: task.task_id, slice_id: task.slice_id, blocked_by: blockedBy };
10337
- const tombstone = readTaskTombstone(root, task.task_id);
10338
- const record = readTaskRecordRaw(root, task.task_id).record;
10339
- if (tombstone && record)
10340
- throw new Error(`Task ${task.task_id} has conflicting settled and active state`);
10341
- if (tombstone) {
10342
- children.push({ ...child, status: "already_settled", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
10343
- continue;
10344
- }
10345
- if (record) {
10346
- children.push({ ...child, status: "already_owned", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
10347
- continue;
10348
- }
10349
- const intentPath = `docs/plans/${task.task_id}.intent.json`;
10350
- try {
10351
- const read = observeTaskIntent(root, task.task_id, intentPath);
10352
- if (read.intent.risk === "critical") {
10353
- children.push({
10354
- ...child,
10355
- status: "needs_human",
10356
- reason: "critical",
10357
- intent_path: read.intent_ref.path,
10358
- intent_revision: read.intent_ref.revision,
10359
- intent_content_hash: read.content_hash
10360
- });
10361
- continue;
10362
- }
10363
- const binding = inspectSpecBinding(read.intent);
10364
- if (!binding.ok) {
10365
- children.push({
10366
- ...child,
10367
- status: "needs_human",
10368
- reason: specBindingReason(binding),
10369
- intent_path: read.intent_ref.path,
10370
- intent_revision: read.intent_ref.revision,
10371
- intent_content_hash: read.content_hash
10372
- });
10373
- continue;
10374
- }
10375
- children.push({
10376
- ...child,
10377
- status: "enrollable",
10378
- reason: null,
10379
- intent_path: read.intent_ref.path,
10380
- intent_revision: read.intent_ref.revision,
10381
- intent_content_hash: read.content_hash
10382
- });
10383
- } catch (error) {
10384
- if (!(error instanceof TaskIntentObservationError))
10385
- throw error;
10386
- children.push({ ...child, status: "needs_human", reason: "invalid_intent", intent_path: null, intent_revision: null, intent_content_hash: null });
10387
- }
10388
- }
10389
- const directDependencies = new Map(order.map((task) => [task.task_id, task.blocked_by]));
10390
- const childById = new Map(children.map((child) => [child.task_id, child]));
10391
- for (const child of children) {
10392
- if (child.status !== "enrollable")
10393
- continue;
10394
- if (directDependencies.get(child.task_id).some((id) => {
10395
- const status = childById.get(id).status;
10396
- return status === "already_owned" || status === "needs_human" || status === "blocked";
10397
- })) {
10398
- child.status = "blocked";
10399
- child.reason = "dependency_unavailable";
10400
- }
10401
- }
10402
- const enrollable = children.filter((child) => child.status === "enrollable").map((child) => ({
10403
- task_id: child.task_id,
10404
- intent_path: child.intent_path,
10405
- intent_revision: child.intent_revision,
10406
- intent_content_hash: child.intent_content_hash,
10407
- blocked_by: child.blocked_by.filter((id) => childById.get(id)?.status === "enrollable")
10408
- }));
10409
- if (!enrollable.length)
10410
- throw new Error("batch plan has no enrollable children; nothing to confirm");
10411
- const planDigest = `sha256:${createHash19("sha256").update(stableStringify(enrollable)).digest("hex")}`;
10412
- return {
10413
- contract: "assurance_kernel/batch_plan/v1",
10414
- initiative_slug: initiativeSlug,
10415
- confirmation_time: confirmationTime.iso,
10416
- tracker_observation: trackerObservation,
10417
- children,
10418
- enrollable,
10419
- plan_digest: planDigest,
10420
- budget: budget(input, enrollable.length, confirmationTime)
10421
- };
10422
- }
10423
-
10424
- // plugins/immune-brain/runtime/unattended/batch_state.ts
10425
- import { existsSync as existsSync6, 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";
10426
- import { randomUUID as randomUUID6 } from "node:crypto";
10427
- import { dirname as dirname7, join as join11 } from "node:path";
10428
- var BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10429
- var CHILD_RUN_STATES = new Set([
10430
- "pending",
10431
- "enrolled",
10432
- "settled",
10433
- "committed",
10434
- "needs_human",
10435
- "skipped_blocked"
10436
- ]);
10437
- var BATCH_RUN_STATES = new Set([
10438
- "prepared",
10439
- "running",
10440
- "needs_human",
10441
- "completed",
10442
- "budget_stopped",
10443
- "failed",
10444
- "rejected"
10445
- ]);
10446
- var ISO_TIMESTAMP_PATTERN2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
10447
- function isCanonicalTimestamp(value) {
10448
- if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN2.test(value))
10449
- return false;
10450
- const milliseconds = Date.parse(value);
10451
- return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === value;
10452
- }
10453
- function validateBatchId(batchId) {
10454
- if (typeof batchId !== "string" || !BATCH_ID_PATTERN.test(batchId))
10455
- throw new Error("batch id is not a safe file identity");
10456
- }
10457
- function statePath(batchId) {
10458
- validateBatchId(batchId);
10459
- return join11(".imm", "state", "batches", `${batchId}.json`);
10460
- }
10461
- function canonicalBytes(record) {
10462
- return `${JSON.stringify(record, null, 2)}
10463
- `;
10464
- }
10465
- function validateRecordShape(value, batchId) {
10466
- if (typeof value !== "object" || value === null)
10467
- throw new Error(`batch run state ${batchId} is not an object`);
10468
- const record = value;
10469
- if (record.contract !== "assurance_kernel/batch_run_state/v1")
10470
- throw new Error(`batch run state ${batchId} has an unknown contract`);
10471
- if (record.batch_id !== batchId)
10472
- throw new Error(`batch run state ${batchId} carries batch_id ${String(record.batch_id)}`);
10473
- if (typeof record.plan_digest !== "string" || !record.plan_digest)
10474
- throw new Error(`batch run state ${batchId} has an invalid plan_digest`);
10475
- if (typeof record.base_head !== "string" || !record.base_head)
10476
- throw new Error(`batch run state ${batchId} has an invalid base_head`);
10477
- if (record.branch !== undefined && (typeof record.branch !== "string" || !record.branch))
10478
- throw new Error(`batch run state ${batchId} has an invalid branch`);
10479
- if (!isCanonicalTimestamp(record.confirmation_time))
10480
- throw new Error(`batch run state ${batchId} has an invalid confirmation_time`);
10481
- if (!isCanonicalTimestamp(record.authorization_expires_at))
10482
- throw new Error(`batch run state ${batchId} has an invalid authorization_expires_at`);
10483
- if (!isCanonicalTimestamp(record.created_at) || !isCanonicalTimestamp(record.updated_at))
10484
- throw new Error(`batch run state ${batchId} has invalid state timestamps`);
10485
- if (!Array.isArray(record.children) || record.children.length === 0)
10486
- throw new Error(`batch run state ${batchId} has no children`);
10487
- if (!BATCH_RUN_STATES.has(String(record.batch_state)))
10488
- throw new Error(`batch run state ${batchId} has an invalid batch_state`);
10489
- if (typeof record.consecutive_qa_failures !== "number" || !Number.isInteger(record.consecutive_qa_failures) || record.consecutive_qa_failures < 0)
10490
- throw new Error(`batch run state ${batchId} has an invalid consecutive_qa_failures`);
10491
- const budget = record.budget;
10492
- if (typeof record.budget !== "object" || record.budget === null || typeof budget.max_children !== "number" || !Number.isInteger(budget.max_children) || budget.max_children <= 0 || !isCanonicalTimestamp(budget.deadline_at) || typeof budget.qa_failure_limit !== "number" || !Number.isInteger(budget.qa_failure_limit) || budget.qa_failure_limit <= 0)
10493
- throw new Error(`batch run state ${batchId} has an invalid budget`);
10494
- if (!Array.isArray(record.commits) || record.commits.some((c) => typeof c !== "string"))
10495
- throw new Error(`batch run state ${batchId} has an invalid commits list`);
10496
- const seenTaskIds = new Set;
10497
- for (const child of record.children) {
10498
- if (typeof child !== "object" || child === null || typeof child.task_id !== "string" || !child.task_id || typeof child.slice_id !== "string" || !child.slice_id)
10499
- throw new Error(`batch run state ${batchId} has an invalid child entry`);
10500
- if (!CHILD_RUN_STATES.has(String(child.state)))
10501
- throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid state`);
10502
- if (!Array.isArray(child.blocked_by) || child.blocked_by.some((b) => typeof b !== "string"))
10503
- throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid blocked_by`);
10504
- if (child.reason !== null && typeof child.reason !== "string" || child.commit !== null && typeof child.commit !== "string")
10505
- throw new Error(`batch run state ${batchId} child ${child.task_id} has invalid terminal fields`);
10506
- if (child.state === "committed" && child.commit === null)
10507
- throw new Error(`batch run state ${batchId} child ${child.task_id} is committed without a commit`);
10508
- if (child.state !== "committed" && child.commit !== null)
10509
- throw new Error(`batch run state ${batchId} child ${child.task_id} has a commit but is not committed`);
10510
- if ((child.state === "needs_human" || child.state === "skipped_blocked") && child.reason === null)
10511
- throw new Error(`batch run state ${batchId} child ${child.task_id} needs a terminal reason`);
10512
- if (seenTaskIds.has(child.task_id))
10513
- throw new Error(`batch run state ${batchId} has a duplicate child ${child.task_id}`);
10514
- seenTaskIds.add(child.task_id);
10515
- }
10516
- for (const child of record.children) {
10517
- if (child.state === "committed" && child.commit !== null && !record.commits.includes(child.commit))
10518
- throw new Error(`batch run state ${batchId} child ${child.task_id} commit is missing from commits`);
10253
+ for (const child of record.children) {
10254
+ if (child.state === "committed" && child.commit !== null && !record.commits.includes(child.commit))
10255
+ throw new Error(`batch run state ${batchId} child ${child.task_id} commit is missing from commits`);
10519
10256
  }
10520
10257
  if ((record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected") && record.children.some((child) => child.state === "enrolled" || child.state === "settled"))
10521
10258
  throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
@@ -10532,7 +10269,6 @@ function prepareBatchRunState(input) {
10532
10269
  base_head: input.base_head,
10533
10270
  branch: input.branch ?? `imm/${input.initiative_slug}`,
10534
10271
  confirmation_time: input.confirmation_time,
10535
- authorization_expires_at: input.authorization_expires_at,
10536
10272
  budget: input.budget,
10537
10273
  batch_state: "prepared",
10538
10274
  children: input.children.map((child) => ({
@@ -10550,13 +10286,17 @@ function prepareBatchRunState(input) {
10550
10286
  };
10551
10287
  return prepared;
10552
10288
  }
10289
+ function parseBatchRunState(raw, batchId) {
10290
+ validateBatchId(batchId);
10291
+ const parsed = JSON.parse(raw);
10292
+ validateRecordShape(parsed, batchId);
10293
+ return withoutRetiredClock(parsed);
10294
+ }
10553
10295
  function readBatchRunState(root, batchId) {
10554
10296
  const path = statePath(batchId);
10555
10297
  if (!existsSync6(join11(root, path)))
10556
10298
  return null;
10557
- const parsed = JSON.parse(readSecureProjectFile(root, path));
10558
- validateRecordShape(parsed, batchId);
10559
- return parsed;
10299
+ return parseBatchRunState(readSecureProjectFile(root, path), batchId);
10560
10300
  }
10561
10301
  function ensureSecureDirectory2(root, relative) {
10562
10302
  const target = join11(root, relative);
@@ -10596,51 +10336,478 @@ function writeFileAtomically(root, relative, bytes) {
10596
10336
  }
10597
10337
  }
10598
10338
  }
10599
- function writeBatchRunState(root, record) {
10600
- const path = statePath(record.batch_id);
10601
- validateRecordShape(record, record.batch_id);
10602
- return withKernelStoreLock(root, () => {
10603
- const existing = existsSync6(join11(root, path)) ? readSecureProjectFile(root, path) : null;
10604
- if (existing !== null && existing === canonicalBytes(record))
10605
- return record;
10606
- const stored = {
10607
- ...record,
10608
- updated_at: new Date().toISOString()
10609
- };
10610
- ensureSecureDirectory2(root, join11(".imm", "state", "batches"));
10611
- writeFileAtomically(root, path, canonicalBytes(stored));
10612
- return stored;
10613
- });
10614
- }
10615
- function reportPath(batchId) {
10616
- validateBatchId(batchId);
10617
- return join11(".imm", "state", "batches", `${batchId}.report.json`);
10618
- }
10619
- function writeBatchRunReport(root, report) {
10620
- const relative = reportPath(report.batch_id);
10621
- return withKernelStoreLock(root, () => {
10622
- const path = join11(root, relative);
10623
- if (existsSync6(path)) {
10624
- const original = JSON.parse(readSecureProjectFile(root, relative));
10625
- if (typeof original !== "object" || original === null || original.contract !== "assurance_kernel/batch_run_report/v1")
10626
- throw new Error(`batch run report ${report.batch_id} has an unknown contract`);
10627
- const prior = original;
10628
- if (canonicalReportBytes(prior) === canonicalReportBytes(report))
10629
- return prior;
10630
- if (prior.batch_state !== "needs_human")
10631
- return prior;
10339
+ function replaceBatchRunState(root, expected, next, validate) {
10340
+ const path = statePath(next.batch_id);
10341
+ validateRecordShape(next, next.batch_id);
10342
+ return withKernelStoreLock(root, () => {
10343
+ if (!readSecureProjectBytes(root, path).equals(expected))
10344
+ throw new Error("batch state CAS mismatch");
10345
+ validate();
10346
+ const stored = withoutRetiredClock({ ...next, updated_at: new Date().toISOString() });
10347
+ writeFileAtomically(root, path, canonicalBytes(stored));
10348
+ return stored;
10349
+ });
10350
+ }
10351
+ function writeBatchRunState(root, record) {
10352
+ const path = statePath(record.batch_id);
10353
+ validateRecordShape(record, record.batch_id);
10354
+ return withKernelStoreLock(root, () => {
10355
+ const existing = existsSync6(join11(root, path)) ? readSecureProjectFile(root, path) : null;
10356
+ if (existing !== null && existing === canonicalBytes(record))
10357
+ return record;
10358
+ const stored = withoutRetiredClock({
10359
+ ...record,
10360
+ updated_at: new Date().toISOString()
10361
+ });
10362
+ ensureSecureDirectory2(root, join11(".imm", "state", "batches"));
10363
+ writeFileAtomically(root, path, canonicalBytes(stored));
10364
+ return stored;
10365
+ });
10366
+ }
10367
+ function reportPath(batchId) {
10368
+ validateBatchId(batchId);
10369
+ return join11(".imm", "state", "batches", `${batchId}.report.json`);
10370
+ }
10371
+ function writeBatchRunReport(root, report) {
10372
+ const relative = reportPath(report.batch_id);
10373
+ return withKernelStoreLock(root, () => {
10374
+ const path = join11(root, relative);
10375
+ if (existsSync6(path)) {
10376
+ const original = JSON.parse(readSecureProjectFile(root, relative));
10377
+ if (typeof original !== "object" || original === null || original.contract !== "assurance_kernel/batch_run_report/v1")
10378
+ throw new Error(`batch run report ${report.batch_id} has an unknown contract`);
10379
+ const prior = original;
10380
+ if (canonicalReportBytes(prior) === canonicalReportBytes(report))
10381
+ return prior;
10382
+ if (prior.batch_state !== "needs_human")
10383
+ return prior;
10384
+ }
10385
+ ensureSecureDirectory2(root, join11(".imm", "state", "batches"));
10386
+ writeFileAtomically(root, relative, canonicalReportBytes(report));
10387
+ return report;
10388
+ });
10389
+ }
10390
+ function canonicalReportBytes(report) {
10391
+ return `${JSON.stringify(report, null, 2)}
10392
+ `;
10393
+ }
10394
+ function isTerminalBatchState(state) {
10395
+ return state === "completed" || state === "budget_stopped" || state === "failed" || state === "rejected";
10396
+ }
10397
+
10398
+ // plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts
10399
+ var oid = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
10400
+ var id = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10401
+ var batchAuthor = process.env.GIT_AUTHOR_NAME || "Immune-Brain Batch";
10402
+ var decode = (bytes) => new TextDecoder("utf-8", { fatal: true }).decode(bytes);
10403
+ function refuse() {
10404
+ throw new Error("plan_digest mismatch: batch plan reconfirmation is not eligible");
10405
+ }
10406
+ function matchesLocalProof(localJson, exported) {
10407
+ return localJson !== null && JSON.stringify(Object.entries(JSON.parse(localJson)).sort()) === JSON.stringify(Object.entries(exported).sort());
10408
+ }
10409
+ function git4(root, args) {
10410
+ const r = spawnSync6("git", ["-C", root, ...args], { encoding: "buffer", maxBuffer: 262144, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
10411
+ if (r.status !== 0)
10412
+ refuse();
10413
+ return decode(r.stdout);
10414
+ }
10415
+ function ownUnpersistedBatchHead(root, record, head) {
10416
+ try {
10417
+ if (!oid.test(head) || !oid.test(record.base_head) || !id.test(record.batch_id) || record.commits.length || record.children[0]?.state !== "settled" || record.children.some((c) => c.commit !== null))
10418
+ return false;
10419
+ const task = record.children[0].task_id;
10420
+ if (!id.test(task))
10421
+ return false;
10422
+ const evidence = JSON.parse(decode(readSecureProjectBytes(root, `.imm/state/batches/commits/${record.batch_id}-${task}.json`)));
10423
+ if (evidence.contract !== "assurance_kernel/batch_commit_evidence/v1" || evidence.batch_id !== record.batch_id || evidence.task_id !== task || evidence.commit !== head || evidence.parent_head !== record.base_head)
10424
+ return false;
10425
+ if (git4(root, ["symbolic-ref", "--short", "HEAD"]).trim() !== record.branch)
10426
+ return false;
10427
+ const parents = git4(root, ["rev-list", "--parents", "-n", "1", head]).trim().split(/\s+/).slice(1);
10428
+ if (parents.length !== 1 || parents[0] !== record.base_head)
10429
+ return false;
10430
+ const metadata = git4(root, ["show", "-s", "--format=%an%x00%s%x00%(trailers:key=Immune-Brain-Batch,valueonly)", head]).split("\x00");
10431
+ if (metadata[0] !== batchAuthor || !metadata[1]?.startsWith(`imm(${task}):`) || metadata[2]?.trim() !== record.batch_id)
10432
+ return false;
10433
+ const run = localRunId(root, task);
10434
+ if (!run || !id.test(run))
10435
+ return false;
10436
+ const pair = readAuditTaskPair(root, task, run);
10437
+ if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4" || pair.record.lifecycle !== "done" || pair.proof.terminal_lifecycle !== "done")
10438
+ return false;
10439
+ const local = withKernelRead(root, (db) => readRunRowByTask(db, task));
10440
+ if (!local || local.run_id !== run || local.state !== "done" || !matchesLocalProof(local.terminal_proof_json, pair.proof))
10441
+ return false;
10442
+ for (const file of ["task-record.json", "terminal-proof.json"]) {
10443
+ const path = `.imm/audit/${task}/${run}/${file}`;
10444
+ if (!readSecureProjectBytes(root, path).equals(Buffer.from(git4(root, ["show", `${head}:${path}`]))))
10445
+ return false;
10446
+ }
10447
+ const paths = git4(root, ["diff-tree", "--no-commit-id", "--name-only", "--no-renames", "-z", "-r", head]).split("\x00").filter(Boolean);
10448
+ if (!paths.length || paths.some((path) => !path.startsWith(`.imm/audit/${task}/${run}/`) && !pair.record.intent_snapshot.scope_hint.some((s) => pathMatchesScope(path, s))))
10449
+ return false;
10450
+ const identity = taskCommitRevisionIdentity(root, pair.record.intent_snapshot.scope_hint, pair.record.git_base_head, head);
10451
+ if (!projectTask(pair.record.intent_snapshot, pair.record, identity.diff_hash, pair.record.intent_ref.content_hash, identity.changed_paths).complete)
10452
+ return false;
10453
+ return git4(root, ["rev-parse", "HEAD"]).trim() === head;
10454
+ } catch {
10455
+ return false;
10456
+ }
10457
+ }
10458
+ var confirmations = new Map;
10459
+ function retainReconfirmation(nonce, snapshot) {
10460
+ if (confirmations.has(nonce))
10461
+ refuse();
10462
+ confirmations.set(nonce, snapshot);
10463
+ }
10464
+ function takeReconfirmation(nonce) {
10465
+ const found = confirmations.get(nonce);
10466
+ confirmations.delete(nonce);
10467
+ if (!found)
10468
+ refuse();
10469
+ return found;
10470
+ }
10471
+ async function captureBatchReconfirmation(root, record, children) {
10472
+ if (!id.test(record.batch_id) || !oid.test(record.base_head) || isTerminalBatchState(record.batch_state) || record.commits.length || record.children.some((c) => c.commit !== null || c.state === "committed") || record.children.length !== children.length || !children.length)
10473
+ refuse();
10474
+ const assertNoCommitEvidence = () => {
10475
+ for (const child of record.children) {
10476
+ if (!id.test(child.task_id))
10477
+ refuse();
10478
+ const path = `.imm/state/batches/commits/${record.batch_id}-${child.task_id}.json`;
10479
+ let missing = false;
10480
+ try {
10481
+ readSecureProjectBytes(root, path);
10482
+ } catch (error) {
10483
+ if (error instanceof Error && error.message === `source_missing: ${path}`)
10484
+ missing = true;
10485
+ else
10486
+ refuse();
10487
+ }
10488
+ if (!missing)
10489
+ refuse();
10490
+ }
10491
+ };
10492
+ assertNoCommitEvidence();
10493
+ const statePath = `.imm/state/batches/${record.batch_id}.json`;
10494
+ const stateBytes = readSecureProjectBytes(root, statePath);
10495
+ if (JSON.stringify(JSON.parse(decode(stateBytes))) !== JSON.stringify(record))
10496
+ refuse();
10497
+ const files = new Map;
10498
+ const capture = (path) => {
10499
+ const bytes = readSecureProjectBytes(root, path);
10500
+ files.set(path, bytes);
10501
+ return bytes;
10502
+ };
10503
+ const oldChildren = [];
10504
+ const changed = [];
10505
+ let scope = null;
10506
+ const workspace = readWorkspaceStateRaw(root);
10507
+ if (workspace.state.current_working !== null)
10508
+ refuse();
10509
+ for (const [index, child] of children.entries()) {
10510
+ const previous = record.children[index];
10511
+ if (!id.test(child.task_id) || child.task_id !== previous.task_id || child.slice_id !== previous.slice_id || JSON.stringify(child.blocked_by) !== JSON.stringify(previous.blocked_by) || child.intent_path !== `docs/plans/${child.task_id}.intent.json`)
10512
+ refuse();
10513
+ const old = parseTaskIntentV1(JSON.parse(git4(root, ["show", `${record.base_head}:${child.intent_path}`])));
10514
+ if (old.task_id !== child.task_id)
10515
+ refuse();
10516
+ const oldHash = canonicalIntentHash(old);
10517
+ oldChildren.push({ task_id: child.task_id, blocked_by: [...child.blocked_by], intent_path: child.intent_path, intent_revision: old.revision, intent_content_hash: oldHash });
10518
+ const currentBytes = capture(child.intent_path);
10519
+ const current = parseTaskIntentV1(JSON.parse(decode(currentBytes)));
10520
+ const currentHash = canonicalIntentHash(current);
10521
+ const tracked = readTaskIntent(root, child.task_id, child.intent_path);
10522
+ if (currentHash !== tracked.content_hash || currentHash !== child.intent_content_hash || current.revision !== child.intent_revision)
10523
+ refuse();
10524
+ if (currentHash === oldHash)
10525
+ continue;
10526
+ if (scope !== null || previous.state === "pending" || previous.state === "skipped_blocked" || current.revision <= old.revision || current.goal !== old.goal || current.owner !== old.owner)
10527
+ refuse();
10528
+ const run = localRunId(root, child.task_id);
10529
+ if (!run || !id.test(run))
10530
+ refuse();
10531
+ const local = withKernelRead(root, (db) => readRunRowByTask(db, child.task_id));
10532
+ if (!local || local.run_id !== run || local.state !== "done" || local.enrollment_event_id !== `enroll-${child.task_id}-${local.created_at}` || Date.parse(local.created_at) > Date.parse(record.updated_at))
10533
+ refuse();
10534
+ const pair = readAuditTaskPair(root, child.task_id, run);
10535
+ if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4")
10536
+ refuse();
10537
+ const r = pair.record;
10538
+ if (r.lifecycle !== "done" || r.intent_ref.content_hash !== currentHash || canonicalIntentHash(r.intent_snapshot) !== currentHash || r.git_base_head !== record.base_head || pair.proof.terminal_lifecycle !== "done")
10539
+ refuse();
10540
+ const terminal = r.history.at(-1);
10541
+ if (terminal?.type !== "complete" || terminal.from_state !== "active:frozen" || terminal.to_state !== "done:frozen" || terminal.id !== pair.proof.terminal_event_id)
10542
+ refuse();
10543
+ const recordPath = `.imm/audit/${child.task_id}/${run}/task-record.json`;
10544
+ const proofPath = `.imm/audit/${child.task_id}/${run}/terminal-proof.json`;
10545
+ if (!capture(recordPath).equals(Buffer.from(local.record_json)))
10546
+ refuse();
10547
+ const capturedProof = JSON.parse(decode(capture(proofPath)));
10548
+ if (!matchesLocalProof(local.terminal_proof_json, capturedProof) || !matchesLocalProof(local.terminal_proof_json, pair.proof))
10549
+ refuse();
10550
+ const identity = taskRevisionIdentity(root, r.intent_snapshot.scope_hint, r.git_base_head, r.task_id);
10551
+ const projection = await projectAssurance(root, child.task_id, (cwd, task) => {
10552
+ if (task.contract !== "assurance_kernel/task_record/v4" || !task.git_base_head)
10553
+ refuse();
10554
+ return taskRevisionIdentity(cwd, task.intent_snapshot.scope_hint, task.git_base_head, task.task_id);
10555
+ });
10556
+ if (projection.error || projection.claim || projection.projection.run_id !== run || projection.projection.lifecycle !== "done" || !projection.projection.completion_ready || projection.projection.intent_content_hash !== currentHash || projection.projection.diff_hash !== identity.diff_hash || !projection.projection.fresh_approval_kinds.includes("qa") || projection.projection.risk !== "routine" && !projection.projection.fresh_approval_kinds.includes("review"))
10557
+ refuse();
10558
+ scope = r.intent_snapshot.scope_hint;
10559
+ changed.push({ task: child.task_id, run, identity: JSON.stringify(identity), local: JSON.stringify(local) });
10560
+ }
10561
+ if (computeBatchPlanDigest(oldChildren) !== record.plan_digest || changed.length !== 1 || scope === null)
10562
+ refuse();
10563
+ const confirmedScope = scope;
10564
+ const owner = changed[0];
10565
+ const status = () => git4(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignore-submodules=none"]);
10566
+ const statusBytes = status();
10567
+ const entries = statusBytes.split("\x00").filter(Boolean);
10568
+ for (const entry of entries) {
10569
+ const code = entry.slice(0, 2), path = entry.slice(3);
10570
+ if (code[1] !== " " || !["A", "M", "D"].includes(code[0]) || !path.startsWith(`.imm/audit/${owner.task}/${owner.run}/`) && !confirmedScope.some((s) => pathMatchesScope(path, s)))
10571
+ refuse();
10572
+ }
10573
+ const flagsBytes = git4(root, ["ls-files", "-v"]);
10574
+ const flags = flagsBytes.split(`
10575
+ `).filter(Boolean);
10576
+ if (flags.some((line) => line[0] === "S" || line[0] !== line[0]?.toUpperCase()))
10577
+ refuse();
10578
+ const head = () => git4(root, ["rev-parse", "HEAD"]).trim();
10579
+ const branch = () => git4(root, ["symbolic-ref", "--short", "HEAD"]).trim();
10580
+ if (head() !== record.base_head || branch() !== record.branch)
10581
+ refuse();
10582
+ const assertDeliveryUnchanged = () => {
10583
+ for (const c of changed) {
10584
+ const pair = readAuditTaskPair(root, c.task, c.run);
10585
+ if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4" || !pair.record.git_base_head)
10586
+ refuse();
10587
+ const local = withKernelRead(root, (db) => readRunRowByTask(db, c.task));
10588
+ const diff = taskRevisionIdentity(root, pair.record.intent_snapshot.scope_hint, pair.record.git_base_head, c.task);
10589
+ if (localRunId(root, c.task) !== c.run || JSON.stringify(local) !== c.local || JSON.stringify(diff) !== c.identity)
10590
+ refuse();
10591
+ }
10592
+ };
10593
+ const assertUnchanged = () => {
10594
+ assertNoCommitEvidence();
10595
+ if (!readSecureProjectBytes(root, statePath).equals(stateBytes) || head() !== record.base_head || branch() !== record.branch || status() !== statusBytes || git4(root, ["ls-files", "-v"]) !== flagsBytes || readWorkspaceStateRaw(root).revision !== workspace.revision)
10596
+ refuse();
10597
+ for (const [path, bytes] of files)
10598
+ if (!readSecureProjectBytes(root, path).equals(bytes))
10599
+ refuse();
10600
+ assertDeliveryUnchanged();
10601
+ };
10602
+ assertUnchanged();
10603
+ const plan_digest = computeBatchPlanDigest(oldChildren.map((old, index) => ({
10604
+ ...old,
10605
+ intent_revision: children[index].intent_revision,
10606
+ intent_content_hash: children[index].intent_content_hash
10607
+ })));
10608
+ return { root, plan_digest, stateBytes, assertUnchanged };
10609
+ }
10610
+
10611
+ // plugins/immune-brain/runtime/unattended/batch_plan.ts
10612
+ import { createHash as createHash19 } from "node:crypto";
10613
+ var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10614
+ var DEFAULT_QA_FAILURE_LIMIT = 2;
10615
+ function specBindingReason(inspection) {
10616
+ if (inspection.missing.length > 0)
10617
+ return `spec_binding_incomplete: ${inspection.missing.join(", ")}`;
10618
+ return inspection.code === "binding_ambiguous" ? "spec_binding_ambiguous" : "spec_binding_missing";
10619
+ }
10620
+ function compareIds(left, right) {
10621
+ return left < right ? -1 : left > right ? 1 : 0;
10622
+ }
10623
+ function positiveSafeInteger(value, name) {
10624
+ if (!Number.isSafeInteger(value) || value <= 0)
10625
+ throw new Error(`${name} must be a positive safe integer`);
10626
+ return value;
10627
+ }
10628
+ var ISO_TIMESTAMP_PATTERN2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
10629
+ function timestamp(value, name) {
10630
+ if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN2.test(value))
10631
+ throw new Error(`${name} must be an ISO timestamp`);
10632
+ const milliseconds = Date.parse(value);
10633
+ if (!Number.isFinite(milliseconds))
10634
+ throw new Error(`${name} must be an ISO timestamp`);
10635
+ return { milliseconds, iso: new Date(milliseconds).toISOString() };
10636
+ }
10637
+ function normalizeObservation(value, initiativeSlug) {
10638
+ if (!value || value.contract !== "immune_brain/github_initiative_observation/v1")
10639
+ throw new Error("tracker returned an invalid Initiative observation contract");
10640
+ if (value.initiative_id !== initiativeSlug)
10641
+ throw new Error("tracker returned an observation for another Initiative");
10642
+ if (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0)
10643
+ throw new Error("tracker returned an invalid Initiative Issue number");
10644
+ if (!Array.isArray(value.tasks))
10645
+ throw new Error("tracker returned an invalid Initiative Task list");
10646
+ const taskIds = new Set;
10647
+ const sliceIds = new Set;
10648
+ const tasks = value.tasks.map((task, index) => {
10649
+ if (!task || typeof task !== "object")
10650
+ throw new Error(`tracker Task ${index} is invalid`);
10651
+ if (!ID_PATTERN2.test(task.task_id))
10652
+ throw new Error(`tracker Task ${index} has an invalid task_id`);
10653
+ if (!ID_PATTERN2.test(task.slice_id))
10654
+ throw new Error(`tracker Task ${index} has an invalid slice_id`);
10655
+ if (!Number.isSafeInteger(task.issue_number) || task.issue_number <= 0)
10656
+ throw new Error(`tracker Task ${task.task_id} has an invalid Issue number`);
10657
+ if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN2.test(id)))
10658
+ throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
10659
+ if (taskIds.has(task.task_id))
10660
+ throw new Error(`tracker returned duplicate Task ${task.task_id}`);
10661
+ if (sliceIds.has(task.slice_id))
10662
+ throw new Error(`tracker returned duplicate Slice ${task.slice_id}`);
10663
+ taskIds.add(task.task_id);
10664
+ sliceIds.add(task.slice_id);
10665
+ const blockedBy = [...task.blocked_by].sort();
10666
+ if (new Set(blockedBy).size !== blockedBy.length || blockedBy.includes(task.task_id))
10667
+ throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
10668
+ return { ...task, blocked_by: blockedBy };
10669
+ });
10670
+ for (const task of tasks) {
10671
+ const unknown = task.blocked_by.find((id) => !taskIds.has(id));
10672
+ if (unknown)
10673
+ throw new Error(`tracker Task ${task.task_id} depends on unknown Task ${unknown}`);
10674
+ }
10675
+ return { ...value, tasks: tasks.sort((left, right) => compareIds(left.task_id, right.task_id)) };
10676
+ }
10677
+ function dependencyOrder(observation) {
10678
+ const remaining = new Map(observation.tasks.map((task) => [task.task_id, task]));
10679
+ const done = new Set;
10680
+ const order = [];
10681
+ while (remaining.size) {
10682
+ const ready = [...remaining.values()].filter((task) => task.blocked_by.every((id) => done.has(id))).sort((left, right) => compareIds(left.task_id, right.task_id));
10683
+ if (!ready.length)
10684
+ throw new Error("Initiative Task dependencies must form an acyclic graph");
10685
+ for (const task of ready) {
10686
+ remaining.delete(task.task_id);
10687
+ done.add(task.task_id);
10688
+ order.push(task);
10689
+ }
10690
+ }
10691
+ return order;
10692
+ }
10693
+ function dependencyClosures(order) {
10694
+ const closures = new Map;
10695
+ for (const task of order) {
10696
+ const closure = new Set;
10697
+ for (const dependency of task.blocked_by) {
10698
+ closure.add(dependency);
10699
+ for (const transitive of closures.get(dependency) ?? [])
10700
+ closure.add(transitive);
10632
10701
  }
10633
- ensureSecureDirectory2(root, join11(".imm", "state", "batches"));
10634
- writeFileAtomically(root, relative, canonicalReportBytes(report));
10635
- return report;
10636
- });
10702
+ closures.set(task.task_id, [...closure].sort());
10703
+ }
10704
+ return closures;
10637
10705
  }
10638
- function canonicalReportBytes(report) {
10639
- return `${JSON.stringify(report, null, 2)}
10640
- `;
10706
+ function budget(input, enrollableCount) {
10707
+ const maxChildren = input.budget?.max_children === undefined ? positiveSafeInteger(enrollableCount, "budget.max_children") : positiveSafeInteger(input.budget.max_children, "budget.max_children");
10708
+ if (maxChildren > enrollableCount)
10709
+ throw new Error("budget.max_children exceeds the enrollable Child count");
10710
+ const qaFailureLimit = input.budget?.qa_failure_limit === undefined ? DEFAULT_QA_FAILURE_LIMIT : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
10711
+ return { max_children: maxChildren, qa_failure_limit: qaFailureLimit };
10641
10712
  }
10642
- function isTerminalBatchState(state) {
10643
- return state === "completed" || state === "budget_stopped" || state === "failed" || state === "rejected";
10713
+ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeGithubInitiative) {
10714
+ if (!ID_PATTERN2.test(initiativeSlug))
10715
+ throw new Error("initiative_slug is invalid");
10716
+ if (!input || typeof input !== "object")
10717
+ throw new Error("batch plan input is required");
10718
+ const confirmationTime = timestamp(input.confirmation_time, "confirmation_time");
10719
+ const trackerObservation = normalizeObservation(await readInitiative(root, initiativeSlug), initiativeSlug);
10720
+ const order = dependencyOrder(trackerObservation);
10721
+ const closures = dependencyClosures(order);
10722
+ const children = [];
10723
+ for (const task of order) {
10724
+ const blockedBy = closures.get(task.task_id);
10725
+ const child = { task_id: task.task_id, slice_id: task.slice_id, blocked_by: blockedBy };
10726
+ const tombstone = readTaskTombstone(root, task.task_id);
10727
+ const record = readTaskRecordRaw(root, task.task_id).record;
10728
+ if (tombstone && record)
10729
+ throw new Error(`Task ${task.task_id} has conflicting settled and active state`);
10730
+ if (tombstone) {
10731
+ children.push({ ...child, status: "already_settled", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
10732
+ continue;
10733
+ }
10734
+ if (record) {
10735
+ children.push({ ...child, status: "already_owned", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
10736
+ continue;
10737
+ }
10738
+ const intentPath = `docs/plans/${task.task_id}.intent.json`;
10739
+ try {
10740
+ const read = observeTaskIntent(root, task.task_id, intentPath);
10741
+ if (read.intent.risk === "critical") {
10742
+ children.push({
10743
+ ...child,
10744
+ status: "needs_human",
10745
+ reason: "critical",
10746
+ intent_path: read.intent_ref.path,
10747
+ intent_revision: read.intent_ref.revision,
10748
+ intent_content_hash: read.content_hash
10749
+ });
10750
+ continue;
10751
+ }
10752
+ const binding = inspectSpecBinding(read.intent);
10753
+ if (!binding.ok) {
10754
+ children.push({
10755
+ ...child,
10756
+ status: "needs_human",
10757
+ reason: specBindingReason(binding),
10758
+ intent_path: read.intent_ref.path,
10759
+ intent_revision: read.intent_ref.revision,
10760
+ intent_content_hash: read.content_hash
10761
+ });
10762
+ continue;
10763
+ }
10764
+ children.push({
10765
+ ...child,
10766
+ status: "enrollable",
10767
+ reason: null,
10768
+ intent_path: read.intent_ref.path,
10769
+ intent_revision: read.intent_ref.revision,
10770
+ intent_content_hash: read.content_hash
10771
+ });
10772
+ } catch (error) {
10773
+ if (!(error instanceof TaskIntentObservationError))
10774
+ throw error;
10775
+ children.push({ ...child, status: "needs_human", reason: "invalid_intent", intent_path: null, intent_revision: null, intent_content_hash: null });
10776
+ }
10777
+ }
10778
+ const directDependencies = new Map(order.map((task) => [task.task_id, task.blocked_by]));
10779
+ const childById = new Map(children.map((child) => [child.task_id, child]));
10780
+ for (const child of children) {
10781
+ if (child.status !== "enrollable")
10782
+ continue;
10783
+ if (directDependencies.get(child.task_id).some((id) => {
10784
+ const status = childById.get(id).status;
10785
+ return status === "already_owned" || status === "needs_human" || status === "blocked";
10786
+ })) {
10787
+ child.status = "blocked";
10788
+ child.reason = "dependency_unavailable";
10789
+ }
10790
+ }
10791
+ const enrollable = children.filter((child) => child.status === "enrollable").map((child) => ({
10792
+ task_id: child.task_id,
10793
+ intent_path: child.intent_path,
10794
+ intent_revision: child.intent_revision,
10795
+ intent_content_hash: child.intent_content_hash,
10796
+ blocked_by: child.blocked_by.filter((id) => childById.get(id)?.status === "enrollable")
10797
+ }));
10798
+ if (!enrollable.length)
10799
+ throw new Error("batch plan has no enrollable children; nothing to confirm");
10800
+ const planDigest = `sha256:${createHash19("sha256").update(stableStringify(enrollable)).digest("hex")}`;
10801
+ return {
10802
+ contract: "assurance_kernel/batch_plan/v1",
10803
+ initiative_slug: initiativeSlug,
10804
+ confirmation_time: confirmationTime.iso,
10805
+ tracker_observation: trackerObservation,
10806
+ children,
10807
+ enrollable,
10808
+ plan_digest: planDigest,
10809
+ budget: budget(input, enrollable.length)
10810
+ };
10644
10811
  }
10645
10812
 
10646
10813
  // plugins/immune-brain/runtime/unattended/batch_preflight.ts
@@ -10661,6 +10828,27 @@ function readActiveClaimTaskId(root) {
10661
10828
  const claim = readBackendClaim(root);
10662
10829
  return workspace.state.current_working || (claim?.lifecycle_status === "active" ? claim.task_id : null);
10663
10830
  }
10831
+ function findResumableBatchSlugForTask(root, taskId) {
10832
+ const batchesDir = join12(root, ".imm", "state", "batches");
10833
+ if (!existsSync7(batchesDir))
10834
+ return null;
10835
+ for (const file of readdirSync6(batchesDir)) {
10836
+ if (!file.endsWith(".json"))
10837
+ continue;
10838
+ try {
10839
+ const record = JSON.parse(readFileSync13(join12(batchesDir, file), "utf8"));
10840
+ if (record?.contract !== "assurance_kernel/batch_run_state/v1")
10841
+ continue;
10842
+ if (record.batch_state !== "running" && record.batch_state !== "needs_human")
10843
+ continue;
10844
+ if (record.children.some((child) => child.task_id === taskId && (child.state === "enrolled" || child.state === "settled")))
10845
+ return record.initiative_slug;
10846
+ } catch {
10847
+ continue;
10848
+ }
10849
+ }
10850
+ return null;
10851
+ }
10664
10852
  function findExistingActiveBatch(root, initiativeSlug) {
10665
10853
  const batchesDir = join12(root, ".imm", "state", "batches");
10666
10854
  if (!existsSync7(batchesDir))
@@ -10732,9 +10920,51 @@ function findSettledBatchRecord(root, initiativeSlug) {
10732
10920
  }
10733
10921
  return newest;
10734
10922
  }
10923
+ function classifyBatchLineage(input) {
10924
+ const { root, branch, expectedHead, childCommits } = input;
10925
+ const run = (args) => spawnSync7("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
10926
+ const broken = (reason) => ({
10927
+ kind: "broken",
10928
+ message: `batch_head_lineage_broken: ${reason}`
10929
+ });
10930
+ const branchResult = run(["symbolic-ref", "--short", "HEAD"]);
10931
+ const currentBranch = branchResult.stdout.trim();
10932
+ if (branchResult.status !== 0 || currentBranch !== branch)
10933
+ return broken(`current branch ${currentBranch} does not match expected branch ${branch}`);
10934
+ const headResult = run(["rev-parse", "HEAD"]);
10935
+ const head = headResult.stdout.trim();
10936
+ if (headResult.status !== 0 || !head)
10937
+ return broken("current HEAD is unreadable");
10938
+ if (head !== expectedHead && run(["merge-base", "--is-ancestor", expectedHead, head]).status !== 0)
10939
+ return broken(`current HEAD ${head} does not descend from expected batch head ${expectedHead}`);
10940
+ for (const commit of childCommits) {
10941
+ if (run(["merge-base", "--is-ancestor", commit, head]).status !== 0)
10942
+ return broken(`recorded commit ${commit} is no longer reachable from HEAD`);
10943
+ }
10944
+ if (head === expectedHead)
10945
+ return { kind: "equal", head };
10946
+ if (input.batchId) {
10947
+ const claimed = run([
10948
+ "log",
10949
+ "--fixed-strings",
10950
+ `--grep=Immune-Brain-Batch: ${input.batchId}`,
10951
+ "--format=%H",
10952
+ `${expectedHead}..${head}`
10953
+ ]);
10954
+ const claimant = claimed.stdout.split(/\s+/).find(Boolean);
10955
+ if (claimed.status !== 0 || claimant)
10956
+ return broken(`commit ${claimant ?? head} claims batch ${input.batchId} without recorded evidence`);
10957
+ }
10958
+ return { kind: "fast_forward", head };
10959
+ }
10735
10960
  function expectedBatchHead(record) {
10736
10961
  const commits = Array.isArray(record.commits) ? record.commits : [];
10737
- return commits.length > 0 ? commits[commits.length - 1] : record.base_head;
10962
+ let head = commits.length > 0 ? commits[commits.length - 1] : record.base_head;
10963
+ for (const adoption of record.adopted_heads ?? []) {
10964
+ if (adoption.from === head)
10965
+ head = adoption.to;
10966
+ }
10967
+ return head;
10738
10968
  }
10739
10969
  function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
10740
10970
  let claim = null;
@@ -10748,7 +10978,7 @@ function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
10748
10978
  const currentTaskId = workspaceOwner || (claim?.lifecycle_status === "active" ? claim?.task_id : null);
10749
10979
  if (currentTaskId !== taskId || !claim)
10750
10980
  return false;
10751
- const branch = spawnSync5("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" }).stdout.trim();
10981
+ const branch = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" }).stdout.trim();
10752
10982
  if (branch !== batchBranch)
10753
10983
  return false;
10754
10984
  const childInBatch = existingBatch.children.find((c) => c.task_id === taskId);
@@ -10764,7 +10994,7 @@ function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
10764
10994
  }
10765
10995
  if (!rec)
10766
10996
  return false;
10767
- const lineageHeads = [existingBatch.base_head].concat(Array.isArray(existingBatch.commits) ? existingBatch.commits : []);
10997
+ const lineageHeads = [existingBatch.base_head].concat(Array.isArray(existingBatch.commits) ? existingBatch.commits : []).concat((existingBatch.adopted_heads ?? []).map((adoption) => adoption.to));
10768
10998
  if (!lineageHeads.includes(rec.git_base_head))
10769
10999
  return false;
10770
11000
  if (claim.enrollment_event_id !== `enroll-${taskId}-${claim.created_at}`)
@@ -10805,7 +11035,7 @@ function authorizedScopeOf(root, taskId, state) {
10805
11035
  return scope;
10806
11036
  }
10807
11037
  function porcelainEntries(root) {
10808
- const statusProc = spawnSync5("git", ["-C", root, "status", "--porcelain=v1", "-z", "--no-renames", "--untracked-files=all"], { encoding: "utf8" });
11038
+ const statusProc = spawnSync7("git", ["-C", root, "status", "--porcelain=v1", "-z", "--no-renames", "--untracked-files=all", "--ignore-submodules=none"], { encoding: "utf8", env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
10809
11039
  if (statusProc.status !== 0)
10810
11040
  return null;
10811
11041
  const entries = [];
@@ -10824,9 +11054,10 @@ async function projectPlanSurface(input) {
10824
11054
  const riskByTask = new Map;
10825
11055
  let budget;
10826
11056
  if (isResuming && existingBatch) {
10827
- budget = { ...existingBatch.budget };
10828
- if (Date.parse(budget.deadline_at) <= Date.parse(now))
10829
- budget.deadline_at = new Date(Date.parse(now) + DEFAULT_DEADLINE_MS).toISOString();
11057
+ budget = {
11058
+ max_children: existingBatch.budget.max_children,
11059
+ qa_failure_limit: existingBatch.budget.qa_failure_limit
11060
+ };
10830
11061
  try {
10831
11062
  recoveryChildren = existingBatch.children.map((c) => {
10832
11063
  const intentPath = `docs/plans/${c.task_id}.intent.json`;
@@ -10943,7 +11174,7 @@ async function projectBatchPreflight(options) {
10943
11174
  } catch (err) {
10944
11175
  return reject("git_head_unreadable", err instanceof Error ? err.message : String(err));
10945
11176
  }
10946
- const branchExists = spawnSync5("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${batchBranch}`]);
11177
+ const branchExists = spawnSync7("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${batchBranch}`]);
10947
11178
  if (branchExists.status === 0 && !existingBatch)
10948
11179
  return reject("branch_already_exists", batchBranch);
10949
11180
  const statusEntries = porcelainEntries(root);
@@ -10992,21 +11223,29 @@ async function projectBatchPreflight(options) {
10992
11223
  });
10993
11224
  if (!planSurface.ok)
10994
11225
  return reject(planSurface.key, planSurface.detail);
10995
- return {
10996
- ok: true,
10997
- projection: {
10998
- initiative_slug: initiativeSlug,
10999
- batch_branch: batchBranch,
11000
- is_resuming: isResuming,
11001
- existing_batch: existingBatch,
11002
- base_head: baseHead,
11003
- budget: planSurface.surface.budget,
11004
- plan_digest: planSurface.surface.plan_digest,
11005
- recovery_children: planSurface.surface.recovery_children,
11006
- risk_by_task: planSurface.surface.risk_by_task,
11007
- excluded: planSurface.surface.excluded
11226
+ let reconfirmation;
11227
+ if (activeRecord && activeRecord.plan_digest !== planSurface.surface.plan_digest) {
11228
+ try {
11229
+ reconfirmation = await captureBatchReconfirmation(root, activeRecord, planSurface.surface.recovery_children);
11230
+ } catch {
11231
+ return reject("plan_projection_failed", "batch plan reconfirmation is not eligible");
11008
11232
  }
11233
+ }
11234
+ const projection = {
11235
+ initiative_slug: initiativeSlug,
11236
+ batch_branch: batchBranch,
11237
+ is_resuming: isResuming,
11238
+ existing_batch: existingBatch,
11239
+ base_head: baseHead,
11240
+ budget: planSurface.surface.budget,
11241
+ plan_digest: planSurface.surface.plan_digest,
11242
+ recovery_children: planSurface.surface.recovery_children,
11243
+ risk_by_task: planSurface.surface.risk_by_task,
11244
+ excluded: planSurface.surface.excluded
11009
11245
  };
11246
+ if (reconfirmation)
11247
+ Object.defineProperty(projection, "reconfirmation", { value: reconfirmation });
11248
+ return { ok: true, projection };
11010
11249
  }
11011
11250
  async function projectBatchDrift(options) {
11012
11251
  const { root, initiative_slug: initiativeSlug, readInitiative } = options;
@@ -11046,34 +11285,37 @@ async function authorizeBatch(options) {
11046
11285
  is_resuming: isResuming
11047
11286
  } = projection;
11048
11287
  const existingBatch = projection.existing_batch;
11049
- const isExistingExpired = isResuming && Date.parse(existingBatch.authorization_expires_at) <= Date.now();
11050
- const branchBefore = spawnSync5("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11288
+ const branchBefore = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11051
11289
  if (branchBefore.status !== 0)
11052
11290
  return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
11291
+ const ownsUnpersistedHead = isResuming && existingBatch !== null && existingBatch.plan_digest === planDigest && expectedBatchHead(existingBatch) !== baseHead && ownUnpersistedBatchHead(root, existingBatch, baseHead);
11292
+ const fastForwardsRecordedHead = isResuming && existingBatch !== null && existingBatch.branch === batchBranch && expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead && classifyBatchLineage({
11293
+ root,
11294
+ branch: batchBranch,
11295
+ expectedHead: expectedBatchHead(existingBatch),
11296
+ childCommits: existingBatch.commits,
11297
+ batchId: existingBatch.batch_id
11298
+ }).kind === "fast_forward";
11299
+ if (isResuming && existingBatch && !existingBatch.commits.length && existingBatch.children[0]?.state === "settled" && expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead && !fastForwardsRecordedHead)
11300
+ return { outcome: "rejected", rejection: batchRejection("head_moved", "first unpersisted commit provenance is invalid") };
11053
11301
  const reuseBlockers = [];
11054
11302
  if (isResuming && existingBatch) {
11055
- if (isExistingExpired)
11056
- reuseBlockers.push("batch_authorization_expired");
11057
- if (Date.parse(existingBatch.budget.deadline_at) <= Date.now())
11058
- reuseBlockers.push("batch_budget_expired");
11059
11303
  if (existingBatch.batch_state !== "running")
11060
11304
  reuseBlockers.push("batch_not_running");
11061
11305
  if (existingBatch.plan_digest !== planDigest)
11062
11306
  reuseBlockers.push("batch_plan_digest_changed");
11063
11307
  if (existingBatch.branch !== batchBranch)
11064
11308
  reuseBlockers.push("batch_branch_changed");
11065
- if (expectedBatchHead(existingBatch) !== baseHead)
11309
+ if (expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead && !fastForwardsRecordedHead)
11066
11310
  reuseBlockers.push("batch_head_lineage_moved");
11067
11311
  }
11068
11312
  const reuseAuthorization = isResuming && reuseBlockers.length === 0;
11069
- const expiresAt = reuseAuthorization ? existingBatch.authorization_expires_at : budget.deadline_at;
11070
11313
  const batchId = isResuming && existingBatch ? existingBatch.batch_id : `batch-${initiativeSlug}-${randomUUID7()}`;
11071
11314
  const facts = {
11072
11315
  initiative_slug: initiativeSlug,
11073
11316
  batch_branch: batchBranch,
11074
11317
  plan_digest: planDigest,
11075
11318
  budget,
11076
- expires_at: expiresAt,
11077
11319
  reuse_blockers: reuseBlockers,
11078
11320
  children: projection.recovery_children.map((child) => ({
11079
11321
  task_id: child.task_id,
@@ -11104,7 +11346,7 @@ async function authorizeBatch(options) {
11104
11346
  return { outcome: "rejected", rejection: batchRejection("plan_changed", drift.plan_digest) };
11105
11347
  if (drift.base_head === null)
11106
11348
  return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
11107
- if (drift.base_head !== baseHead)
11349
+ if (drift.base_head !== baseHead || ownsUnpersistedHead && !ownUnpersistedBatchHead(root, existingBatch, baseHead))
11108
11350
  return { outcome: "rejected", rejection: batchRejection("head_moved", drift.base_head) };
11109
11351
  const finalClaimTaskId = readActiveClaimTaskId(root);
11110
11352
  const finalOwnClaim = isResuming && finalClaimTaskId !== null && isOwnBatchClaim(root, existingBatch, finalClaimTaskId, batchBranch);
@@ -11115,11 +11357,16 @@ async function authorizeBatch(options) {
11115
11357
  if (!current || current.corrupt || JSON.stringify(current.record) !== JSON.stringify(existingBatch))
11116
11358
  return { outcome: "rejected", rejection: batchRejection("plan_changed") };
11117
11359
  }
11118
- const branchAfter = spawnSync5("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11360
+ const branchAfter = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11119
11361
  if (branchAfter.status !== 0 || branchAfter.stdout !== branchBefore.stdout)
11120
11362
  return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "Git branch moved after native confirmation") };
11121
- if (Date.parse(expiresAt) <= Date.now())
11122
- return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "confirmed batch deadline expired before authorization issuance") };
11363
+ if (projection.reconfirmation) {
11364
+ try {
11365
+ projection.reconfirmation.assertUnchanged();
11366
+ } catch {
11367
+ return { outcome: "rejected", rejection: batchRejection("plan_changed") };
11368
+ }
11369
+ }
11123
11370
  const binding = {
11124
11371
  batch_id: batchId,
11125
11372
  initiative_slug: initiativeSlug,
@@ -11129,26 +11376,26 @@ async function authorizeBatch(options) {
11129
11376
  budget,
11130
11377
  actor_id: LITERAL_USER_ACTOR_ID,
11131
11378
  confirmation_ref: options.confirmationRef({ batch_id: batchId, request_id: requestId }),
11132
- expires_at: expiresAt,
11133
11379
  nonce: options.nonce
11134
11380
  };
11381
+ if (projection.reconfirmation)
11382
+ retainReconfirmation(binding.nonce, projection.reconfirmation);
11135
11383
  return {
11136
11384
  outcome: "authorized",
11137
11385
  batch_id: batchId,
11138
11386
  reuse_authorization: reuseAuthorization,
11139
11387
  reuse_blockers: reuseBlockers,
11140
- expires_at: expiresAt,
11141
11388
  binding
11142
11389
  };
11143
11390
  }
11144
11391
 
11145
11392
  // plugins/immune-brain/runtime/unattended/batch_runner.ts
11146
- import { spawnSync as spawnSync7 } from "node:child_process";
11393
+ import { spawnSync as spawnSync9 } from "node:child_process";
11147
11394
  import { existsSync as existsSync9 } from "node:fs";
11148
11395
  import { join as join14 } from "node:path";
11149
11396
 
11150
11397
  // plugins/immune-brain/runtime/unattended/batch_git.ts
11151
- import { spawnSync as spawnSync6 } from "node:child_process";
11398
+ import { spawnSync as spawnSync8 } from "node:child_process";
11152
11399
  import { createHash as createHash20, randomUUID as randomUUID8 } from "node:crypto";
11153
11400
  import {
11154
11401
  constants as constants6,
@@ -11173,7 +11420,7 @@ var DEFAULT_GIT_ENV = {
11173
11420
  function runBatchGitPreflight(input) {
11174
11421
  const { root, initiative_slug: initiativeSlug, base_head: baseHead } = input;
11175
11422
  const branch = `imm/${initiativeSlug}`;
11176
- const toplevelResult = spawnSync6("git", ["-C", root, "rev-parse", "--show-toplevel"], {
11423
+ const toplevelResult = spawnSync8("git", ["-C", root, "rev-parse", "--show-toplevel"], {
11177
11424
  encoding: "utf8",
11178
11425
  stdio: ["ignore", "pipe", "pipe"]
11179
11426
  });
@@ -11211,7 +11458,7 @@ function runBatchGitPreflight(input) {
11211
11458
  message: `unsupported index flags (assume-unchanged/skip-worktree) detected: ${flaggedPreflight.join(", ")}`
11212
11459
  };
11213
11460
  }
11214
- const statusResult = spawnSync6("git", ["-C", root, "status", "--porcelain", "--untracked-files=all", "--ignore-submodules=none"], {
11461
+ const statusResult = spawnSync8("git", ["-C", root, "status", "--porcelain", "--untracked-files=all", "--ignore-submodules=none"], {
11215
11462
  encoding: "utf8",
11216
11463
  stdio: ["ignore", "pipe", "pipe"]
11217
11464
  });
@@ -11229,7 +11476,7 @@ function runBatchGitPreflight(input) {
11229
11476
  message: "working tree is dirty before batch preflight"
11230
11477
  };
11231
11478
  }
11232
- const headResult = spawnSync6("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
11479
+ const headResult = spawnSync8("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
11233
11480
  encoding: "utf8",
11234
11481
  stdio: ["ignore", "pipe", "pipe"]
11235
11482
  });
@@ -11240,11 +11487,11 @@ function runBatchGitPreflight(input) {
11240
11487
  message: "HEAD is uncommitted or not a valid commit"
11241
11488
  };
11242
11489
  }
11243
- const branchCheck = spawnSync6("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { stdio: ["ignore", "ignore", "ignore"] });
11490
+ const branchCheck = spawnSync8("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { stdio: ["ignore", "ignore", "ignore"] });
11244
11491
  if (branchCheck.status === 0) {
11245
11492
  const settled = findSettledBatchRecord(root, initiativeSlug);
11246
- const currentBranch = spawnSync6("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], { encoding: "utf8" });
11247
- if (findExistingActiveBatch(root, initiativeSlug) === null && settled?.branch === branch && currentBranch.status === 0 && currentBranch.stdout.trim() === branch && headResult.stdout.trim() === baseHead && spawnSync6("git", ["-C", root, "merge-base", "--is-ancestor", expectedBatchHead(settled), baseHead]).status === 0)
11493
+ const currentBranch = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], { encoding: "utf8" });
11494
+ if (findExistingActiveBatch(root, initiativeSlug) === null && settled?.branch === branch && currentBranch.status === 0 && currentBranch.stdout.trim() === branch && headResult.stdout.trim() === baseHead && spawnSync8("git", ["-C", root, "merge-base", "--is-ancestor", expectedBatchHead(settled), baseHead]).status === 0)
11248
11495
  return { ok: true, branch };
11249
11496
  return {
11250
11497
  ok: false,
@@ -11252,25 +11499,25 @@ function runBatchGitPreflight(input) {
11252
11499
  message: `branch refs/heads/${branch} already exists`
11253
11500
  };
11254
11501
  }
11255
- const originalBranchResult = spawnSync6("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11502
+ const originalBranchResult = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11256
11503
  encoding: "utf8",
11257
11504
  stdio: ["ignore", "pipe", "pipe"]
11258
11505
  });
11259
11506
  const originalBranch = originalBranchResult.stdout.trim();
11260
- const checkoutResult = spawnSync6("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", "-b", branch, baseHead], {
11507
+ const checkoutResult = spawnSync8("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", "-b", branch, baseHead], {
11261
11508
  encoding: "utf8",
11262
11509
  stdio: ["ignore", "pipe", "pipe"],
11263
11510
  env: DEFAULT_GIT_ENV
11264
11511
  });
11265
11512
  if (checkoutResult.status !== 0) {
11266
11513
  const stderr = checkoutResult.stderr?.trim() || "";
11267
- const branchExists = stderr.includes("already exists") || spawnSync6("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
11268
- const currentBranchCheck = spawnSync6("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11514
+ const branchExists = stderr.includes("already exists") || spawnSync8("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
11515
+ const currentBranchCheck = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11269
11516
  encoding: "utf8",
11270
11517
  stdio: ["ignore", "pipe", "pipe"]
11271
11518
  }).stdout.trim();
11272
11519
  if (currentBranchCheck === branch && originalBranch && originalBranch !== branch) {
11273
- spawnSync6("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", originalBranch], {
11520
+ spawnSync8("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", originalBranch], {
11274
11521
  stdio: ["ignore", "ignore", "ignore"]
11275
11522
  });
11276
11523
  }
@@ -11293,7 +11540,7 @@ function hasBoundaryWhitespace(path) {
11293
11540
  return path.split("/").some((segment) => segment.trim() !== segment || segment.length === 0);
11294
11541
  }
11295
11542
  function getUnsupportedIndexFlags(root) {
11296
- const result = spawnSync6("git", ["-C", root, "ls-files", "-v", "-z", "--"], {
11543
+ const result = spawnSync8("git", ["-C", root, "ls-files", "-v", "-z", "--"], {
11297
11544
  encoding: "utf8",
11298
11545
  stdio: ["ignore", "pipe", "pipe"]
11299
11546
  });
@@ -11327,13 +11574,13 @@ function isPathAllowedForChild(path, taskId, scopeHint) {
11327
11574
  });
11328
11575
  }
11329
11576
  function getChangedProjectPaths(root) {
11330
- const tracked = spawnSync6("git", ["-C", root, "diff-index", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
11577
+ const tracked = spawnSync8("git", ["-C", root, "diff-index", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
11331
11578
  encoding: "utf8",
11332
11579
  stdio: ["ignore", "pipe", "pipe"]
11333
11580
  });
11334
11581
  if (tracked.status !== 0)
11335
11582
  throw new Error("failed to inspect tracked diff vs HEAD");
11336
- const untracked = spawnSync6("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
11583
+ const untracked = spawnSync8("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
11337
11584
  encoding: "utf8",
11338
11585
  stdio: ["ignore", "pipe", "pipe"]
11339
11586
  });
@@ -11343,7 +11590,7 @@ function getChangedProjectPaths(root) {
11343
11590
  return [...new Set([...splitZ(tracked.stdout), ...splitZ(untracked.stdout)])];
11344
11591
  }
11345
11592
  function getStagedProjectPaths(root) {
11346
- const staged = spawnSync6("git", ["-C", root, "diff-index", "--cached", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
11593
+ const staged = spawnSync8("git", ["-C", root, "diff-index", "--cached", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
11347
11594
  encoding: "utf8",
11348
11595
  stdio: ["ignore", "pipe", "pipe"]
11349
11596
  });
@@ -11352,13 +11599,13 @@ function getStagedProjectPaths(root) {
11352
11599
  return staged.stdout.split("\x00").filter((p) => p.length > 0);
11353
11600
  }
11354
11601
  function getUnstagedProjectPaths(root) {
11355
- const diffFiles = spawnSync6("git", ["-C", root, "diff-files", "--name-only", "-z", "--ignore-submodules=none", "--"], {
11602
+ const diffFiles = spawnSync8("git", ["-C", root, "diff-files", "--name-only", "-z", "--ignore-submodules=none", "--"], {
11356
11603
  encoding: "utf8",
11357
11604
  stdio: ["ignore", "pipe", "pipe"]
11358
11605
  });
11359
11606
  if (diffFiles.status !== 0)
11360
11607
  throw new Error("failed to inspect unstaged tracked changes");
11361
- const untracked = spawnSync6("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
11608
+ const untracked = spawnSync8("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
11362
11609
  encoding: "utf8",
11363
11610
  stdio: ["ignore", "pipe", "pipe"]
11364
11611
  });
@@ -11368,7 +11615,7 @@ function getUnstagedProjectPaths(root) {
11368
11615
  return [...new Set([...splitZ(diffFiles.stdout), ...splitZ(untracked.stdout)])];
11369
11616
  }
11370
11617
  function getCommittedDeltaPaths(root) {
11371
- const delta = spawnSync6("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", "HEAD~1", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
11618
+ const delta = spawnSync8("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", "HEAD~1", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
11372
11619
  if (delta.status !== 0)
11373
11620
  throw new Error("failed to inspect committed tree delta");
11374
11621
  return delta.stdout.split("\x00").filter((p) => p.length > 0);
@@ -11474,7 +11721,7 @@ async function commitBatchChild(input) {
11474
11721
  const goal = typeof intentSnapshot.goal === "string" ? intentSnapshot.goal : "";
11475
11722
  const scopeHint = Array.isArray(intentSnapshot.scope_hint) ? intentSnapshot.scope_hint.filter((s) => typeof s === "string") : [];
11476
11723
  if (expectedBranch !== undefined) {
11477
- const currentBranchResult = spawnSync6("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11724
+ const currentBranchResult = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11478
11725
  encoding: "utf8",
11479
11726
  stdio: ["ignore", "pipe", "pipe"]
11480
11727
  });
@@ -11483,7 +11730,7 @@ async function commitBatchChild(input) {
11483
11730
  throw new Error(`batch_head_lineage_broken: current branch ${currentBranch} does not match expected branch ${expectedBranch}`);
11484
11731
  }
11485
11732
  }
11486
- const currentHeadResult = spawnSync6("git", ["-C", root, "rev-parse", "HEAD"], {
11733
+ const currentHeadResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
11487
11734
  encoding: "utf8",
11488
11735
  stdio: ["ignore", "pipe", "pipe"]
11489
11736
  });
@@ -11502,7 +11749,7 @@ async function commitBatchChild(input) {
11502
11749
  }
11503
11750
  const pathsToStage = getUnstagedProjectPaths(root);
11504
11751
  if (pathsToStage.length > 0) {
11505
- const addResult = spawnSync6("git", ["-C", root, "--literal-pathspecs", "add", "-A", "--", ...pathsToStage], {
11752
+ const addResult = spawnSync8("git", ["-C", root, "--literal-pathspecs", "add", "-A", "--", ...pathsToStage], {
11506
11753
  encoding: "utf8",
11507
11754
  stdio: ["ignore", "pipe", "pipe"]
11508
11755
  });
@@ -11517,7 +11764,7 @@ async function commitBatchChild(input) {
11517
11764
  const staged = getStagedProjectPaths(root);
11518
11765
  const stagedOutside = staged.filter((p) => !isPathAllowedForChild(p, taskId, scopeHint));
11519
11766
  if (stagedOutside.length > 0) {
11520
- spawnSync6("git", ["-C", root, "reset", "--quiet"], { stdio: ["ignore", "ignore", "ignore"] });
11767
+ spawnSync8("git", ["-C", root, "reset", "--quiet"], { stdio: ["ignore", "ignore", "ignore"] });
11521
11768
  throw new Error("dirty_outside_scope");
11522
11769
  }
11523
11770
  const record = auditPair.record;
@@ -11535,7 +11782,7 @@ async function commitBatchChild(input) {
11535
11782
 
11536
11783
  Immune-Brain-Batch: ${batchId}
11537
11784
  `;
11538
- const commitResult = spawnSync6("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "commit", "--no-verify", "-F", "-"], {
11785
+ const commitResult = spawnSync8("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "commit", "--no-verify", "-F", "-"], {
11539
11786
  input: commitMessage,
11540
11787
  encoding: "utf8",
11541
11788
  stdio: ["pipe", "pipe", "pipe"],
@@ -11544,7 +11791,7 @@ Immune-Brain-Batch: ${batchId}
11544
11791
  if (commitResult.status !== 0) {
11545
11792
  throw new Error(`commit failed for child ${taskId}: ${commitResult.stderr?.trim() || "git commit failed"}`);
11546
11793
  }
11547
- const newHeadResult = spawnSync6("git", ["-C", root, "rev-parse", "HEAD"], {
11794
+ const newHeadResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
11548
11795
  encoding: "utf8",
11549
11796
  stdio: ["ignore", "pipe", "pipe"]
11550
11797
  });
@@ -11552,7 +11799,7 @@ Immune-Brain-Batch: ${batchId}
11552
11799
  if (newHeadResult.status !== 0 || !newHead || newHead === expectedHead) {
11553
11800
  throw new Error("commit_failed");
11554
11801
  }
11555
- const parentsResult = spawnSync6("git", ["-C", root, "rev-parse", "HEAD^@"], {
11802
+ const parentsResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD^@"], {
11556
11803
  encoding: "utf8",
11557
11804
  stdio: ["ignore", "pipe", "pipe"]
11558
11805
  });
@@ -11585,7 +11832,7 @@ async function lookupBatchCommit(input) {
11585
11832
  let candidates = [];
11586
11833
  let evidenceBacked = false;
11587
11834
  if (evidence && evidence.commit) {
11588
- const direct = spawnSync6("git", ["-C", root, "log", "-n", "1", `--format=${FORMAT}`, evidence.commit, "--"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
11835
+ const direct = spawnSync8("git", ["-C", root, "log", "-n", "1", `--format=${FORMAT}`, evidence.commit, "--"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
11589
11836
  if (direct.status !== 0) {
11590
11837
  throw new Error(`failed to inspect batch commit for ${taskId}: ${direct.stderr?.trim() || "git log failed"}`);
11591
11838
  }
@@ -11598,7 +11845,7 @@ async function lookupBatchCommit(input) {
11598
11845
  }
11599
11846
  }
11600
11847
  if (!candidates.length) {
11601
- const result = spawnSync6("git", [
11848
+ const result = spawnSync8("git", [
11602
11849
  "-C",
11603
11850
  root,
11604
11851
  "log",
@@ -11635,7 +11882,7 @@ async function lookupBatchCommit(input) {
11635
11882
  throw new Error(`batch_head_lineage_broken: adopted commit author ${match.authorName} does not match batch authority ${DEFAULT_GIT_ENV.GIT_AUTHOR_NAME}`);
11636
11883
  }
11637
11884
  if (expectedBranch !== undefined) {
11638
- const currentBranchResult = spawnSync6("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11885
+ const currentBranchResult = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11639
11886
  encoding: "utf8",
11640
11887
  stdio: ["ignore", "pipe", "pipe"]
11641
11888
  });
@@ -11645,15 +11892,17 @@ async function lookupBatchCommit(input) {
11645
11892
  }
11646
11893
  }
11647
11894
  if (expectedHead !== undefined) {
11648
- const currentHeadResult = spawnSync6("git", ["-C", root, "rev-parse", "HEAD"], {
11895
+ const currentHeadResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
11649
11896
  encoding: "utf8",
11650
11897
  stdio: ["ignore", "pipe", "pipe"]
11651
11898
  });
11652
11899
  const currentHead = currentHeadResult.stdout.trim();
11653
- if (currentHead !== commit) {
11900
+ if (currentHead !== commit && spawnSync8("git", ["-C", root, "merge-base", "--is-ancestor", commit, currentHead], {
11901
+ stdio: ["ignore", "ignore", "ignore"]
11902
+ }).status !== 0) {
11654
11903
  throw new Error(`batch_head_lineage_broken: current HEAD ${currentHead} diverged from adopted commit ${commit}`);
11655
11904
  }
11656
- const parentsResult = spawnSync6("git", ["-C", root, "rev-parse", `${commit}^@`], {
11905
+ const parentsResult = spawnSync8("git", ["-C", root, "rev-parse", `${commit}^@`], {
11657
11906
  encoding: "utf8",
11658
11907
  stdio: ["ignore", "pipe", "pipe"]
11659
11908
  });
@@ -11671,7 +11920,7 @@ async function lookupBatchCommit(input) {
11671
11920
  throw new Error(`batch_head_lineage_broken: adopted commit task lifecycle is not done: ${lifecycle}`);
11672
11921
  }
11673
11922
  const scopeHint = Array.isArray(auditPair.record.intent_snapshot.scope_hint) ? auditPair.record.intent_snapshot.scope_hint.filter((s) => typeof s === "string") : [];
11674
- const deltaResult = spawnSync6("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", expectedHead, commit], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
11923
+ const deltaResult = spawnSync8("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", expectedHead, commit], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
11675
11924
  if (deltaResult.status !== 0) {
11676
11925
  throw new Error("batch_head_lineage_broken: failed to inspect adopted commit delta");
11677
11926
  }
@@ -11710,18 +11959,12 @@ function skipDependents(record, taskId, reason) {
11710
11959
  }
11711
11960
  record.children = record.children.map((child) => skip.has(child.task_id) && child.state === "pending" ? { ...child, state: "skipped_blocked", reason } : child);
11712
11961
  }
11713
- function budgetStopReason(record, now) {
11962
+ function budgetStopReason(record) {
11714
11963
  const enrolledCount = record.children.filter((child) => child.state !== "pending" && child.state !== "skipped_blocked").length;
11715
11964
  if (enrolledCount >= record.budget.max_children)
11716
11965
  return `max_children budget exhausted (${record.budget.max_children})`;
11717
- const deadline = Date.parse(record.budget.deadline_at);
11718
- if (!Number.isNaN(deadline) && now >= deadline)
11719
- return `deadline_at reached (${record.budget.deadline_at})`;
11720
11966
  return null;
11721
11967
  }
11722
- function isAuthorizationExpiryError(error) {
11723
- return error instanceof BatchAuthorizationExpiryError;
11724
- }
11725
11968
  function requireFreshProjection(result, taskId) {
11726
11969
  if (result.error !== null)
11727
11970
  throw new Error(`cannot reconcile Kernel projection for ${taskId}: ${result.error}`);
@@ -11732,7 +11975,7 @@ function nextEnrollableChild(record) {
11732
11975
  }
11733
11976
  var TERMINAL_NEXT_ACTIONS = {
11734
11977
  completed: "The batch settled every enrollable child; review the commits and the tracker.",
11735
- budget_stopped: "Budget, deadline, or authorization expiry stopped new enrollments; re-confirm to continue under a new authorization.",
11978
+ budget_stopped: "The child budget stopped new enrollments; re-confirm to continue under a new authorization.",
11736
11979
  failed: "A commit or lineage failure stopped the batch; inspect the failing child and the branch state.",
11737
11980
  rejected: "The batch was rejected before any enrollment; correct the stated reason and re-confirm.",
11738
11981
  needs_human: "A parked child needs a human decision; resolve it, then re-confirm to continue.",
@@ -11787,28 +12030,44 @@ function failPersistedLineage(root, existing, message) {
11787
12030
  }
11788
12031
  return writeBatchRunState(root, record);
11789
12032
  }
11790
- function externalHeadDriftMessage(root, record) {
12033
+ function reconcileLineage(root, record) {
11791
12034
  if (!existsSync9(join14(root, ".git")))
11792
- return null;
11793
- const head = record.commits.length ? record.commits[record.commits.length - 1] : record.base_head;
11794
- const headCheck = spawnSync7("git", ["-C", root, "rev-parse", "HEAD"], {
11795
- encoding: "utf8",
11796
- stdio: ["ignore", "pipe", "pipe"]
11797
- });
11798
- if (headCheck.status === 0 && headCheck.stdout.trim() && headCheck.stdout.trim() !== head) {
11799
- return `batch_head_lineage_broken: current HEAD ${headCheck.stdout.trim()} does not match expected batch head ${head}`;
11800
- }
11801
- const branchCheck = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11802
- encoding: "utf8",
11803
- stdio: ["ignore", "pipe", "pipe"]
12035
+ return { record, failure: null };
12036
+ const expected = expectedBatchHead(record);
12037
+ const lineage = classifyBatchLineage({
12038
+ root,
12039
+ branch: record.branch ?? "",
12040
+ expectedHead: expected,
12041
+ childCommits: record.commits,
12042
+ batchId: record.batch_id
11804
12043
  });
11805
- const currentBranch = branchCheck.stdout.trim();
11806
- if (branchCheck.status !== 0 || currentBranch !== record.branch) {
11807
- return `batch_head_lineage_broken: current branch ${currentBranch} does not match expected batch branch ${record.branch}`;
11808
- }
11809
- return null;
12044
+ if (lineage.kind === "broken")
12045
+ return { record, failure: lineage.message };
12046
+ if (lineage.kind === "equal")
12047
+ return { record, failure: null };
12048
+ return {
12049
+ record: writeBatchRunState(root, {
12050
+ ...record,
12051
+ adopted_heads: [...record.adopted_heads ?? [], { from: expected, to: lineage.head }]
12052
+ }),
12053
+ failure: null
12054
+ };
11810
12055
  }
11811
12056
  async function validatePersistedRun(input, record) {
12057
+ if (!record.commits.length && record.children[0]?.state === "settled" && existsSync9(join14(input.root, ".git"))) {
12058
+ const head = spawnSync9("git", ["-C", input.root, "rev-parse", "HEAD"], { encoding: "utf8" });
12059
+ const live = head.stdout.trim();
12060
+ const expected = expectedBatchHead(record);
12061
+ const adoptable = () => classifyBatchLineage({
12062
+ root: input.root,
12063
+ branch: record.branch ?? "",
12064
+ expectedHead: expected,
12065
+ childCommits: record.commits,
12066
+ batchId: record.batch_id
12067
+ }).kind === "fast_forward";
12068
+ if (head.status !== 0 || live !== expected && !ownUnpersistedBatchHead(input.root, record, live) && !adoptable())
12069
+ throw new Error("first unpersisted batch commit provenance is invalid");
12070
+ }
11812
12071
  const plan = input.registry.children(input.capability);
11813
12072
  if (record.plan_digest !== computeBatchPlanDigest(plan) || record.children.length !== plan.length || record.children.some((child, index) => {
11814
12073
  const expected = plan[index];
@@ -11830,7 +12089,7 @@ async function validatePersistedRun(input, record) {
11830
12089
  });
11831
12090
  if (!evidence || evidence.commit !== child.commit) {
11832
12091
  if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync9(join14(input.root, ".git"))) {
11833
- const reach = spawnSync7("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
12092
+ const reach = spawnSync9("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
11834
12093
  if (reach.status !== 0) {
11835
12094
  throw new Error(`batch_head_lineage_broken: recorded commit ${child.commit} for ${child.task_id} is no longer reachable from HEAD`);
11836
12095
  }
@@ -11854,8 +12113,7 @@ function validateRunAuthorization(input, existing) {
11854
12113
  plan_digest: existing?.plan_digest ?? input.plan_digest,
11855
12114
  base_head: existing?.base_head ?? input.base_head,
11856
12115
  initiative_slug: existing?.initiative_slug ?? input.initiative_slug,
11857
- budget: input.budget,
11858
- expires_at: input.authorization_expires_at
12116
+ budget: input.budget
11859
12117
  }
11860
12118
  });
11861
12119
  if (authorized.issued_at !== input.confirmation_time || existing && Date.parse(authorized.issued_at) <= Date.parse(existing.confirmation_time))
@@ -11868,6 +12126,40 @@ function validateRunAuthorization(input, existing) {
11868
12126
  });
11869
12127
  if (computeBatchPlanDigest(children) !== authorized.plan_digest || input.plan_digest !== authorized.plan_digest || input.base_head !== authorized.base_head)
11870
12128
  throw new Error("batch run input does not match the authorized plan or base_head");
12129
+ return authorized;
12130
+ }
12131
+ function applyPlanReconfirmation(input, existing) {
12132
+ if (existing.plan_digest === input.plan_digest)
12133
+ return existing;
12134
+ const next = {
12135
+ ...existing,
12136
+ plan_digest: input.plan_digest,
12137
+ confirmation_time: input.confirmation_time,
12138
+ budget: input.budget
12139
+ };
12140
+ if (!existing.branch || input.batch_id !== existing.batch_id || input.initiative_slug !== existing.initiative_slug || input.base_head !== existing.base_head || input.budget.max_children !== existing.budget.max_children || input.budget.qa_failure_limit !== existing.budget.qa_failure_limit)
12141
+ throw new Error("plan_digest mismatch: reconfirmation binding changed");
12142
+ const validate = () => {
12143
+ const plan = input.registry.children(input.capability);
12144
+ if (existing.children.length !== plan.length || plan.some((child, index) => {
12145
+ const old = existing.children[index], nextChild = input.children[index];
12146
+ return !nextChild || old.task_id !== child.task_id || old.slice_id !== nextChild.slice_id || JSON.stringify(old.blocked_by) !== JSON.stringify(child.blocked_by) || JSON.stringify(nextChild.blocked_by) !== JSON.stringify(child.blocked_by);
12147
+ }))
12148
+ throw new Error("plan_digest mismatch: reconfirmation topology changed");
12149
+ const { issued_at: _issuedAt, ...binding } = validateRunAuthorization(input, { ...existing, plan_digest: input.plan_digest });
12150
+ if (binding.branch !== existing.branch)
12151
+ throw new Error("plan_digest mismatch: reconfirmation branch changed");
12152
+ prepareBatchRunState(input);
12153
+ return input.registry.inspect(input.capability, binding);
12154
+ };
12155
+ const authority = validate();
12156
+ const captured = takeReconfirmation(authority.nonce);
12157
+ if (captured.root !== input.root || captured.plan_digest !== input.plan_digest)
12158
+ throw new Error("plan_digest mismatch: captured reconfirmation does not match run input");
12159
+ return replaceBatchRunState(input.root, captured.stateBytes, next, () => {
12160
+ captured.assertUnchanged();
12161
+ validate();
12162
+ });
11871
12163
  }
11872
12164
  async function startBatch(input) {
11873
12165
  try {
@@ -11898,7 +12190,9 @@ async function startBatchLocked(input) {
11898
12190
  return reportFor({ ...rejected, batch_state: "rejected" }, "batch plan is empty", "Provide a non-empty enrollable child plan.");
11899
12191
  }
11900
12192
  let existing = readBatchRunState(input.root, input.batch_id);
12193
+ const reconfirmed = existing !== null && existing.plan_digest !== input.plan_digest;
11901
12194
  if (existing) {
12195
+ existing = applyPlanReconfirmation(input, existing);
11902
12196
  try {
11903
12197
  await validatePersistedRun(input, existing);
11904
12198
  } catch (error) {
@@ -11918,12 +12212,12 @@ async function startBatchLocked(input) {
11918
12212
  if (existing?.batch_state === "needs_human") {
11919
12213
  const priorConfirmation = Date.parse(existing.confirmation_time);
11920
12214
  const nextConfirmation = Date.parse(input.confirmation_time);
11921
- const nextExpiry = Date.parse(input.authorization_expires_at);
11922
- const freshAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation && Number.isFinite(nextExpiry) && nextExpiry > Date.now();
11923
- if (!freshAuthorization) {
12215
+ const freshAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation;
12216
+ if (!freshAuthorization && !reconfirmed) {
11924
12217
  return finalize(input.root, existing, "the parked batch requires a fresh literal-user confirmation", "Resolve the parked child, then re-confirm the batch to continue.");
11925
12218
  }
11926
- validateRunAuthorization(input, existing);
12219
+ if (!reconfirmed)
12220
+ validateRunAuthorization(input, existing);
11927
12221
  prepareBatchRunState({ ...input, now: input.now });
11928
12222
  const remapped = await Promise.all(existing.children.map(async (child) => {
11929
12223
  if (child.state !== "needs_human")
@@ -11945,7 +12239,6 @@ async function startBatchLocked(input) {
11945
12239
  existing = writeBatchRunState(input.root, {
11946
12240
  ...reparked,
11947
12241
  confirmation_time: input.confirmation_time,
11948
- authorization_expires_at: input.authorization_expires_at,
11949
12242
  budget: input.budget,
11950
12243
  batch_state: "needs_human"
11951
12244
  });
@@ -11955,28 +12248,11 @@ async function startBatchLocked(input) {
11955
12248
  existing = writeBatchRunState(input.root, {
11956
12249
  ...existing,
11957
12250
  confirmation_time: input.confirmation_time,
11958
- authorization_expires_at: input.authorization_expires_at,
11959
12251
  budget: input.budget,
11960
12252
  batch_state: "running",
11961
12253
  children: resumedChildren
11962
12254
  });
11963
12255
  }
11964
- if (existing?.batch_state === "running") {
11965
- const nextConfirmation = Date.parse(input.confirmation_time);
11966
- const nextExpiry = Date.parse(input.authorization_expires_at);
11967
- const priorConfirmation = Date.parse(existing.confirmation_time);
11968
- const persistedExpiry = Date.parse(existing.authorization_expires_at);
11969
- const renewedAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation && Number.isFinite(nextExpiry) && nextExpiry > Date.now() && nextExpiry > persistedExpiry;
11970
- if (renewedAuthorization) {
11971
- validateRunAuthorization(input, existing);
11972
- existing = writeBatchRunState(input.root, {
11973
- ...existing,
11974
- confirmation_time: input.confirmation_time,
11975
- authorization_expires_at: input.authorization_expires_at,
11976
- budget: input.budget
11977
- });
11978
- }
11979
- }
11980
12256
  if (existing) {
11981
12257
  const interruptedChild = existing.children.find((child) => child.state === "enrolled" || child.state === "settled");
11982
12258
  if (interruptedChild) {
@@ -12014,7 +12290,6 @@ async function startBatchLocked(input) {
12014
12290
  plan_digest: input.plan_digest,
12015
12291
  base_head: input.base_head,
12016
12292
  confirmation_time: input.confirmation_time,
12017
- authorization_expires_at: input.authorization_expires_at,
12018
12293
  budget: input.budget,
12019
12294
  now: input.now
12020
12295
  });
@@ -12025,13 +12300,14 @@ async function startBatchLocked(input) {
12025
12300
  record.batch_state = "running";
12026
12301
  persist();
12027
12302
  }
12028
- let head = record.commits.length ? record.commits[record.commits.length - 1] : record.base_head;
12029
- const driftMessage = externalHeadDriftMessage(input.root, record);
12030
- if (driftMessage) {
12303
+ const lineage = reconcileLineage(input.root, record);
12304
+ record = lineage.record;
12305
+ if (lineage.failure) {
12031
12306
  record.batch_state = "failed";
12032
12307
  persist();
12033
- return finalize(input.root, record, driftMessage, "");
12308
+ return finalize(input.root, record, lineage.failure, "");
12034
12309
  }
12310
+ let head = expectedBatchHead(record);
12035
12311
  while (record.batch_state === "running") {
12036
12312
  const child = nextEnrollableChild(record);
12037
12313
  if (!child) {
@@ -12058,8 +12334,7 @@ async function startBatchLocked(input) {
12058
12334
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "enrolled", reason: "adopted existing batch claim after interruption" } : c);
12059
12335
  persist();
12060
12336
  } else {
12061
- const now = Date.now();
12062
- if (now >= Date.parse(record.authorization_expires_at) || budgetStopReason(record, now)) {
12337
+ if (budgetStopReason(record)) {
12063
12338
  record.batch_state = record.children.some((c) => c.state === "needs_human" || c.state === "skipped_blocked") ? "needs_human" : "budget_stopped";
12064
12339
  persist();
12065
12340
  break;
@@ -12074,8 +12349,7 @@ async function startBatchLocked(input) {
12074
12349
  plan_digest: record.plan_digest,
12075
12350
  base_head: record.base_head,
12076
12351
  initiative_slug: record.initiative_slug,
12077
- budget: record.budget,
12078
- expires_at: record.authorization_expires_at
12352
+ budget: record.budget
12079
12353
  }
12080
12354
  });
12081
12355
  for (const committed of record.children) {
@@ -12095,12 +12369,6 @@ async function startBatchLocked(input) {
12095
12369
  persist();
12096
12370
  } catch (error) {
12097
12371
  const message = error instanceof Error ? error.message : String(error);
12098
- if (isAuthorizationExpiryError(error)) {
12099
- record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "pending", reason: message } : c);
12100
- record.batch_state = "budget_stopped";
12101
- persist();
12102
- break;
12103
- }
12104
12372
  if (isLineageBreakError(message)) {
12105
12373
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
12106
12374
  skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
@@ -12123,7 +12391,7 @@ async function startBatchLocked(input) {
12123
12391
  function stopReasonFor(record) {
12124
12392
  switch (record.batch_state) {
12125
12393
  case "budget_stopped":
12126
- return budgetStopReason(record, Date.now()) ?? "budget, deadline, or authorization expiry stopped new enrollments";
12394
+ return budgetStopReason(record) ?? "the child budget stopped new enrollments";
12127
12395
  case "failed": {
12128
12396
  const failedChild = record.children.find((c) => c.state === "needs_human" && c.reason);
12129
12397
  return failedChild?.reason ?? "a commit or lineage failure stopped the batch";
@@ -12138,8 +12406,9 @@ function stopReasonFor(record) {
12138
12406
  }
12139
12407
  async function resumeBatch(input, projection) {
12140
12408
  let existing = readBatchRunState(input.root, input.batch_id);
12141
- if (!existing)
12409
+ if (!existing || existing.batch_state === "needs_human")
12142
12410
  return startBatch(input);
12411
+ existing = applyPlanReconfirmation(input, existing);
12143
12412
  try {
12144
12413
  await validatePersistedRun(input, existing);
12145
12414
  } catch (error) {
@@ -12154,15 +12423,14 @@ async function resumeBatch(input, projection) {
12154
12423
  }
12155
12424
  if (isTerminalBatchState(existing.batch_state))
12156
12425
  return replayTerminal(input.root, existing);
12157
- if (existing.batch_state === "needs_human")
12158
- return startBatch(input);
12159
12426
  const driven = existing.children.find((child) => child.state === "enrolled" || child.state === "settled");
12160
12427
  if (driven) {
12161
12428
  if (driven.state === "enrolled") {
12162
- const drivenDrift = externalHeadDriftMessage(input.root, existing);
12163
- if (drivenDrift) {
12164
- const failed = failPersistedLineage(input.root, existing, drivenDrift);
12165
- return finalize(input.root, failed, drivenDrift, "");
12429
+ const drivenLineage = reconcileLineage(input.root, existing);
12430
+ existing = drivenLineage.record;
12431
+ if (drivenLineage.failure) {
12432
+ const failed = failPersistedLineage(input.root, existing, drivenLineage.failure);
12433
+ return finalize(input.root, failed, drivenLineage.failure, "");
12166
12434
  }
12167
12435
  const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, driven.task_id), driven.task_id);
12168
12436
  const holdsClaim = fresh.claim !== null && fresh.claim.task_id === driven.task_id && input.kernel.ownsTaskClaim(driven.task_id);
@@ -12241,7 +12509,7 @@ async function driveInterruptedChild(input, child) {
12241
12509
  record.batch_state = "running";
12242
12510
  persist();
12243
12511
  }
12244
- let head = record.commits.length ? record.commits[record.commits.length - 1] : record.base_head;
12512
+ let head = expectedBatchHead(record);
12245
12513
  while (child.state === "enrolled" || child.state === "settled") {
12246
12514
  if (child.state === "settled") {
12247
12515
  let existing = null;
@@ -12262,6 +12530,10 @@ async function driveInterruptedChild(input, child) {
12262
12530
  persist();
12263
12531
  throw new BatchCommitAbortError(`commit lookup failed: ${message}`);
12264
12532
  }
12533
+ if (!existing) {
12534
+ record = reconcileLineage(input.root, record).record;
12535
+ head = expectedBatchHead(record);
12536
+ }
12265
12537
  const planChild = input.children.find((c) => c.task_id === child.task_id);
12266
12538
  const intentPath = planChild?.intent_path ?? undefined;
12267
12539
  const doCommit = async () => {
@@ -12280,6 +12552,8 @@ async function driveInterruptedChild(input, child) {
12280
12552
  intentPath
12281
12553
  });
12282
12554
  };
12555
+ if (existing && !record.commits.length && existsSync9(join14(input.root, ".git")) && !ownUnpersistedBatchHead(input.root, record, existing.commit))
12556
+ throw new Error("first unpersisted batch commit provenance is invalid");
12283
12557
  const adopted = existing ?? await doCommit().catch((error) => {
12284
12558
  const message = error instanceof Error ? error.message : String(error);
12285
12559
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
@@ -12555,7 +12829,6 @@ async function mintCapability(registry, input) {
12555
12829
  diff_hash: input.diff_hash,
12556
12830
  actor_id: input.actor_id,
12557
12831
  confirmation_ref: input.confirmation_ref,
12558
- expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
12559
12832
  findings_digest: input.action_kind === "request_rework" ? findingsDigestV2(input.findings) : null
12560
12833
  };
12561
12834
  return registry.issue(binding);
@@ -12692,7 +12965,6 @@ class ClaudeRuntime {
12692
12965
  preparation_digest: preparation.digest,
12693
12966
  actor_id: LITERAL_USER_ACTOR_ID,
12694
12967
  confirmation_ref: gate.confirmation_ref,
12695
- expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
12696
12968
  nonce
12697
12969
  };
12698
12970
  const capability = this.enrollmentRegistry.issue(binding);
@@ -12716,11 +12988,41 @@ class ClaudeRuntime {
12716
12988
  throw error;
12717
12989
  }
12718
12990
  }
12719
- async advance(taskId, signal) {
12720
- return this.withTerminalTracker(taskId, await this.coordinator.advance(taskId, { cwd: this.cwd }, signal));
12991
+ async advance(taskId, signal, meta) {
12992
+ return this.closeOutBatchChild(taskId, await this.withTerminalTracker(taskId, await this.coordinator.advance(taskId, { cwd: this.cwd }, signal)), meta);
12993
+ }
12994
+ async submitReview(taskId, verdictInput, meta) {
12995
+ return this.closeOutBatchChild(taskId, await this.withTerminalTracker(taskId, await submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput)), meta);
12721
12996
  }
12722
- async submitReview(taskId, verdictInput) {
12723
- return this.withTerminalTracker(taskId, await submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput));
12997
+ async closeOutBatchChild(taskId, result, meta) {
12998
+ if (!meta || result === null || typeof result !== "object")
12999
+ return result;
13000
+ const lifecycle = result.record?.lifecycle;
13001
+ if (result.state !== "completed" && lifecycle !== "done")
13002
+ return result;
13003
+ let slug;
13004
+ try {
13005
+ slug = findResumableBatchSlugForTask(this.cwd, taskId);
13006
+ } catch {
13007
+ return result;
13008
+ }
13009
+ if (!slug)
13010
+ return result;
13011
+ try {
13012
+ if (existsSync10(join15(this.cwd, ".imm", "audit", taskId)))
13013
+ execFileSync7("git", ["-C", this.cwd, "add", "--", `.imm/audit/${taskId}`], { stdio: "ignore" });
13014
+ const batch = await this.startUnattendedBatch(slug, meta, { reuseOnly: true });
13015
+ return { ...result, batch };
13016
+ } catch (error) {
13017
+ return {
13018
+ ...result,
13019
+ batch: {
13020
+ state: "rejected",
13021
+ reason: `batch continuation failed: ${error instanceof Error ? error.message : String(error)}`,
13022
+ recovery_action: "call start_unattended_batch with the same Initiative to continue the batch"
13023
+ }
13024
+ };
13025
+ }
12724
13026
  }
12725
13027
  async withTerminalTracker(taskId, result) {
12726
13028
  if (result === null || typeof result !== "object")
@@ -13022,15 +13324,16 @@ class ClaudeRuntime {
13022
13324
  throw error;
13023
13325
  }
13024
13326
  }
13025
- async startUnattendedBatch(initiativeSlug, meta) {
13327
+ async startUnattendedBatch(initiativeSlug, meta, options = {}) {
13026
13328
  throwIfCancelled(meta.signal);
13329
+ const reuseOnly = options.reuseOnly === true;
13027
13330
  const probe = probeHost(this.env, process.platform, this.hostVersion);
13028
13331
  if (!probe.ok)
13029
13332
  throw new NativeAuthorityError("unsupported_host", probe.reason);
13030
13333
  const interactive = meta.interactive ?? this.interactive;
13031
- if (!interactive)
13334
+ if (!reuseOnly && !interactive)
13032
13335
  throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
13033
- if (!this.requestConfirmation)
13336
+ if (!reuseOnly && !this.requestConfirmation)
13034
13337
  throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
13035
13338
  let now = new Date().toISOString();
13036
13339
  const preflight = await projectBatchPreflight({
@@ -13060,7 +13363,15 @@ class ClaudeRuntime {
13060
13363
  readInitiative: this.readInitiative ?? observeGithubInitiative,
13061
13364
  nonce: enrollmentNonce(),
13062
13365
  gate: async (facts) => {
13063
- const deadline = startConfirmationDeadline({ env: this.env, signal: meta.signal });
13366
+ if (reuseOnly)
13367
+ return {
13368
+ kind: "host_rejection",
13369
+ value: {
13370
+ state: "blocked",
13371
+ reason: `batch authorization cannot be reused (${facts.reuse_blockers.join(", ")})`,
13372
+ recovery_action: "call start_unattended_batch with the same Initiative to confirm a fresh authorization"
13373
+ }
13374
+ };
13064
13375
  let confirmationResult;
13065
13376
  try {
13066
13377
  confirmationResult = await this.requestConfirmation({
@@ -13082,17 +13393,14 @@ class ClaudeRuntime {
13082
13393
  reason: child.reason
13083
13394
  })),
13084
13395
  budget: facts.budget,
13085
- expires_at: facts.expires_at,
13086
13396
  ...facts.reuse_blockers.length > 0 ? {
13087
13397
  re_confirmation_required: facts.reuse_blockers,
13088
13398
  recovery: "confirm to issue a fresh authorization bound to the current plan and HEAD"
13089
13399
  } : {}
13090
13400
  },
13091
- signal: deadline.signal
13401
+ signal: meta.signal
13092
13402
  });
13093
13403
  } catch (err) {
13094
- if (deadline.timedOut())
13095
- return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
13096
13404
  if (meta.signal?.aborted)
13097
13405
  return { kind: "host_rejection", value: batchReason("cancelled_before_execution") };
13098
13406
  if (err instanceof NativeAuthorityError) {
@@ -13113,11 +13421,7 @@ class ClaudeRuntime {
13113
13421
  kind: "host_rejection",
13114
13422
  value: batchReason("confirmation_failed", err instanceof Error ? err.message : String(err))
13115
13423
  };
13116
- } finally {
13117
- deadline.clear();
13118
13424
  }
13119
- if (deadline.timedOut())
13120
- return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
13121
13425
  if (confirmationResult.decision === "cancel" && meta.signal?.aborted)
13122
13426
  return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
13123
13427
  if (meta.signal?.aborted)
@@ -13143,12 +13447,10 @@ class ClaudeRuntime {
13143
13447
  return authorization.value;
13144
13448
  if (authorization.outcome === "rejected")
13145
13449
  return authorization.rejection;
13146
- const { binding, batch_id: batchId, expires_at: expiresAt } = authorization;
13450
+ const { binding, batch_id: batchId } = authorization;
13147
13451
  if (meta.signal?.aborted)
13148
13452
  return batchReason("cancelled_before_execution");
13149
13453
  now = new Date().toISOString();
13150
- if (Date.parse(expiresAt) <= Date.parse(now))
13151
- return batchReason("confirmation_failed", "confirmed batch deadline expired before authorization issuance");
13152
13454
  const capability = this.batchRegistry.issue(binding, recoveryChildren, now);
13153
13455
  const basePort = this.createBatchKernelPort(this.batchRegistry, capability, binding);
13154
13456
  const kernelPort = {
@@ -13170,7 +13472,6 @@ class ClaudeRuntime {
13170
13472
  plan_digest: planDigest,
13171
13473
  base_head: binding.base_head,
13172
13474
  confirmation_time: now,
13173
- authorization_expires_at: expiresAt,
13174
13475
  budget,
13175
13476
  now,
13176
13477
  kernel: kernelPort,
@@ -13381,11 +13682,11 @@ function createMcpRuntime(options = {}) {
13381
13682
  if (name === "enroll")
13382
13683
  return runtime.enroll(taskId, toolMeta);
13383
13684
  if (name === "advance_assurance")
13384
- return runtime.advance(taskId, toolMeta.signal);
13685
+ return runtime.advance(taskId, toolMeta.signal, toolMeta);
13385
13686
  if (name === "submit_review") {
13386
13687
  if (!Object.hasOwn(args, "verdict"))
13387
13688
  throw new Error("verdict is required");
13388
- return runtime.submitReview(taskId, args.verdict);
13689
+ return runtime.submitReview(taskId, args.verdict, toolMeta);
13389
13690
  }
13390
13691
  if (name === "revise_intent") {
13391
13692
  if (!Object.hasOwn(args, "next_intent"))
@@ -13581,8 +13882,7 @@ ${b.children.map((c) => ` - ${c.task_id} (${c.slice_id}) [risk: ${c.risk ?? "un
13581
13882
  b?.excluded && b.excluded.length > 0 ? `Excluded children (${b.excluded.length}):
13582
13883
  ${b.excluded.map((e) => ` - ${e.task_id} (${e.slice_id}): ${e.reason}`).join(`
13583
13884
  `)}` : null,
13584
- b?.budget ? `Budget: max_children=${b.budget.max_children}, deadline_at=${b.budget.deadline_at}, qa_failure_limit=${b.budget.qa_failure_limit}` : null,
13585
- b?.expires_at ? `Expires at: ${b.expires_at}` : null
13885
+ b?.budget ? `Budget: max_children=${b.budget.max_children}, qa_failure_limit=${b.budget.qa_failure_limit}` : null
13586
13886
  ].filter(Boolean);
13587
13887
  return {
13588
13888
  mode: "form",