immune-brain 4.5.0 → 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 (38) 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/claude/mcp-server.mjs +679 -762
  9. package/plugins/immune-brain/dist/imm-loop.md +11 -4
  10. package/plugins/immune-brain/dist/imm-planner.md +37 -12
  11. package/plugins/immune-brain/runtime/assurance/coordinator.ts +55 -2
  12. package/plugins/immune-brain/runtime/assurance/review_evidence.ts +4 -0
  13. package/plugins/immune-brain/runtime/claude/interaction.ts +1 -2
  14. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +73 -27
  15. package/plugins/immune-brain/runtime/claude/mcp_server.ts +3 -4
  16. package/plugins/immune-brain/runtime/claude/review_host.ts +15 -0
  17. package/plugins/immune-brain/runtime/github_issue_tracker.ts +738 -677
  18. package/plugins/immune-brain/runtime/kernel/application.ts +2 -5
  19. package/plugins/immune-brain/runtime/kernel/authority_port.ts +5 -14
  20. package/plugins/immune-brain/runtime/kernel/batch_authority.ts +30 -49
  21. package/plugins/immune-brain/runtime/kernel/canary_application.ts +1 -2
  22. package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +0 -1
  23. package/plugins/immune-brain/runtime/kernel/capability_registry.ts +7 -9
  24. package/plugins/immune-brain/runtime/kernel/enrollment.ts +21 -12
  25. package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +5 -11
  26. package/plugins/immune-brain/runtime/kernel/types.ts +2 -1
  27. package/plugins/immune-brain/runtime/kernel/validation.ts +3 -1
  28. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  29. package/plugins/immune-brain/runtime/unattended/batch_git.ts +9 -1
  30. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +3 -9
  31. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +120 -31
  32. package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
  33. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +6 -7
  34. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +72 -119
  35. package/plugins/immune-brain/runtime/unattended/batch_state.ts +36 -14
  36. package/plugins/immune-brain/runtime/unattended/types.ts +0 -2
  37. package/plugins/immune-brain/runtime/workspace_scope.ts +44 -8
  38. 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.5.0";
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(() => {
@@ -2409,7 +2460,7 @@ function fileFingerprint(root, relativePath) {
2409
2460
  return `file:${stat.mode}:${createHash7("sha256").update(readFileSync3(absolutePath)).digest("hex")}`;
2410
2461
  }
2411
2462
  function dirtyPaths(root) {
2412
- const tracked = git(root, ["diff", "--name-only", "-z", "HEAD", "--"]);
2463
+ const tracked = git(root, ["diff", "--name-only", "--no-renames", "-z", "HEAD", "--"]);
2413
2464
  const untracked = git(root, [
2414
2465
  "ls-files",
2415
2466
  "--others",
@@ -2584,6 +2635,25 @@ function headEntry(root, head, path) {
2584
2635
  function taskPathMatchesScope(path, scope) {
2585
2636
  return scope.some((scopePath) => pathMatchesScope(path, scopePath));
2586
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
+ }
2587
2657
  function taskSnapshotOnce(root, scope, taskId) {
2588
2658
  const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
2589
2659
  const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
@@ -2606,9 +2676,8 @@ function taskSnapshotOnce(root, scope, taskId) {
2606
2676
  const uncommittedInScope = [...new Set([...unstagedPaths, ...untrackedPaths])].filter((path) => taskPathMatchesScope(path, scope)).sort(comparePaths);
2607
2677
  if (uncommittedInScope.length > 0)
2608
2678
  throw new Error(`task scope contains unstaged or untracked changes: ${uncommittedInScope.join(", ")}`);
2609
- const baseline = readEnrollmentBaseline(root);
2610
- const current = baseline ? captureGitWorkspaceSnapshot(root) : null;
2611
- 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);
2612
2681
  const stagedFiles = {};
2613
2682
  for (const path of taskPaths) {
2614
2683
  const current = indexEntry(root, path);
@@ -2686,9 +2755,8 @@ function taskRevisionSnapshotOnce(root, scope, baseHead, taskId) {
2686
2755
  const unstagedPaths = decodeNullPaths(gitBytes(root, ["diff", "--no-renames", "--name-only", "-z", "--"]), "unstaged task revision paths");
2687
2756
  const untrackedPaths = decodeNullPaths(gitBytes(root, ["ls-files", "--others", "--exclude-standard", "-z", "--"]), "untracked task revision paths");
2688
2757
  assertNoEnvelopeEscape(root, stagedPaths, scope, taskId);
2689
- const baseline = readEnrollmentBaseline(root);
2690
- const current = baseline ? captureGitWorkspaceSnapshot(root) : null;
2691
- 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);
2692
2760
  const scopedUnstagedPaths = unstagedPaths.filter((path) => taskPathMatchesScope(path, scope));
2693
2761
  const scopedUntrackedPaths = untrackedPaths.filter((path) => taskPathMatchesScope(path, scope));
2694
2762
  assertNoCaseFoldCollisions([...scopedStagedPaths, ...scopedUnstagedPaths, ...scopedUntrackedPaths], "Git task revision paths");
@@ -4196,7 +4264,7 @@ function parseHistoryV3(value, index, violations) {
4196
4264
  actor_id: stringAt(auth.actor_id, `${path}.authority.actor_id`, violations),
4197
4265
  confirmation_ref: stringAt(auth.confirmation_ref, `${path}.authority.confirmation_ref`, violations),
4198
4266
  issued_at: stringAt(auth.issued_at, `${path}.authority.issued_at`, violations),
4199
- 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) } : {}
4200
4268
  };
4201
4269
  }
4202
4270
  return {
@@ -6718,6 +6786,8 @@ function publishInput(root, input) {
6718
6786
  const recomputed = `sha256:${createHash13("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
6719
6787
  if (recomputed !== input.expectedDiffHash)
6720
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");
6721
6791
  return { snapshot, revision: publishReviewRevision(snapshot.repository_root, snapshot, recomputed, input.taskId) };
6722
6792
  }
6723
6793
  function captureReviewManifest(root, input) {
@@ -7007,7 +7077,7 @@ async function projectAssurance(root, taskId, diffProvider) {
7007
7077
 
7008
7078
  // plugins/immune-brain/runtime/kernel/application.ts
7009
7079
  function applyTaskAction(input) {
7010
- const { root, task_id, prior_intent_token, registry, capability, diffProvider, now } = input;
7080
+ const { root, task_id, prior_intent_token, registry, capability, diffProvider } = input;
7011
7081
  return withKernelStoreLock(root, () => {
7012
7082
  const current = readTaskRecordRaw(root, task_id);
7013
7083
  if (!current.record)
@@ -7058,7 +7128,7 @@ function applyTaskAction(input) {
7058
7128
  diff_hash: trustedDiff.diff_hash,
7059
7129
  ...action.type === "request_rework" ? { findings_digest: findingsDigestV2(action.findings) } : {}
7060
7130
  } : null;
7061
- const inspectedAudit = expectedAuthority ? registry.inspect(capability, expectedAuthority, now) : null;
7131
+ const inspectedAudit = expectedAuthority ? registry.inspect(capability, expectedAuthority) : null;
7062
7132
  const authorityRunId = inspectedAudit?.run_id;
7063
7133
  const mutation = reduceTask(current.record, action, inspectedAudit ? inspectedAudit.audit : null, trustedDiff.changed_paths);
7064
7134
  if (!isReducedMutation(mutation))
@@ -7084,7 +7154,7 @@ function applyTaskAction(input) {
7084
7154
  consumeIntentToken(prior_intent_token);
7085
7155
  consumeIntentToken(freshRead.token);
7086
7156
  if (expectedAuthority) {
7087
- registry.consume(capability, expectedAuthority, now);
7157
+ registry.consume(capability, expectedAuthority);
7088
7158
  }
7089
7159
  const nextWorking = mutation.next_workspace_working;
7090
7160
  const nextRecord = input.artifact_transition ? {
@@ -7410,7 +7480,6 @@ function createCanaryApplication(registry) {
7410
7480
  registry,
7411
7481
  capability,
7412
7482
  diffProvider: input.diffProvider,
7413
- now: Date.parse(now),
7414
7483
  ...operation.op === "complete" || operation.op === "stop" ? { terminal: { terminalized_at: now } } : {},
7415
7484
  ...artifactTransition ? { artifact_transition: artifactTransition } : {}
7416
7485
  });
@@ -7458,7 +7527,7 @@ function createCanaryApplication(registry) {
7458
7527
  intent_revision: current.record.intent_snapshot.revision,
7459
7528
  intent_content_hash: current.record.intent_ref.content_hash,
7460
7529
  diff_hash: "sha256:" + "0".repeat(64)
7461
- }, Date.parse(now));
7530
+ });
7462
7531
  const nextClaim = {
7463
7532
  ...claim,
7464
7533
  lifecycle_status: "draining",
@@ -7499,7 +7568,7 @@ function createCapabilityRegistry(capabilityBrand, hooks, domainLabel) {
7499
7568
  return {
7500
7569
  brand,
7501
7570
  issue(binding, issuedAt = new Date().toISOString()) {
7502
- hooks.validateBinding(binding, issuedAt);
7571
+ hooks.validateBinding(binding);
7503
7572
  const capability = Object.freeze(Object.defineProperties({}, {
7504
7573
  [capabilityBrand]: { value: true, enumerable: false, writable: false, configurable: false },
7505
7574
  [brand]: { value: true, enumerable: false, writable: false, configurable: false }
@@ -7507,16 +7576,16 @@ function createCapabilityRegistry(capabilityBrand, hooks, domainLabel) {
7507
7576
  states.set(capability, { binding: { ...binding }, issued_at: issuedAt, consumed: false });
7508
7577
  return capability;
7509
7578
  },
7510
- inspect(capability, expected, now = Date.now()) {
7579
+ inspect(capability, expected) {
7511
7580
  if (!isCapability(capability))
7512
7581
  throw new Error(`${domainLabel} capability is not recognized by this registry`);
7513
7582
  const state = stateOf(capability);
7514
7583
  if (state.consumed)
7515
7584
  throw new Error(`${domainLabel} capability already consumed`);
7516
- return hooks.validateAndProject({ ...state.binding, issued_at: state.issued_at }, expected, now);
7585
+ return hooks.validateAndProject({ ...state.binding, issued_at: state.issued_at }, expected);
7517
7586
  },
7518
- consume(capability, expected, now = Date.now()) {
7519
- const validated = this.inspect(capability, expected, now);
7587
+ consume(capability, expected) {
7588
+ const validated = this.inspect(capability, expected);
7520
7589
  stateOf(capability).consumed = true;
7521
7590
  return validated;
7522
7591
  },
@@ -7533,7 +7602,7 @@ function digestOfAction(action) {
7533
7602
  }
7534
7603
  function createMutationAuthorityRegistry() {
7535
7604
  const inner = createCapabilityRegistry(MUTATION_AUTHORITY_CAPABILITY_BRAND, {
7536
- validateBinding(binding, issuedAt) {
7605
+ validateBinding(binding) {
7537
7606
  const missing = [];
7538
7607
  if (binding.run_id !== undefined && binding.run_id.length === 0)
7539
7608
  throw new Error("authority capability run_id must not be empty");
@@ -7547,12 +7616,8 @@ function createMutationAuthorityRegistry() {
7547
7616
  throw new Error(`authority capability binding is incomplete: ${missing.join(", ")}`);
7548
7617
  if (binding.findings_digest !== null && !/^sha256:[a-f0-9]{64}$/.test(binding.findings_digest))
7549
7618
  throw new Error("authority capability findings_digest must be a canonical sha256 hash");
7550
- if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
7551
- throw new Error("authority capability must have a future expiry");
7552
7619
  },
7553
- validateAndProject(state, expected, now) {
7554
- if (Date.parse(state.expires_at) <= now)
7555
- throw new Error("authority capability has expired");
7620
+ validateAndProject(state, expected) {
7556
7621
  const actionDigest = digestOfAction(expected.action);
7557
7622
  if (state.action_digest !== actionDigest)
7558
7623
  throw new Error("authority capability action digest mismatch");
@@ -7579,8 +7644,7 @@ function createMutationAuthorityRegistry() {
7579
7644
  authority_kind: state.authority_kind,
7580
7645
  actor_id: canonicalActorId(state.actor_id),
7581
7646
  confirmation_ref: state.confirmation_ref,
7582
- issued_at: state.issued_at,
7583
- expires_at: state.expires_at
7647
+ issued_at: state.issued_at
7584
7648
  },
7585
7649
  action_digest: actionDigest,
7586
7650
  ...state.run_id !== undefined ? { run_id: state.run_id } : {},
@@ -7595,20 +7659,20 @@ function createMutationAuthorityRegistry() {
7595
7659
  return {
7596
7660
  brand: inner.brand,
7597
7661
  issue: inner.issue.bind(inner),
7598
- inspect(capability, expected, now = Date.now()) {
7662
+ inspect(capability, expected) {
7599
7663
  if (!capability)
7600
7664
  throw new Error("privileged action requires an opaque authority capability");
7601
7665
  try {
7602
- return inner.inspect(capability, expected, now);
7666
+ return inner.inspect(capability, expected);
7603
7667
  } catch (err) {
7604
7668
  if (err instanceof Error && err.message.includes("not recognized by this registry"))
7605
7669
  throw new Error("privileged action requires an opaque authority capability");
7606
7670
  throw err;
7607
7671
  }
7608
7672
  },
7609
- consume(capability, expected, now = Date.now()) {
7673
+ consume(capability, expected) {
7610
7674
  try {
7611
- return inner.consume(capability, expected, now);
7675
+ return inner.consume(capability, expected);
7612
7676
  } catch (err) {
7613
7677
  if (err instanceof Error && err.message.includes("not recognized by this registry"))
7614
7678
  throw new Error("privileged action requires an opaque authority capability");
@@ -7623,7 +7687,7 @@ function createMutationAuthorityRegistry() {
7623
7687
  var ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollment-capability-brand");
7624
7688
  function createEnrollmentAuthorityRegistry() {
7625
7689
  return createCapabilityRegistry(ENROLLMENT_CAPABILITY_BRAND, {
7626
- validateBinding(binding, issuedAt) {
7690
+ validateBinding(binding) {
7627
7691
  const missing = [];
7628
7692
  for (const [key, value] of Object.entries(binding)) {
7629
7693
  if (value === undefined || value === null || value === "")
@@ -7631,12 +7695,8 @@ function createEnrollmentAuthorityRegistry() {
7631
7695
  }
7632
7696
  if (missing.length > 0)
7633
7697
  throw new Error(`enrollment capability binding is incomplete: ${missing.join(", ")}`);
7634
- if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
7635
- throw new Error("enrollment capability must have a future expiry");
7636
7698
  },
7637
- validateAndProject(state, expected, now) {
7638
- if (Date.parse(state.expires_at) <= now)
7639
- throw new Error("enrollment capability has expired");
7699
+ validateAndProject(state, expected) {
7640
7700
  for (const key of Object.keys(expected)) {
7641
7701
  if (state[key] !== expected[key])
7642
7702
  throw new Error(`enrollment capability ${key} mismatch`);
@@ -7650,13 +7710,16 @@ function createEnrollmentAuthorityRegistry() {
7650
7710
  actor_id: state.actor_id,
7651
7711
  confirmation_ref: state.confirmation_ref,
7652
7712
  issued_at: state.issued_at,
7653
- expires_at: state.expires_at,
7654
7713
  nonce: state.nonce
7655
7714
  };
7656
7715
  }
7657
7716
  }, "enrollment");
7658
7717
  }
7659
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
+
7660
7723
  // plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts
7661
7724
  import { createHash as createHash15 } from "node:crypto";
7662
7725
  import { spawnSync as spawnSync3 } from "node:child_process";
@@ -7758,6 +7821,238 @@ function revalidatePiCanary(root, input, previous) {
7758
7821
  return { unchanged: current.digest === previous.digest, current };
7759
7822
  }
7760
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
+
7761
8056
  // plugins/immune-brain/runtime/kernel/enrollment.ts
7762
8057
  function runEnrollmentPreconditionChecks(root, input, capability, registry, mode, beforeLock, onReady) {
7763
8058
  const blockers = [];
@@ -7809,6 +8104,17 @@ function runEnrollmentPreconditionChecks(root, input, capability, registry, mode
7809
8104
  } catch (error) {
7810
8105
  fail(`git base: ${error instanceof Error ? error.message : String(error)}`, error);
7811
8106
  }
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
+ }
7812
8118
  const state = {
7813
8119
  validated,
7814
8120
  current,
@@ -7905,16 +8211,16 @@ function enrollCanaryTask(root, input, registry) {
7905
8211
  if (checks.gitBaseHead !== gitBaseHead)
7906
8212
  throw new Error("Git HEAD moved after the enrollment confirmation");
7907
8213
  if (input.batch) {
7908
- const batch = input.batch.registry.inspect(input.batch.capability, input.batch.binding, Date.parse(input.now));
7909
- if (input.batch.registry.consumedChildren(input.batch.capability).length === 0 && input.batch.expected_head !== batch.base_head)
7910
- 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);
7911
8217
  if (checks.gitBaseHead !== input.batch.expected_head)
7912
8218
  throw new Error(`batch_head_lineage_broken: expected ${input.batch.expected_head}, found ${checks.gitBaseHead}`);
7913
8219
  }
7914
8220
  registry.consume(input.capability, input.capability_binding);
7915
8221
  consumed = true;
7916
8222
  if (input.batch)
7917
- 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);
7918
8224
  if (!gitBaseHead)
7919
8225
  throw new Error("enrollment requires a committed Git HEAD");
7920
8226
  const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
@@ -7968,7 +8274,7 @@ function enrollCanaryTask(root, input, registry) {
7968
8274
  }
7969
8275
 
7970
8276
  // plugins/immune-brain/runtime/assurance/enrollment_git_base.ts
7971
- import { execFileSync as execFileSync4, spawnSync as spawnSync4 } from "node:child_process";
8277
+ import { execFileSync as execFileSync4, spawnSync as spawnSync5 } from "node:child_process";
7972
8278
  function git2(root, args, input, env = process.env) {
7973
8279
  return execFileSync4("git", args, { cwd: root, encoding: "utf8", input, env, stdio: ["pipe", "pipe", "pipe"] }).trim();
7974
8280
  }
@@ -7981,8 +8287,8 @@ function inspectEnrollmentGitBase(root) {
7981
8287
  const branch = git2(root, ["symbolic-ref", "--no-recurse", "HEAD"]);
7982
8288
  if (!branch.startsWith("refs/heads/"))
7983
8289
  throw new Error("Enrollment HEAD must name a local branch");
7984
- const ref = spawnSync4("git", ["show-ref", "--verify", "--quiet", branch], { cwd: root });
7985
- 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 });
7986
8292
  if (ref.status !== 1 || symbolic.status !== 1)
7987
8293
  throw new Error("Enrollment Git HEAD is invalid; only an absent branch can be initialized");
7988
8294
  return { state: "unborn", branch };
@@ -7992,7 +8298,7 @@ function enrollmentGitBaseNotice(base) {
7992
8298
  }
7993
8299
  function identityEnv(root) {
7994
8300
  const configured = (key) => {
7995
- const result = spawnSync4("git", ["config", "--get", key], { cwd: root, encoding: "utf8" });
8301
+ const result = spawnSync5("git", ["config", "--get", key], { cwd: root, encoding: "utf8" });
7996
8302
  if (result.status !== 0 && result.status !== 1)
7997
8303
  throw new Error(`Cannot read Git identity: ${key}`);
7998
8304
  return result.stdout?.trim() ?? "";
@@ -8041,10 +8347,10 @@ function initializeEnrollmentGitBase(root, input, previous, base, signal) {
8041
8347
  }
8042
8348
 
8043
8349
  // plugins/immune-brain/runtime/assurance/qa.ts
8044
- import { createHash as createHash17 } from "node:crypto";
8350
+ import { createHash as createHash18 } from "node:crypto";
8045
8351
  // plugins/immune-brain/runtime/assurance/delivery_workspace.ts
8046
8352
  import { execFileSync as execFileSync5 } from "node:child_process";
8047
- import { createHash as createHash16 } from "node:crypto";
8353
+ import { createHash as createHash17 } from "node:crypto";
8048
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";
8049
8355
  import { tmpdir as tmpdir3 } from "node:os";
8050
8356
  import { dirname as dirname6, isAbsolute as isAbsolute5, join as join8, parse, relative as relative4, resolve as resolve9, sep as sep5 } from "node:path";
@@ -8194,7 +8500,7 @@ function workspaceFiles(root, dir = root, result = Object.create(null)) {
8194
8500
  result[rel] = `${stat.mode & 4095}:directory`;
8195
8501
  workspaceFiles(root, path, result);
8196
8502
  } else if (stat.isFile())
8197
- 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")}`;
8198
8504
  else
8199
8505
  throw new DeliveryWorkspaceError("delivery contains an unsupported filesystem entry");
8200
8506
  }
@@ -8296,7 +8602,7 @@ function deliveryTreeForSnapshot(snapshot, writeTree) {
8296
8602
  if (!record || record.contract !== "assurance_kernel/task_record/v4" || !record.git_base_head)
8297
8603
  throw new Error("QA delivery requires a TaskRecord v4 git_base_head");
8298
8604
  const captured = captureGitTaskRevisionSnapshot(snapshot.root, record.intent_snapshot.scope_hint, record.git_base_head, snapshot.task_id);
8299
- const digest = `sha256:${createHash17("sha256").update(JSON.stringify(captured)).digest("hex")}`;
8605
+ const digest = `sha256:${createHash18("sha256").update(JSON.stringify(captured)).digest("hex")}`;
8300
8606
  if (digest !== snapshot.diff_hash)
8301
8607
  throw new Error("QA delivery identity does not match the frozen snapshot");
8302
8608
  return writeDeliveryTree(snapshot.root, captured);
@@ -8331,7 +8637,7 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
8331
8637
  const diagnostics = (stage, outcome, affected = group, commandStage = "check", elapsed = 0, result) => affected.map((item) => ({
8332
8638
  acceptance_id: item.id,
8333
8639
  descriptor_ref: `acceptance/${item.index}/verification/${commandStage === "prepare" ? "environment/prepare" : "command"}`,
8334
- 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")}`,
8335
8641
  stage,
8336
8642
  outcome,
8337
8643
  elapsed_ms: elapsed,
@@ -8405,8 +8711,8 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
8405
8711
  }
8406
8712
  evidence.push({
8407
8713
  stage,
8408
- environment: createHash17("sha256").update(environmentKey).digest("hex"),
8409
- 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"),
8410
8716
  entry: frozen?.entry?.content_hash ?? null,
8411
8717
  interpreter: frozen?.interpreter?.content_hash ?? null,
8412
8718
  exit_code: result.exit_code,
@@ -8484,7 +8790,7 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
8484
8790
  };
8485
8791
  if (findings.length)
8486
8792
  return { ...base, decision: "rework", findings };
8487
- const executionDigest = createHash17("sha256").update(JSON.stringify({
8793
+ const executionDigest = createHash18("sha256").update(JSON.stringify({
8488
8794
  tree,
8489
8795
  platform: process.platform,
8490
8796
  arch: process.arch,
@@ -8569,11 +8875,6 @@ var BATCH_REASONS = Object.freeze({
8569
8875
  reason: "native confirmation port is unavailable",
8570
8876
  recovery_action: "retry through a fresh native gate in the current Host"
8571
8877
  },
8572
- confirmation_timed_out: {
8573
- state: "rejected",
8574
- reason: "native confirmation timed out waiting for user interaction",
8575
- recovery_action: "retry through a fresh native gate in the current Host"
8576
- },
8577
8878
  confirmation_cancelled: {
8578
8879
  state: "cancelled",
8579
8880
  reason: "native interaction cancelled",
@@ -8639,23 +8940,6 @@ function batchReason(key, detail = "") {
8639
8940
  };
8640
8941
  }
8641
8942
 
8642
- // plugins/immune-brain/runtime/unattended/confirmation_deadline.ts
8643
- var CONFIRMATION_TIMEOUT_DEFAULT_MS = 60000;
8644
- var CONFIRMATION_TIMEOUT_ENV = "IMMUNE_BRAIN_BATCH_TIMEOUT_MS";
8645
- function startConfirmationDeadline(input) {
8646
- const configured = Number(input.env?.[CONFIRMATION_TIMEOUT_ENV]);
8647
- const timeoutMs = Number.isFinite(configured) && configured > 0 ? configured : CONFIRMATION_TIMEOUT_DEFAULT_MS;
8648
- const controller = new AbortController;
8649
- const timer = setTimeout(() => {
8650
- controller.abort(new Error("native confirmation timed out waiting for user interaction"));
8651
- }, timeoutMs);
8652
- return {
8653
- signal: input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal,
8654
- timedOut: () => controller.signal.aborted && !input.signal?.aborted,
8655
- clear: () => clearTimeout(timer)
8656
- };
8657
- }
8658
-
8659
8943
  // plugins/immune-brain/runtime/authorization_operation.ts
8660
8944
  function deriveAuthorizationOperation(input) {
8661
8945
  if (input.readiness.state === "resolve_user_decision")
@@ -8885,17 +9169,17 @@ function createGhTransport(binary = "gh") {
8885
9169
  return new Promise((complete) => {
8886
9170
  let stdout = Buffer.alloc(0);
8887
9171
  let stderr = Buffer.alloc(0);
8888
- const remaining = options.budget === undefined ? GH_TIMEOUT_MS : Math.min(GH_TIMEOUT_MS, options.budget.deadline_ms - Date.now());
8889
- if (options.budget && (options.budget.signal?.aborted || remaining <= 0)) {
9172
+ if (options.signal?.aborted) {
8890
9173
  complete({
8891
9174
  exit_code: 1,
8892
9175
  stdout: "",
8893
- stderr: options.budget.signal?.aborted ? "operation cancelled by the caller" : "publication deadline exceeded before the next remote call",
9176
+ stderr: "operation cancelled by the caller",
8894
9177
  timed_out: true,
8895
9178
  output_exceeded: false
8896
9179
  });
8897
9180
  return;
8898
9181
  }
9182
+ const remaining = GH_TIMEOUT_MS;
8899
9183
  let timedOut = false;
8900
9184
  let outputExceeded = false;
8901
9185
  let timer;
@@ -9283,7 +9567,7 @@ ${provenance}## How to use this Issue
9283
9567
 
9284
9568
  - Edit planning prose and Slice ordering directly after creation.
9285
9569
  - Keep each Slice marker attached to exactly one stable Slice entry.
9286
- - 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.
9287
9571
 
9288
9572
  ## Problem
9289
9573
 
@@ -9432,7 +9716,7 @@ ${publicText(projection.agent_handoff ?? "Implement only the bounded TaskIntent
9432
9716
  ${ISSUE_FOOTER}
9433
9717
  `;
9434
9718
  }
9435
- async function upsertTask(root, gh, operation, source, pendingBinding = null, amendmentContext = undefined) {
9719
+ async function upsertTask(root, gh, operation, source) {
9436
9720
  const labelFailure = await labelAvailabilityFailure(root, gh, source.repository, desiredTaskLabels(operation));
9437
9721
  if (labelFailure)
9438
9722
  return labelFailure;
@@ -9447,8 +9731,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
9447
9731
  const found = lookup(source.issues);
9448
9732
  if (found.kind === "ambiguous")
9449
9733
  return result(operation.op, "ambiguous_remote_state", found.message);
9450
- if (found.kind === "missing" && pendingBinding !== null && pendingBinding !== undefined)
9451
- 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);
9452
9734
  const blockerIds = operation.projection?.blocked_by ?? [];
9453
9735
  const blockers = [];
9454
9736
  for (const blockerId of blockerIds) {
@@ -9473,32 +9755,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
9473
9755
  let createdChild = false;
9474
9756
  let labelsConverged = false;
9475
9757
  if (found.kind === "missing") {
9476
- if (amendmentContext !== undefined) {
9477
- const reRead = await snapshot(root, gh, operation.op);
9478
- if ("contract" in reRead)
9479
- return reRead;
9480
- const reReadParent = initiativeLookup(reRead.issues, reRead.repository.id, operation.initiative_id);
9481
- if (reReadParent.kind !== "found")
9482
- return result(operation.op, "ambiguous_remote_state", reReadParent.kind === "ambiguous" ? reReadParent.message : "amendment Parent is not observable before creating a new Child", parent.issue);
9483
- if (reReadParent.issue.number !== amendmentContext.parentIssueNumber)
9484
- 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);
9485
- if (reReadParent.issue.state !== "open")
9486
- return result(operation.op, "ambiguous_remote_state", "amendment Parent is no longer open before creating a new Child", reReadParent.issue);
9487
- if (amendmentContext.parent.title !== reReadParent.issue.title || amendmentContext.parent.body !== reReadParent.issue.body)
9488
- return result(operation.op, "ambiguous_remote_state", "amendment Parent content changed before creating a new Child", reReadParent.issue);
9489
- if (sliceCount(reReadParent.issue.body, operation.slice_id) !== 1)
9490
- 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);
9491
- const raced = lookup(reRead.issues);
9492
- if (raced.kind === "ambiguous")
9493
- return result(operation.op, "ambiguous_remote_state", raced.message);
9494
- if (raced.kind === "found") {
9495
- const approved = amendmentContext.pendingContent.get(operation.task_id);
9496
- const resumable = approved !== undefined && raced.issue.state === "open" && carriesApprovedContent(raced.issue.title, raced.issue.body, approved);
9497
- if (!resumable)
9498
- 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);
9499
- return updatePendingChild(root, gh, reRead, operation, raced.issue, undefined, blockers, approved, amendmentContext);
9500
- }
9501
- }
9502
9758
  const mutation = await gh.run([
9503
9759
  "issue",
9504
9760
  "create",
@@ -9521,11 +9777,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
9521
9777
  }
9522
9778
  if (created.issue.body !== body || created.issue.title !== title)
9523
9779
  return result(operation.op, "retryable_failure", "Task Issue did not converge to the requested title and body", created.issue);
9524
- if (amendmentContext !== undefined) {
9525
- if (created.issue.state !== "open")
9526
- return result(operation.op, "ambiguous_remote_state", `new pending Task ${operation.task_id} (Issue #${created.issue.number}) is not open after creation`, created.issue);
9527
- return updatePendingChild(root, gh, source, operation, created.issue, undefined, blockers, amendmentContext.pendingContent.get(operation.task_id), amendmentContext);
9528
- }
9529
9780
  child = created.issue;
9530
9781
  createdChild = true;
9531
9782
  } else {
@@ -9533,27 +9784,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
9533
9784
  if (owned.kind !== "found")
9534
9785
  return result(operation.op, "ambiguous_remote_state", owned.kind === "ambiguous" ? owned.message : "Task Issue ownership changed during publication", found.issue);
9535
9786
  child = owned.issue;
9536
- if (pendingBinding !== null) {
9537
- if (child.state !== "open")
9538
- return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} is closed and cannot be amended as pending work`, found.issue);
9539
- if (pendingBinding !== undefined && pendingBinding.issue_number !== child.number)
9540
- 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);
9541
- const approvedFinal = amendmentContext?.pendingContent.get(operation.task_id);
9542
- const isFinal = approvedFinal !== undefined && carriesApprovedContent(found.issue.title, found.issue.body, approvedFinal);
9543
- if (!isFinal) {
9544
- const baselineMatches = pendingBinding !== undefined && pendingBinding.title === found.issue.title && carriesApprovedContent(found.issue.title, found.issue.body, pendingBinding);
9545
- if (!baselineMatches)
9546
- return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} changed since the approved amendment baseline`, found.issue);
9547
- }
9548
- return updatePendingChild(root, gh, source, operation, child, pendingBinding?.issue_number, blockers, approvedFinal, amendmentContext);
9549
- }
9550
- if (amendmentContext !== undefined) {
9551
- const approvedFinal = amendmentContext.pendingContent.get(operation.task_id);
9552
- const resumable = approvedFinal !== undefined && child.state === "open" && carriesApprovedContent(found.issue.title, found.issue.body, approvedFinal);
9553
- if (!resumable)
9554
- 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);
9555
- return updatePendingChild(root, gh, source, operation, child, undefined, blockers, approvedFinal, amendmentContext);
9556
- }
9557
9787
  if (!sameTrackedBody(found.issue.body, body) || found.issue.title !== title)
9558
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);
9559
9789
  const intentMismatch = publishedIntentMismatch(found.issue.body, operation.task_id, operation.intent_hash);
@@ -9660,7 +9890,7 @@ async function closeTerminalIssue(root, gh, operation, source, issue, lookup) {
9660
9890
  return result(operation.op, "updated", "terminal Task Issue closure confirmed", found.issue);
9661
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);
9662
9892
  }
9663
- async function markTerminal(root, gh, operation, source) {
9893
+ async function markChildTerminal(root, gh, operation, source) {
9664
9894
  const lookup = (issues) => taskLookup(issues, source.repository.id, operation.task_id);
9665
9895
  const found = lookup(source.issues);
9666
9896
  if (found.kind === "missing")
@@ -9710,6 +9940,69 @@ async function markTerminal(root, gh, operation, source) {
9710
9940
  return refreshedOwnership;
9711
9941
  return closeTerminalIssue(root, gh, operation, refreshed, reread.issue, lookup);
9712
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
+ }
9713
10006
  function normalizedList(value, name, max = 2000) {
9714
10007
  if (value === undefined)
9715
10008
  return;
@@ -9850,457 +10143,15 @@ async function runGithubTrackerOperation(root, input, gh = createGhTransport())
9850
10143
  return markTerminal(absoluteRoot, gh, operation, source);
9851
10144
  }
9852
10145
  }
9853
- function issueTerminalEventId(body) {
9854
- const suffixMatch = [...body.matchAll(/<!-- immune-brain:terminal-event=([A-Za-z0-9._:-]+) -->/g)];
9855
- if (suffixMatch.length === 0)
9856
- return null;
9857
- if (suffixMatch.length > 1)
9858
- return "multiple";
9859
- const eventId = suffixMatch[0][1];
9860
- if (eventId.length > MAX_TERMINAL_EVENT_ID || !body.endsWith(terminalSuffix(eventId)))
9861
- return "malformed";
9862
- return eventId;
9863
- }
9864
- function stripTerminalSuffixFromBytes(body) {
9865
- if (body === undefined)
9866
- return null;
9867
- const eventId = issueTerminalEventId(body);
9868
- if (eventId === null || eventId === "multiple" || eventId === "malformed")
9869
- return eventId;
9870
- const suffix = terminalSuffix(eventId);
9871
- if (!body.endsWith(suffix))
9872
- return null;
9873
- return body.slice(0, body.length - suffix.length);
9874
- }
9875
- function carriesApprovedContent(title, body, approved) {
9876
- if (title !== approved.title)
9877
- return false;
9878
- if (body === approved.body)
9879
- return true;
9880
- const stripped = stripTerminalSuffixFromBytes(body);
9881
- return typeof stripped === "string" && stripped.trimEnd() === approved.body.trimEnd();
9882
- }
9883
- async function updatePendingChild(root, gh, source, op, child, boundNumber, desiredBlockers, approvedFinal, amendmentContext = undefined) {
9884
- const parent = initiativeLookup(source.issues, source.repository.id, op.initiative_id);
9885
- const body = childBody(source.repository, op, parent.kind === "found" ? parent.issue : child);
9886
- const oversized = bodyLimitFailure(op.op, body, MAX_TERMINAL_SUFFIX_BYTES);
9887
- if (oversized)
9888
- return oversized;
9889
- const title = taskIssueTitle(op, sliceOrdinalFromChecklist(parent.kind === "found" ? parent.issue.body : child.body, op.slice_id, op.projection?.slice_ordinal ?? 1));
9890
- const parentApprovedContent = amendmentContext?.parent;
9891
- const parentBoundNumber = amendmentContext?.parentIssueNumber;
9892
- const baseBody = child.body;
9893
- const suffixEvent = issueTerminalEventId(baseBody);
9894
- if (suffixEvent === "multiple")
9895
- return result(op.op, "ambiguous_remote_state", "pending Task has multiple terminal markers", child);
9896
- if (suffixEvent === "malformed")
9897
- return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a malformed terminal marker (marker without its exact canonical suffix)`, child);
9898
- let finalBody = suffixEvent !== null ? `${body.trimEnd()}${terminalSuffix(suffixEvent)}` : body;
9899
- const approvedMatches = approvedFinal !== undefined && carriesApprovedContent(title, body, approvedFinal);
9900
- if (!approvedMatches)
9901
- return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a terminal suffix that diverges from the approved amendment content`, child);
9902
- const approvedNow = approvedFinal !== undefined && carriesApprovedContent(child.title, child.body, approvedFinal);
9903
- if (approvedNow) {
9904
- const currentParent = initiativeLookup(source.issues, source.repository.id, op.initiative_id);
9905
- if (currentParent.kind !== "found")
9906
- return result(op.op, "ambiguous_remote_state", "pending Task Parent is not observable before attachment convergence", child);
9907
- const revalidated = await revalidatePendingChildBeforeWrite(root, gh, child.number, op.task_id, approvedFinal, undefined, true, parentApprovedContent, parentBoundNumber);
9908
- if ("contract" in revalidated)
9909
- return revalidated;
9910
- const targetParentNumber = parentBoundNumber ?? currentParent.issue.number;
9911
- const attachment = await confirmAttachment(root, gh, op.op, source.repository, targetParentNumber, child.number);
9912
- if (!("attached" in attachment))
9913
- return attachment;
9914
- if (!attachment.attached) {
9915
- const attached = await attachSubIssue(root, gh, op.op, source.repository, targetParentNumber, child);
9916
- if (!("attached" in attached))
9917
- return attached;
9918
- }
9919
- }
9920
- let observed = child;
9921
- const desiredLabels = desiredTaskLabels(op);
9922
- if (child.title !== title || child.body !== finalBody || labelMutationArgs(child.labels, desiredLabels).length) {
9923
- const revalidated = await revalidatePendingChildBeforeWrite(root, gh, child.number, op.task_id, approvedFinal, { title: child.title, body: child.body }, false, parentApprovedContent, parentBoundNumber);
9924
- if ("contract" in revalidated)
9925
- return revalidated;
9926
- observed = revalidated;
9927
- const observedEvent = issueTerminalEventId(observed.body);
9928
- if (observedEvent === "malformed")
9929
- return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a malformed terminal marker (marker without its exact canonical suffix)`, child);
9930
- const writeBody = typeof observedEvent === "string" ? `${body.trimEnd()}${terminalSuffix(observedEvent)}` : finalBody;
9931
- const labelArgs = labelMutationArgs(observed.labels, desiredLabels);
9932
- if (observed.title !== title || observed.body !== writeBody || labelArgs.length) {
9933
- const edited = await gh.run([
9934
- "issue",
9935
- "edit",
9936
- String(child.number),
9937
- "--repo",
9938
- source.repository.name_with_owner,
9939
- "--title",
9940
- title,
9941
- "--body-file",
9942
- "-",
9943
- ...labelArgs
9944
- ], { cwd: root, stdin: writeBody });
9945
- if (edited.exit_code !== 0 || edited.output_exceeded)
9946
- return ghFailure(op.op, edited, `pending Task Issue #${child.number} update failed`);
9947
- finalBody = writeBody;
9948
- }
9949
- }
9950
- const dependencies = await convergePendingDependencies(root, gh, source, child.number, op.task_id, desiredBlockers, approvedFinal, parentApprovedContent, parentBoundNumber);
9951
- if (!("complete" in dependencies))
9952
- return dependencies;
9953
- const refreshed = await snapshot(root, gh, op.op);
9954
- if ("contract" in refreshed)
9955
- return refreshed;
9956
- const reread = ownedTaskLookup(refreshed.issues, refreshed.repository.id, op.task_id, op.initiative_id, op.slice_id);
9957
- if (reread.kind !== "found" || reread.issue.number !== child.number)
9958
- return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} changed identity during amendment`, child);
9959
- if (reread.issue.title !== title || reread.issue.body !== finalBody)
9960
- return result(op.op, "retryable_failure", `pending Task ${op.task_id} update did not converge`, reread.issue);
9961
- const labelsCurrent = !desiredLabels.some((label) => !reread.issue.labels.includes(label));
9962
- if (!labelsCurrent)
9963
- return result(op.op, "retryable_failure", `pending Task ${op.task_id} labels did not converge`, reread.issue);
9964
- const currentDependencies = await confirmBlockedBy(root, gh, op.op, refreshed.repository, reread.issue.number, desiredBlockers);
9965
- if (!("complete" in currentDependencies))
9966
- return currentDependencies;
9967
- if (!currentDependencies.complete)
9968
- return result(op.op, "retryable_failure", `pending Task ${op.task_id} dependencies did not converge`, reread.issue);
9969
- const contentCurrent = child.title === title && child.body === finalBody && labelMutationArgs(child.labels, desiredLabels).length === 0;
9970
- 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);
9971
- }
9972
- async function convergePendingDependencies(root, gh, source, childNumber, childTaskId, requestedBlockers, approvedFinal, parentApprovedContent = undefined, parentBoundNumber = undefined) {
9973
- const expected = requestedBlockers.map((blocker) => blocker.id);
9974
- const existing = await readBlockedByIds(root, gh, "upsert-task", source.repository, childNumber);
9975
- if (!Array.isArray(existing))
9976
- return existing;
9977
- const removed = existing.filter((id) => !expected.includes(id));
9978
- for (const id of removed) {
9979
- const revalidated = await revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, undefined, false, parentApprovedContent, parentBoundNumber);
9980
- if ("contract" in revalidated)
9981
- return revalidated;
9982
- const mutation = await gh.run([
9983
- "api",
9984
- "--method",
9985
- "DELETE",
9986
- `repos/${source.repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by/${id}`
9987
- ], { cwd: root });
9988
- if (mutation.exit_code !== 0 || mutation.output_exceeded)
9989
- return ghFailure("upsert-task", mutation, `native blocked_by removal failed for Issue #${childNumber}`);
9990
- }
9991
- const additions = requestedBlockers.filter((blocker) => !existing.includes(blocker.id));
9992
- for (const blocker of additions) {
9993
- const revalidated = await revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, undefined, false, parentApprovedContent, parentBoundNumber);
9994
- if ("contract" in revalidated)
9995
- return revalidated;
9996
- const mutation = await gh.run([
9997
- "api",
9998
- "-F",
9999
- `issue_id=${blocker.id}`,
10000
- `repos/${source.repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by`
10001
- ], { cwd: root });
10002
- if (mutation.exit_code !== 0 || mutation.output_exceeded)
10003
- return ghFailure("upsert-task", mutation, `native blocked_by attachment failed for Issue #${blocker.number}`);
10004
- }
10005
- const confirm = await confirmBlockedBy(root, gh, "upsert-task", source.repository, childNumber, requestedBlockers);
10006
- if (!("complete" in confirm))
10007
- return confirm;
10008
- return confirm.complete ? { complete: true } : { complete: true };
10009
- }
10010
- async function revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, allowedBaseline = undefined, skipAttachmentCheck = false, parentApproved = undefined, parentExpectedNumber = undefined) {
10011
- const refreshed = await snapshot(root, gh, "upsert-task");
10012
- if ("contract" in refreshed)
10013
- return refreshed;
10014
- const resolved = taskLookup(refreshed.issues, refreshed.repository.id, childTaskId);
10015
- if (resolved.kind !== "found")
10016
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} is not uniquely owned before a write: ${resolved.kind}`);
10017
- const child = resolved.issue;
10018
- if (child.number !== childNumber)
10019
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} resolves to Issue #${child.number}, not the bound Issue #${childNumber}`, child);
10020
- if (child.state !== "open")
10021
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) is no longer open before a dependency write`, child);
10022
- const contentFinal = approvedFinal !== undefined && carriesApprovedContent(child.title, child.body, approvedFinal);
10023
- const contentBaseline = allowedBaseline !== undefined && child.title === allowedBaseline.title && child.body === allowedBaseline.body;
10024
- if (!contentFinal && !contentBaseline)
10025
- 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);
10026
- const initiativeId = [...child.body.matchAll(/<!-- immune-brain:initiative-id=([A-Za-z0-9._:-]+) -->/g)].map((match) => match[1])[0];
10027
- if (!initiativeId)
10028
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its Initiative marker before a dependency write`, child);
10029
- const parent = initiativeLookup(refreshed.issues, refreshed.repository.id, initiativeId);
10030
- if (parent.kind !== "found" || parent.issue.state !== "open")
10031
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its open Parent before a dependency write`, child);
10032
- if (parentExpectedNumber !== undefined && parent.issue.number !== parentExpectedNumber)
10033
- 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);
10034
- if (parentApproved !== undefined && (parent.issue.title !== parentApproved.title || parent.issue.body !== parentApproved.body))
10035
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) Parent changed since the approved amendment content before a dependency write`, child);
10036
- if (!skipAttachmentCheck) {
10037
- const attachedNow = await readSubIssueNumbers(root, gh, "upsert-task", refreshed.repository, parent.issue.number);
10038
- if (!Array.isArray(attachedNow))
10039
- return attachedNow;
10040
- if (!attachedNow.includes(childNumber))
10041
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) is no longer attached to the Parent before a dependency write`, child);
10042
- }
10043
- const sliceId = ownershipMarkerValue(child.body, "slice-id");
10044
- if (sliceId && sliceCount(parent.issue.body, sliceId) !== 1)
10045
- return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its exact Slice in the Parent before a dependency write`, child);
10046
- return child;
10047
- }
10048
10146
 
10049
10147
  // plugins/immune-brain/runtime/unattended/batch_preflight.ts
10050
10148
  import { existsSync as existsSync7, readdirSync as readdirSync6, readFileSync as readFileSync13 } from "node:fs";
10051
10149
  import { randomUUID as randomUUID7 } from "node:crypto";
10052
10150
  import { join as join12 } from "node:path";
10053
- import { spawnSync as spawnSync6 } from "node:child_process";
10054
-
10055
- // plugins/immune-brain/runtime/kernel/batch_authority.ts
10056
- import { createHash as createHash18 } from "node:crypto";
10057
- var BATCH_AUTHORITY_CAPABILITY_BRAND = Symbol.for("assurance-kernel.batch-authority-capability-brand");
10058
- var GIT_COMMIT_ID2 = /^[a-f0-9]{40}$/;
10059
-
10060
- class BatchAuthorizationExpiryError extends Error {
10061
- constructor(message) {
10062
- super(message);
10063
- this.name = "BatchAuthorizationExpiryError";
10064
- }
10065
- }
10066
- function sha256Hex3(bytes) {
10067
- return createHash18("sha256").update(bytes).digest("hex");
10068
- }
10069
- function stableStringify3(value) {
10070
- if (value === null || typeof value !== "object")
10071
- return JSON.stringify(value);
10072
- if (Array.isArray(value))
10073
- return `[${value.map((entry) => stableStringify3(entry)).join(",")}]`;
10074
- const record = value;
10075
- return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify3(record[key])}`).join(",")}}`;
10076
- }
10077
- function computeBatchPlanDigest(children) {
10078
- const canonical = children.map((child) => ({
10079
- blocked_by: [...child.blocked_by],
10080
- intent_content_hash: child.intent_content_hash,
10081
- intent_path: child.intent_path,
10082
- intent_revision: child.intent_revision,
10083
- task_id: child.task_id
10084
- }));
10085
- return `sha256:${sha256Hex3(stableStringify3(canonical))}`;
10086
- }
10087
- function requireNonEmpty(binding) {
10088
- const missing = [];
10089
- for (const key of [
10090
- "batch_id",
10091
- "initiative_slug",
10092
- "plan_digest",
10093
- "branch",
10094
- "base_head",
10095
- "actor_id",
10096
- "confirmation_ref",
10097
- "expires_at",
10098
- "nonce"
10099
- ]) {
10100
- const value = binding[key];
10101
- if (value === undefined || value === null || value === "")
10102
- missing.push(key);
10103
- }
10104
- if (!binding.budget || typeof binding.budget !== "object")
10105
- missing.push("budget");
10106
- if (missing.length > 0)
10107
- throw new Error(`batch authorization binding is incomplete: ${missing.join(", ")}`);
10108
- }
10109
- function validateBudget(budget, issuedAt) {
10110
- if (!Number.isInteger(budget.max_children) || budget.max_children <= 0)
10111
- throw new Error("batch budget max_children must be a positive integer");
10112
- if (!Number.isInteger(budget.qa_failure_limit) || budget.qa_failure_limit <= 0)
10113
- throw new Error("batch budget qa_failure_limit must be a positive integer");
10114
- const deadline = Date.parse(budget.deadline_at);
10115
- if (Number.isNaN(deadline) || deadline <= Date.parse(issuedAt))
10116
- throw new Error("batch budget must have a future deadline_at");
10117
- }
10118
- function validateChildren(children, planDigest) {
10119
- if (!Array.isArray(children) || children.length === 0)
10120
- throw new Error("batch authorization requires a non-empty child plan");
10121
- const seen = new Set;
10122
- for (const child of children) {
10123
- if (!child || typeof child !== "object")
10124
- throw new Error("batch plan child must be an object");
10125
- for (const key of ["task_id", "intent_path", "intent_content_hash"]) {
10126
- if (typeof child[key] !== "string" || child[key] === "")
10127
- throw new Error(`batch plan child ${key} must be a non-empty string`);
10128
- }
10129
- if (!Number.isInteger(child.intent_revision) || child.intent_revision <= 0)
10130
- throw new Error("batch plan child intent_revision must be a positive integer");
10131
- if (!Array.isArray(child.blocked_by) || child.blocked_by.some((id) => typeof id !== "string" || id === ""))
10132
- throw new Error("batch plan child blocked_by must be an array of task ids");
10133
- if (seen.has(child.task_id))
10134
- throw new Error(`batch plan child ${child.task_id} appears more than once`);
10135
- seen.add(child.task_id);
10136
- }
10137
- for (const child of children) {
10138
- for (const blocker of child.blocked_by) {
10139
- if (!seen.has(blocker))
10140
- throw new Error(`batch plan child ${child.task_id} is blocked by ${blocker}, which is not in the confirmed plan`);
10141
- }
10142
- }
10143
- const dependencies = new Map(children.map((child) => [child.task_id, child.blocked_by]));
10144
- const visiting = new Set;
10145
- const visited = new Set;
10146
- function visit(taskId) {
10147
- if (visiting.has(taskId))
10148
- throw new Error(`batch plan dependency cycle at ${taskId}`);
10149
- if (visited.has(taskId))
10150
- return;
10151
- visiting.add(taskId);
10152
- for (const blocker of dependencies.get(taskId))
10153
- visit(blocker);
10154
- visiting.delete(taskId);
10155
- visited.add(taskId);
10156
- }
10157
- for (const child of children)
10158
- visit(child.task_id);
10159
- if (computeBatchPlanDigest(children) !== planDigest)
10160
- throw new Error("batch plan digest does not match the confirmed child plan");
10161
- }
10162
- function createBatchAuthorityRegistry() {
10163
- const inner = createCapabilityRegistry(BATCH_AUTHORITY_CAPABILITY_BRAND, {
10164
- validateBinding(binding, issuedAt) {
10165
- requireNonEmpty(binding);
10166
- if (!isLiteralUserActor(binding.actor_id))
10167
- throw new Error("batch authorization requires a literal-user actor_id");
10168
- if (!GIT_COMMIT_ID2.test(binding.base_head))
10169
- throw new Error("batch authorization base_head must be a committed 40-hex commit id");
10170
- const expires = Date.parse(binding.expires_at);
10171
- if (Number.isNaN(expires) || expires <= Date.parse(issuedAt))
10172
- throw new Error("batch authorization must have a future expiry");
10173
- validateBudget(binding.budget, issuedAt);
10174
- },
10175
- validateAndProject(state, expected, now) {
10176
- if (!Number.isFinite(now))
10177
- throw new Error("batch authorization requires a valid clock");
10178
- const expires = Date.parse(state.expires_at);
10179
- if (Number.isNaN(expires) || expires <= now)
10180
- throw new BatchAuthorizationExpiryError("batch authorization has expired");
10181
- if (Date.parse(state.budget.deadline_at) <= now)
10182
- throw new BatchAuthorizationExpiryError("batch authorization deadline has expired");
10183
- for (const key of Object.keys(expected)) {
10184
- if (key === "budget") {
10185
- const a = state.budget ?? {};
10186
- const b = expected.budget ?? {};
10187
- if (a.max_children !== b.max_children || a.deadline_at !== b.deadline_at || a.qa_failure_limit !== b.qa_failure_limit)
10188
- throw new Error("batch authorization budget mismatch");
10189
- continue;
10190
- }
10191
- if (state[key] !== expected[key])
10192
- throw new Error(`batch authorization ${key} mismatch`);
10193
- }
10194
- return {
10195
- batch_id: state.batch_id,
10196
- initiative_slug: state.initiative_slug,
10197
- plan_digest: state.plan_digest,
10198
- branch: state.branch,
10199
- base_head: state.base_head,
10200
- budget: { ...state.budget },
10201
- actor_id: state.actor_id,
10202
- confirmation_ref: state.confirmation_ref,
10203
- issued_at: state.issued_at,
10204
- expires_at: state.expires_at,
10205
- nonce: state.nonce
10206
- };
10207
- }
10208
- }, "batch authorization");
10209
- const plans = new WeakMap;
10210
- const consumed = new WeakMap;
10211
- function planOf(capability) {
10212
- const plan = plans.get(capability);
10213
- if (!plan)
10214
- throw new Error("batch authorization capability is not recognized by this registry");
10215
- return plan;
10216
- }
10217
- function slotsOf(capability) {
10218
- const slots = consumed.get(capability);
10219
- if (!slots)
10220
- throw new Error("batch authorization capability is not recognized by this registry");
10221
- return slots;
10222
- }
10223
- return {
10224
- brand: inner.brand,
10225
- issue(binding, children, issuedAt = new Date().toISOString()) {
10226
- requireNonEmpty(binding);
10227
- validateChildren(children, binding.plan_digest);
10228
- const capability = inner.issue(binding, issuedAt);
10229
- plans.set(capability, children.map((child) => ({ ...child, blocked_by: [...child.blocked_by] })));
10230
- consumed.set(capability, new Set);
10231
- return capability;
10232
- },
10233
- inspect(capability, expected, now = Date.now()) {
10234
- planOf(capability);
10235
- return inner.inspect(capability, expected, now);
10236
- },
10237
- children(capability) {
10238
- return planOf(capability).map((child) => ({ ...child, blocked_by: [...child.blocked_by] }));
10239
- },
10240
- consumedChildren(capability) {
10241
- return [...slotsOf(capability)];
10242
- },
10243
- isChildConsumed(capability, taskId) {
10244
- return slotsOf(capability).has(taskId);
10245
- },
10246
- consumeChild(capability, expected, taskId, now = Date.now()) {
10247
- const validated = this.inspect(capability, expected, now);
10248
- const plan = planOf(capability);
10249
- if (!plan.some((child) => child.task_id === taskId))
10250
- throw new Error(`batch_child_not_in_plan: ${taskId}`);
10251
- const slots = slotsOf(capability);
10252
- if (slots.has(taskId))
10253
- throw new Error(`batch_child_slot_consumed: ${taskId}`);
10254
- slots.add(taskId);
10255
- return validated;
10256
- },
10257
- releaseChild(capability, taskId) {
10258
- slotsOf(capability).delete(taskId);
10259
- },
10260
- isExhausted(capability) {
10261
- return slotsOf(capability).size >= planOf(capability).length;
10262
- }
10263
- };
10264
- }
10265
- function deriveChildEnrollment(root, registry, input) {
10266
- const validated = registry.inspect(input.capability, input.binding, Date.parse(input.now));
10267
- const child = registry.children(input.capability).find((entry) => entry.task_id === input.task_id);
10268
- if (!child)
10269
- throw new Error(`batch_child_not_in_plan: ${input.task_id}`);
10270
- if (registry.isChildConsumed(input.capability, input.task_id))
10271
- throw new Error(`batch_child_slot_consumed: ${input.task_id}`);
10272
- if (!GIT_COMMIT_ID2.test(input.expected_head))
10273
- throw new Error("batch_head_lineage_broken: expected_head is not a commit id");
10274
- const preparation = preparePiCanary(root, { task_id: input.task_id, now: input.now });
10275
- if (!preparation.git_base_head)
10276
- throw new Error(preparation.git_error ?? "enrollment requires a committed Git HEAD");
10277
- if (preparation.git_base_head !== input.expected_head)
10278
- throw new Error(`batch_head_lineage_broken: expected ${input.expected_head}, found ${preparation.git_base_head}`);
10279
- if (!preparation.intent)
10280
- throw new Error(`batch_child_intent_changed: ${input.task_id} intent sidecar is unreadable`);
10281
- if (preparation.intent.path !== child.intent_path || preparation.intent.revision !== child.intent_revision || preparation.intent.content_hash !== child.intent_content_hash)
10282
- throw new Error(`batch_child_intent_changed: ${input.task_id}`);
10283
- if (registry.consumedChildren(input.capability).length === 0 && input.expected_head !== validated.base_head)
10284
- throw new Error(`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${validated.base_head}, not ${input.expected_head}`);
10285
- return {
10286
- child,
10287
- preparation,
10288
- binding: {
10289
- task_id: child.task_id,
10290
- intent_path: child.intent_path,
10291
- intent_revision: child.intent_revision,
10292
- intent_content_hash: child.intent_content_hash,
10293
- preparation_digest: preparation.digest,
10294
- actor_id: validated.actor_id,
10295
- confirmation_ref: validated.confirmation_ref,
10296
- expires_at: validated.expires_at,
10297
- nonce: `${validated.nonce}:${child.task_id}`
10298
- }
10299
- };
10300
- }
10151
+ import { spawnSync as spawnSync7 } from "node:child_process";
10301
10152
 
10302
10153
  // plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts
10303
- import { spawnSync as spawnSync5 } from "node:child_process";
10154
+ import { spawnSync as spawnSync6 } from "node:child_process";
10304
10155
 
10305
10156
  // plugins/immune-brain/runtime/unattended/batch_state.ts
10306
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";
@@ -10339,8 +10190,13 @@ function statePath(batchId) {
10339
10190
  validateBatchId(batchId);
10340
10191
  return join11(".imm", "state", "batches", `${batchId}.json`);
10341
10192
  }
10193
+ function withoutRetiredClock(record) {
10194
+ const { authorization_expires_at: _expiry, ...rest } = record;
10195
+ const { deadline_at: _deadline, ...budget } = rest.budget;
10196
+ return { ...rest, budget };
10197
+ }
10342
10198
  function canonicalBytes(record) {
10343
- return `${JSON.stringify(record, null, 2)}
10199
+ return `${JSON.stringify(withoutRetiredClock(record), null, 2)}
10344
10200
  `;
10345
10201
  }
10346
10202
  function validateRecordShape(value, batchId) {
@@ -10359,8 +10215,6 @@ function validateRecordShape(value, batchId) {
10359
10215
  throw new Error(`batch run state ${batchId} has an invalid branch`);
10360
10216
  if (!isCanonicalTimestamp(record.confirmation_time))
10361
10217
  throw new Error(`batch run state ${batchId} has an invalid confirmation_time`);
10362
- if (!isCanonicalTimestamp(record.authorization_expires_at))
10363
- throw new Error(`batch run state ${batchId} has an invalid authorization_expires_at`);
10364
10218
  if (!isCanonicalTimestamp(record.created_at) || !isCanonicalTimestamp(record.updated_at))
10365
10219
  throw new Error(`batch run state ${batchId} has invalid state timestamps`);
10366
10220
  if (!Array.isArray(record.children) || record.children.length === 0)
@@ -10370,10 +10224,12 @@ function validateRecordShape(value, batchId) {
10370
10224
  if (typeof record.consecutive_qa_failures !== "number" || !Number.isInteger(record.consecutive_qa_failures) || record.consecutive_qa_failures < 0)
10371
10225
  throw new Error(`batch run state ${batchId} has an invalid consecutive_qa_failures`);
10372
10226
  const budget = record.budget;
10373
- 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)
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)
10374
10228
  throw new Error(`batch run state ${batchId} has an invalid budget`);
10375
10229
  if (!Array.isArray(record.commits) || record.commits.some((c) => typeof c !== "string"))
10376
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`);
10377
10233
  const seenTaskIds = new Set;
10378
10234
  for (const child of record.children) {
10379
10235
  if (typeof child !== "object" || child === null || typeof child.task_id !== "string" || !child.task_id || typeof child.slice_id !== "string" || !child.slice_id)
@@ -10413,7 +10269,6 @@ function prepareBatchRunState(input) {
10413
10269
  base_head: input.base_head,
10414
10270
  branch: input.branch ?? `imm/${input.initiative_slug}`,
10415
10271
  confirmation_time: input.confirmation_time,
10416
- authorization_expires_at: input.authorization_expires_at,
10417
10272
  budget: input.budget,
10418
10273
  batch_state: "prepared",
10419
10274
  children: input.children.map((child) => ({
@@ -10435,7 +10290,7 @@ function parseBatchRunState(raw, batchId) {
10435
10290
  validateBatchId(batchId);
10436
10291
  const parsed = JSON.parse(raw);
10437
10292
  validateRecordShape(parsed, batchId);
10438
- return parsed;
10293
+ return withoutRetiredClock(parsed);
10439
10294
  }
10440
10295
  function readBatchRunState(root, batchId) {
10441
10296
  const path = statePath(batchId);
@@ -10488,7 +10343,7 @@ function replaceBatchRunState(root, expected, next, validate) {
10488
10343
  if (!readSecureProjectBytes(root, path).equals(expected))
10489
10344
  throw new Error("batch state CAS mismatch");
10490
10345
  validate();
10491
- const stored = { ...next, updated_at: new Date().toISOString() };
10346
+ const stored = withoutRetiredClock({ ...next, updated_at: new Date().toISOString() });
10492
10347
  writeFileAtomically(root, path, canonicalBytes(stored));
10493
10348
  return stored;
10494
10349
  });
@@ -10500,10 +10355,10 @@ function writeBatchRunState(root, record) {
10500
10355
  const existing = existsSync6(join11(root, path)) ? readSecureProjectFile(root, path) : null;
10501
10356
  if (existing !== null && existing === canonicalBytes(record))
10502
10357
  return record;
10503
- const stored = {
10358
+ const stored = withoutRetiredClock({
10504
10359
  ...record,
10505
10360
  updated_at: new Date().toISOString()
10506
- };
10361
+ });
10507
10362
  ensureSecureDirectory2(root, join11(".imm", "state", "batches"));
10508
10363
  writeFileAtomically(root, path, canonicalBytes(stored));
10509
10364
  return stored;
@@ -10552,7 +10407,7 @@ function matchesLocalProof(localJson, exported) {
10552
10407
  return localJson !== null && JSON.stringify(Object.entries(JSON.parse(localJson)).sort()) === JSON.stringify(Object.entries(exported).sort());
10553
10408
  }
10554
10409
  function git4(root, args) {
10555
- const r = spawnSync5("git", ["-C", root, ...args], { encoding: "buffer", maxBuffer: 262144, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
10410
+ const r = spawnSync6("git", ["-C", root, ...args], { encoding: "buffer", maxBuffer: 262144, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
10556
10411
  if (r.status !== 0)
10557
10412
  refuse();
10558
10413
  return decode(r.stdout);
@@ -10601,20 +10456,17 @@ function ownUnpersistedBatchHead(root, record, head) {
10601
10456
  }
10602
10457
  }
10603
10458
  var confirmations = new Map;
10604
- function retainReconfirmation(nonce, expires, snapshot) {
10605
- for (const [key, value] of confirmations)
10606
- if (value.expires <= Date.now())
10607
- confirmations.delete(key);
10459
+ function retainReconfirmation(nonce, snapshot) {
10608
10460
  if (confirmations.has(nonce))
10609
10461
  refuse();
10610
- confirmations.set(nonce, { snapshot, expires: Date.parse(expires) });
10462
+ confirmations.set(nonce, snapshot);
10611
10463
  }
10612
10464
  function takeReconfirmation(nonce) {
10613
10465
  const found = confirmations.get(nonce);
10614
10466
  confirmations.delete(nonce);
10615
- if (!found || found.expires <= Date.now())
10467
+ if (!found)
10616
10468
  refuse();
10617
- return found.snapshot;
10469
+ return found;
10618
10470
  }
10619
10471
  async function captureBatchReconfirmation(root, record, children) {
10620
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)
@@ -10759,7 +10611,6 @@ async function captureBatchReconfirmation(root, record, children) {
10759
10611
  // plugins/immune-brain/runtime/unattended/batch_plan.ts
10760
10612
  import { createHash as createHash19 } from "node:crypto";
10761
10613
  var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10762
- var DEFAULT_DEADLINE_MS = 8 * 60 * 60 * 1000;
10763
10614
  var DEFAULT_QA_FAILURE_LIMIT = 2;
10764
10615
  function specBindingReason(inspection) {
10765
10616
  if (inspection.missing.length > 0)
@@ -10852,15 +10703,12 @@ function dependencyClosures(order) {
10852
10703
  }
10853
10704
  return closures;
10854
10705
  }
10855
- function budget(input, enrollableCount, confirmationTime) {
10706
+ function budget(input, enrollableCount) {
10856
10707
  const maxChildren = input.budget?.max_children === undefined ? positiveSafeInteger(enrollableCount, "budget.max_children") : positiveSafeInteger(input.budget.max_children, "budget.max_children");
10857
10708
  if (maxChildren > enrollableCount)
10858
10709
  throw new Error("budget.max_children exceeds the enrollable Child count");
10859
10710
  const qaFailureLimit = input.budget?.qa_failure_limit === undefined ? DEFAULT_QA_FAILURE_LIMIT : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
10860
- const deadline = input.budget?.deadline_at === undefined ? new Date(confirmationTime.milliseconds + DEFAULT_DEADLINE_MS).toISOString() : timestamp(input.budget.deadline_at, "budget.deadline_at").iso;
10861
- if (Date.parse(deadline) <= confirmationTime.milliseconds)
10862
- throw new Error("budget.deadline_at must be later than confirmation_time");
10863
- return { max_children: maxChildren, deadline_at: deadline, qa_failure_limit: qaFailureLimit };
10711
+ return { max_children: maxChildren, qa_failure_limit: qaFailureLimit };
10864
10712
  }
10865
10713
  async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeGithubInitiative) {
10866
10714
  if (!ID_PATTERN2.test(initiativeSlug))
@@ -10958,7 +10806,7 @@ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = ob
10958
10806
  children,
10959
10807
  enrollable,
10960
10808
  plan_digest: planDigest,
10961
- budget: budget(input, enrollable.length, confirmationTime)
10809
+ budget: budget(input, enrollable.length)
10962
10810
  };
10963
10811
  }
10964
10812
 
@@ -10980,6 +10828,27 @@ function readActiveClaimTaskId(root) {
10980
10828
  const claim = readBackendClaim(root);
10981
10829
  return workspace.state.current_working || (claim?.lifecycle_status === "active" ? claim.task_id : null);
10982
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
+ }
10983
10852
  function findExistingActiveBatch(root, initiativeSlug) {
10984
10853
  const batchesDir = join12(root, ".imm", "state", "batches");
10985
10854
  if (!existsSync7(batchesDir))
@@ -11051,9 +10920,51 @@ function findSettledBatchRecord(root, initiativeSlug) {
11051
10920
  }
11052
10921
  return newest;
11053
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
+ }
11054
10960
  function expectedBatchHead(record) {
11055
10961
  const commits = Array.isArray(record.commits) ? record.commits : [];
11056
- 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;
11057
10968
  }
11058
10969
  function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
11059
10970
  let claim = null;
@@ -11067,7 +10978,7 @@ function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
11067
10978
  const currentTaskId = workspaceOwner || (claim?.lifecycle_status === "active" ? claim?.task_id : null);
11068
10979
  if (currentTaskId !== taskId || !claim)
11069
10980
  return false;
11070
- const branch = spawnSync6("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" }).stdout.trim();
10981
+ const branch = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" }).stdout.trim();
11071
10982
  if (branch !== batchBranch)
11072
10983
  return false;
11073
10984
  const childInBatch = existingBatch.children.find((c) => c.task_id === taskId);
@@ -11083,7 +10994,7 @@ function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
11083
10994
  }
11084
10995
  if (!rec)
11085
10996
  return false;
11086
- 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));
11087
10998
  if (!lineageHeads.includes(rec.git_base_head))
11088
10999
  return false;
11089
11000
  if (claim.enrollment_event_id !== `enroll-${taskId}-${claim.created_at}`)
@@ -11124,7 +11035,7 @@ function authorizedScopeOf(root, taskId, state) {
11124
11035
  return scope;
11125
11036
  }
11126
11037
  function porcelainEntries(root) {
11127
- const statusProc = spawnSync6("git", ["-C", root, "status", "--porcelain=v1", "-z", "--no-renames", "--untracked-files=all", "--ignore-submodules=none"], { encoding: "utf8", env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
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" } });
11128
11039
  if (statusProc.status !== 0)
11129
11040
  return null;
11130
11041
  const entries = [];
@@ -11143,9 +11054,10 @@ async function projectPlanSurface(input) {
11143
11054
  const riskByTask = new Map;
11144
11055
  let budget;
11145
11056
  if (isResuming && existingBatch) {
11146
- budget = { ...existingBatch.budget };
11147
- if (Date.parse(budget.deadline_at) <= Date.parse(now))
11148
- 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
+ };
11149
11061
  try {
11150
11062
  recoveryChildren = existingBatch.children.map((c) => {
11151
11063
  const intentPath = `docs/plans/${c.task_id}.intent.json`;
@@ -11262,7 +11174,7 @@ async function projectBatchPreflight(options) {
11262
11174
  } catch (err) {
11263
11175
  return reject("git_head_unreadable", err instanceof Error ? err.message : String(err));
11264
11176
  }
11265
- const branchExists = spawnSync6("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${batchBranch}`]);
11177
+ const branchExists = spawnSync7("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${batchBranch}`]);
11266
11178
  if (branchExists.status === 0 && !existingBatch)
11267
11179
  return reject("branch_already_exists", batchBranch);
11268
11180
  const statusEntries = porcelainEntries(root);
@@ -11373,37 +11285,37 @@ async function authorizeBatch(options) {
11373
11285
  is_resuming: isResuming
11374
11286
  } = projection;
11375
11287
  const existingBatch = projection.existing_batch;
11376
- const isExistingExpired = isResuming && Date.parse(existingBatch.authorization_expires_at) <= Date.now();
11377
- const branchBefore = spawnSync6("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11288
+ const branchBefore = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11378
11289
  if (branchBefore.status !== 0)
11379
11290
  return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
11380
11291
  const ownsUnpersistedHead = isResuming && existingBatch !== null && existingBatch.plan_digest === planDigest && expectedBatchHead(existingBatch) !== baseHead && ownUnpersistedBatchHead(root, existingBatch, baseHead);
11381
- if (isResuming && existingBatch && !existingBatch.commits.length && existingBatch.children[0]?.state === "settled" && expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead)
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)
11382
11300
  return { outcome: "rejected", rejection: batchRejection("head_moved", "first unpersisted commit provenance is invalid") };
11383
11301
  const reuseBlockers = [];
11384
11302
  if (isResuming && existingBatch) {
11385
- if (isExistingExpired)
11386
- reuseBlockers.push("batch_authorization_expired");
11387
- if (Date.parse(existingBatch.budget.deadline_at) <= Date.now())
11388
- reuseBlockers.push("batch_budget_expired");
11389
11303
  if (existingBatch.batch_state !== "running")
11390
11304
  reuseBlockers.push("batch_not_running");
11391
11305
  if (existingBatch.plan_digest !== planDigest)
11392
11306
  reuseBlockers.push("batch_plan_digest_changed");
11393
11307
  if (existingBatch.branch !== batchBranch)
11394
11308
  reuseBlockers.push("batch_branch_changed");
11395
- if (expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead)
11309
+ if (expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead && !fastForwardsRecordedHead)
11396
11310
  reuseBlockers.push("batch_head_lineage_moved");
11397
11311
  }
11398
11312
  const reuseAuthorization = isResuming && reuseBlockers.length === 0;
11399
- const expiresAt = reuseAuthorization ? existingBatch.authorization_expires_at : budget.deadline_at;
11400
11313
  const batchId = isResuming && existingBatch ? existingBatch.batch_id : `batch-${initiativeSlug}-${randomUUID7()}`;
11401
11314
  const facts = {
11402
11315
  initiative_slug: initiativeSlug,
11403
11316
  batch_branch: batchBranch,
11404
11317
  plan_digest: planDigest,
11405
11318
  budget,
11406
- expires_at: expiresAt,
11407
11319
  reuse_blockers: reuseBlockers,
11408
11320
  children: projection.recovery_children.map((child) => ({
11409
11321
  task_id: child.task_id,
@@ -11445,11 +11357,9 @@ async function authorizeBatch(options) {
11445
11357
  if (!current || current.corrupt || JSON.stringify(current.record) !== JSON.stringify(existingBatch))
11446
11358
  return { outcome: "rejected", rejection: batchRejection("plan_changed") };
11447
11359
  }
11448
- const branchAfter = spawnSync6("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11360
+ const branchAfter = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11449
11361
  if (branchAfter.status !== 0 || branchAfter.stdout !== branchBefore.stdout)
11450
11362
  return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "Git branch moved after native confirmation") };
11451
- if (Date.parse(expiresAt) <= Date.now())
11452
- return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "confirmed batch deadline expired before authorization issuance") };
11453
11363
  if (projection.reconfirmation) {
11454
11364
  try {
11455
11365
  projection.reconfirmation.assertUnchanged();
@@ -11466,28 +11376,26 @@ async function authorizeBatch(options) {
11466
11376
  budget,
11467
11377
  actor_id: LITERAL_USER_ACTOR_ID,
11468
11378
  confirmation_ref: options.confirmationRef({ batch_id: batchId, request_id: requestId }),
11469
- expires_at: expiresAt,
11470
11379
  nonce: options.nonce
11471
11380
  };
11472
11381
  if (projection.reconfirmation)
11473
- retainReconfirmation(binding.nonce, binding.expires_at, projection.reconfirmation);
11382
+ retainReconfirmation(binding.nonce, projection.reconfirmation);
11474
11383
  return {
11475
11384
  outcome: "authorized",
11476
11385
  batch_id: batchId,
11477
11386
  reuse_authorization: reuseAuthorization,
11478
11387
  reuse_blockers: reuseBlockers,
11479
- expires_at: expiresAt,
11480
11388
  binding
11481
11389
  };
11482
11390
  }
11483
11391
 
11484
11392
  // plugins/immune-brain/runtime/unattended/batch_runner.ts
11485
- import { spawnSync as spawnSync8 } from "node:child_process";
11393
+ import { spawnSync as spawnSync9 } from "node:child_process";
11486
11394
  import { existsSync as existsSync9 } from "node:fs";
11487
11395
  import { join as join14 } from "node:path";
11488
11396
 
11489
11397
  // plugins/immune-brain/runtime/unattended/batch_git.ts
11490
- import { spawnSync as spawnSync7 } from "node:child_process";
11398
+ import { spawnSync as spawnSync8 } from "node:child_process";
11491
11399
  import { createHash as createHash20, randomUUID as randomUUID8 } from "node:crypto";
11492
11400
  import {
11493
11401
  constants as constants6,
@@ -11512,7 +11420,7 @@ var DEFAULT_GIT_ENV = {
11512
11420
  function runBatchGitPreflight(input) {
11513
11421
  const { root, initiative_slug: initiativeSlug, base_head: baseHead } = input;
11514
11422
  const branch = `imm/${initiativeSlug}`;
11515
- const toplevelResult = spawnSync7("git", ["-C", root, "rev-parse", "--show-toplevel"], {
11423
+ const toplevelResult = spawnSync8("git", ["-C", root, "rev-parse", "--show-toplevel"], {
11516
11424
  encoding: "utf8",
11517
11425
  stdio: ["ignore", "pipe", "pipe"]
11518
11426
  });
@@ -11550,7 +11458,7 @@ function runBatchGitPreflight(input) {
11550
11458
  message: `unsupported index flags (assume-unchanged/skip-worktree) detected: ${flaggedPreflight.join(", ")}`
11551
11459
  };
11552
11460
  }
11553
- const statusResult = spawnSync7("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"], {
11554
11462
  encoding: "utf8",
11555
11463
  stdio: ["ignore", "pipe", "pipe"]
11556
11464
  });
@@ -11568,7 +11476,7 @@ function runBatchGitPreflight(input) {
11568
11476
  message: "working tree is dirty before batch preflight"
11569
11477
  };
11570
11478
  }
11571
- const headResult = spawnSync7("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
11479
+ const headResult = spawnSync8("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
11572
11480
  encoding: "utf8",
11573
11481
  stdio: ["ignore", "pipe", "pipe"]
11574
11482
  });
@@ -11579,11 +11487,11 @@ function runBatchGitPreflight(input) {
11579
11487
  message: "HEAD is uncommitted or not a valid commit"
11580
11488
  };
11581
11489
  }
11582
- const branchCheck = spawnSync7("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"] });
11583
11491
  if (branchCheck.status === 0) {
11584
11492
  const settled = findSettledBatchRecord(root, initiativeSlug);
11585
- const currentBranch = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], { encoding: "utf8" });
11586
- if (findExistingActiveBatch(root, initiativeSlug) === null && settled?.branch === branch && currentBranch.status === 0 && currentBranch.stdout.trim() === branch && headResult.stdout.trim() === baseHead && spawnSync7("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)
11587
11495
  return { ok: true, branch };
11588
11496
  return {
11589
11497
  ok: false,
@@ -11591,25 +11499,25 @@ function runBatchGitPreflight(input) {
11591
11499
  message: `branch refs/heads/${branch} already exists`
11592
11500
  };
11593
11501
  }
11594
- const originalBranchResult = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11502
+ const originalBranchResult = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11595
11503
  encoding: "utf8",
11596
11504
  stdio: ["ignore", "pipe", "pipe"]
11597
11505
  });
11598
11506
  const originalBranch = originalBranchResult.stdout.trim();
11599
- const checkoutResult = spawnSync7("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], {
11600
11508
  encoding: "utf8",
11601
11509
  stdio: ["ignore", "pipe", "pipe"],
11602
11510
  env: DEFAULT_GIT_ENV
11603
11511
  });
11604
11512
  if (checkoutResult.status !== 0) {
11605
11513
  const stderr = checkoutResult.stderr?.trim() || "";
11606
- const branchExists = stderr.includes("already exists") || spawnSync7("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
11607
- const currentBranchCheck = spawnSync7("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"], {
11608
11516
  encoding: "utf8",
11609
11517
  stdio: ["ignore", "pipe", "pipe"]
11610
11518
  }).stdout.trim();
11611
11519
  if (currentBranchCheck === branch && originalBranch && originalBranch !== branch) {
11612
- spawnSync7("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", originalBranch], {
11520
+ spawnSync8("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", originalBranch], {
11613
11521
  stdio: ["ignore", "ignore", "ignore"]
11614
11522
  });
11615
11523
  }
@@ -11632,7 +11540,7 @@ function hasBoundaryWhitespace(path) {
11632
11540
  return path.split("/").some((segment) => segment.trim() !== segment || segment.length === 0);
11633
11541
  }
11634
11542
  function getUnsupportedIndexFlags(root) {
11635
- const result = spawnSync7("git", ["-C", root, "ls-files", "-v", "-z", "--"], {
11543
+ const result = spawnSync8("git", ["-C", root, "ls-files", "-v", "-z", "--"], {
11636
11544
  encoding: "utf8",
11637
11545
  stdio: ["ignore", "pipe", "pipe"]
11638
11546
  });
@@ -11666,13 +11574,13 @@ function isPathAllowedForChild(path, taskId, scopeHint) {
11666
11574
  });
11667
11575
  }
11668
11576
  function getChangedProjectPaths(root) {
11669
- const tracked = spawnSync7("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", "--"], {
11670
11578
  encoding: "utf8",
11671
11579
  stdio: ["ignore", "pipe", "pipe"]
11672
11580
  });
11673
11581
  if (tracked.status !== 0)
11674
11582
  throw new Error("failed to inspect tracked diff vs HEAD");
11675
- const untracked = spawnSync7("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
11583
+ const untracked = spawnSync8("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
11676
11584
  encoding: "utf8",
11677
11585
  stdio: ["ignore", "pipe", "pipe"]
11678
11586
  });
@@ -11682,7 +11590,7 @@ function getChangedProjectPaths(root) {
11682
11590
  return [...new Set([...splitZ(tracked.stdout), ...splitZ(untracked.stdout)])];
11683
11591
  }
11684
11592
  function getStagedProjectPaths(root) {
11685
- const staged = spawnSync7("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", "--"], {
11686
11594
  encoding: "utf8",
11687
11595
  stdio: ["ignore", "pipe", "pipe"]
11688
11596
  });
@@ -11691,13 +11599,13 @@ function getStagedProjectPaths(root) {
11691
11599
  return staged.stdout.split("\x00").filter((p) => p.length > 0);
11692
11600
  }
11693
11601
  function getUnstagedProjectPaths(root) {
11694
- const diffFiles = spawnSync7("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", "--"], {
11695
11603
  encoding: "utf8",
11696
11604
  stdio: ["ignore", "pipe", "pipe"]
11697
11605
  });
11698
11606
  if (diffFiles.status !== 0)
11699
11607
  throw new Error("failed to inspect unstaged tracked changes");
11700
- const untracked = spawnSync7("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
11608
+ const untracked = spawnSync8("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
11701
11609
  encoding: "utf8",
11702
11610
  stdio: ["ignore", "pipe", "pipe"]
11703
11611
  });
@@ -11707,7 +11615,7 @@ function getUnstagedProjectPaths(root) {
11707
11615
  return [...new Set([...splitZ(diffFiles.stdout), ...splitZ(untracked.stdout)])];
11708
11616
  }
11709
11617
  function getCommittedDeltaPaths(root) {
11710
- const delta = spawnSync7("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"] });
11711
11619
  if (delta.status !== 0)
11712
11620
  throw new Error("failed to inspect committed tree delta");
11713
11621
  return delta.stdout.split("\x00").filter((p) => p.length > 0);
@@ -11813,7 +11721,7 @@ async function commitBatchChild(input) {
11813
11721
  const goal = typeof intentSnapshot.goal === "string" ? intentSnapshot.goal : "";
11814
11722
  const scopeHint = Array.isArray(intentSnapshot.scope_hint) ? intentSnapshot.scope_hint.filter((s) => typeof s === "string") : [];
11815
11723
  if (expectedBranch !== undefined) {
11816
- const currentBranchResult = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11724
+ const currentBranchResult = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11817
11725
  encoding: "utf8",
11818
11726
  stdio: ["ignore", "pipe", "pipe"]
11819
11727
  });
@@ -11822,7 +11730,7 @@ async function commitBatchChild(input) {
11822
11730
  throw new Error(`batch_head_lineage_broken: current branch ${currentBranch} does not match expected branch ${expectedBranch}`);
11823
11731
  }
11824
11732
  }
11825
- const currentHeadResult = spawnSync7("git", ["-C", root, "rev-parse", "HEAD"], {
11733
+ const currentHeadResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
11826
11734
  encoding: "utf8",
11827
11735
  stdio: ["ignore", "pipe", "pipe"]
11828
11736
  });
@@ -11841,7 +11749,7 @@ async function commitBatchChild(input) {
11841
11749
  }
11842
11750
  const pathsToStage = getUnstagedProjectPaths(root);
11843
11751
  if (pathsToStage.length > 0) {
11844
- const addResult = spawnSync7("git", ["-C", root, "--literal-pathspecs", "add", "-A", "--", ...pathsToStage], {
11752
+ const addResult = spawnSync8("git", ["-C", root, "--literal-pathspecs", "add", "-A", "--", ...pathsToStage], {
11845
11753
  encoding: "utf8",
11846
11754
  stdio: ["ignore", "pipe", "pipe"]
11847
11755
  });
@@ -11856,7 +11764,7 @@ async function commitBatchChild(input) {
11856
11764
  const staged = getStagedProjectPaths(root);
11857
11765
  const stagedOutside = staged.filter((p) => !isPathAllowedForChild(p, taskId, scopeHint));
11858
11766
  if (stagedOutside.length > 0) {
11859
- spawnSync7("git", ["-C", root, "reset", "--quiet"], { stdio: ["ignore", "ignore", "ignore"] });
11767
+ spawnSync8("git", ["-C", root, "reset", "--quiet"], { stdio: ["ignore", "ignore", "ignore"] });
11860
11768
  throw new Error("dirty_outside_scope");
11861
11769
  }
11862
11770
  const record = auditPair.record;
@@ -11874,7 +11782,7 @@ async function commitBatchChild(input) {
11874
11782
 
11875
11783
  Immune-Brain-Batch: ${batchId}
11876
11784
  `;
11877
- const commitResult = spawnSync7("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", "-"], {
11878
11786
  input: commitMessage,
11879
11787
  encoding: "utf8",
11880
11788
  stdio: ["pipe", "pipe", "pipe"],
@@ -11883,7 +11791,7 @@ Immune-Brain-Batch: ${batchId}
11883
11791
  if (commitResult.status !== 0) {
11884
11792
  throw new Error(`commit failed for child ${taskId}: ${commitResult.stderr?.trim() || "git commit failed"}`);
11885
11793
  }
11886
- const newHeadResult = spawnSync7("git", ["-C", root, "rev-parse", "HEAD"], {
11794
+ const newHeadResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
11887
11795
  encoding: "utf8",
11888
11796
  stdio: ["ignore", "pipe", "pipe"]
11889
11797
  });
@@ -11891,7 +11799,7 @@ Immune-Brain-Batch: ${batchId}
11891
11799
  if (newHeadResult.status !== 0 || !newHead || newHead === expectedHead) {
11892
11800
  throw new Error("commit_failed");
11893
11801
  }
11894
- const parentsResult = spawnSync7("git", ["-C", root, "rev-parse", "HEAD^@"], {
11802
+ const parentsResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD^@"], {
11895
11803
  encoding: "utf8",
11896
11804
  stdio: ["ignore", "pipe", "pipe"]
11897
11805
  });
@@ -11924,7 +11832,7 @@ async function lookupBatchCommit(input) {
11924
11832
  let candidates = [];
11925
11833
  let evidenceBacked = false;
11926
11834
  if (evidence && evidence.commit) {
11927
- const direct = spawnSync7("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"] });
11928
11836
  if (direct.status !== 0) {
11929
11837
  throw new Error(`failed to inspect batch commit for ${taskId}: ${direct.stderr?.trim() || "git log failed"}`);
11930
11838
  }
@@ -11937,7 +11845,7 @@ async function lookupBatchCommit(input) {
11937
11845
  }
11938
11846
  }
11939
11847
  if (!candidates.length) {
11940
- const result = spawnSync7("git", [
11848
+ const result = spawnSync8("git", [
11941
11849
  "-C",
11942
11850
  root,
11943
11851
  "log",
@@ -11974,7 +11882,7 @@ async function lookupBatchCommit(input) {
11974
11882
  throw new Error(`batch_head_lineage_broken: adopted commit author ${match.authorName} does not match batch authority ${DEFAULT_GIT_ENV.GIT_AUTHOR_NAME}`);
11975
11883
  }
11976
11884
  if (expectedBranch !== undefined) {
11977
- const currentBranchResult = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11885
+ const currentBranchResult = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11978
11886
  encoding: "utf8",
11979
11887
  stdio: ["ignore", "pipe", "pipe"]
11980
11888
  });
@@ -11984,15 +11892,17 @@ async function lookupBatchCommit(input) {
11984
11892
  }
11985
11893
  }
11986
11894
  if (expectedHead !== undefined) {
11987
- const currentHeadResult = spawnSync7("git", ["-C", root, "rev-parse", "HEAD"], {
11895
+ const currentHeadResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
11988
11896
  encoding: "utf8",
11989
11897
  stdio: ["ignore", "pipe", "pipe"]
11990
11898
  });
11991
11899
  const currentHead = currentHeadResult.stdout.trim();
11992
- 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) {
11993
11903
  throw new Error(`batch_head_lineage_broken: current HEAD ${currentHead} diverged from adopted commit ${commit}`);
11994
11904
  }
11995
- const parentsResult = spawnSync7("git", ["-C", root, "rev-parse", `${commit}^@`], {
11905
+ const parentsResult = spawnSync8("git", ["-C", root, "rev-parse", `${commit}^@`], {
11996
11906
  encoding: "utf8",
11997
11907
  stdio: ["ignore", "pipe", "pipe"]
11998
11908
  });
@@ -12010,7 +11920,7 @@ async function lookupBatchCommit(input) {
12010
11920
  throw new Error(`batch_head_lineage_broken: adopted commit task lifecycle is not done: ${lifecycle}`);
12011
11921
  }
12012
11922
  const scopeHint = Array.isArray(auditPair.record.intent_snapshot.scope_hint) ? auditPair.record.intent_snapshot.scope_hint.filter((s) => typeof s === "string") : [];
12013
- const deltaResult = spawnSync7("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"] });
12014
11924
  if (deltaResult.status !== 0) {
12015
11925
  throw new Error("batch_head_lineage_broken: failed to inspect adopted commit delta");
12016
11926
  }
@@ -12049,18 +11959,12 @@ function skipDependents(record, taskId, reason) {
12049
11959
  }
12050
11960
  record.children = record.children.map((child) => skip.has(child.task_id) && child.state === "pending" ? { ...child, state: "skipped_blocked", reason } : child);
12051
11961
  }
12052
- function budgetStopReason(record, now) {
11962
+ function budgetStopReason(record) {
12053
11963
  const enrolledCount = record.children.filter((child) => child.state !== "pending" && child.state !== "skipped_blocked").length;
12054
11964
  if (enrolledCount >= record.budget.max_children)
12055
11965
  return `max_children budget exhausted (${record.budget.max_children})`;
12056
- const deadline = Date.parse(record.budget.deadline_at);
12057
- if (!Number.isNaN(deadline) && now >= deadline)
12058
- return `deadline_at reached (${record.budget.deadline_at})`;
12059
11966
  return null;
12060
11967
  }
12061
- function isAuthorizationExpiryError(error) {
12062
- return error instanceof BatchAuthorizationExpiryError;
12063
- }
12064
11968
  function requireFreshProjection(result, taskId) {
12065
11969
  if (result.error !== null)
12066
11970
  throw new Error(`cannot reconcile Kernel projection for ${taskId}: ${result.error}`);
@@ -12071,7 +11975,7 @@ function nextEnrollableChild(record) {
12071
11975
  }
12072
11976
  var TERMINAL_NEXT_ACTIONS = {
12073
11977
  completed: "The batch settled every enrollable child; review the commits and the tracker.",
12074
- 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.",
12075
11979
  failed: "A commit or lineage failure stopped the batch; inspect the failing child and the branch state.",
12076
11980
  rejected: "The batch was rejected before any enrollment; correct the stated reason and re-confirm.",
12077
11981
  needs_human: "A parked child needs a human decision; resolve it, then re-confirm to continue.",
@@ -12126,31 +12030,42 @@ function failPersistedLineage(root, existing, message) {
12126
12030
  }
12127
12031
  return writeBatchRunState(root, record);
12128
12032
  }
12129
- function externalHeadDriftMessage(root, record) {
12033
+ function reconcileLineage(root, record) {
12130
12034
  if (!existsSync9(join14(root, ".git")))
12131
- return null;
12132
- const head = record.commits.length ? record.commits[record.commits.length - 1] : record.base_head;
12133
- const headCheck = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
12134
- encoding: "utf8",
12135
- stdio: ["ignore", "pipe", "pipe"]
12136
- });
12137
- if (headCheck.status === 0 && headCheck.stdout.trim() && headCheck.stdout.trim() !== head) {
12138
- return `batch_head_lineage_broken: current HEAD ${headCheck.stdout.trim()} does not match expected batch head ${head}`;
12139
- }
12140
- const branchCheck = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
12141
- encoding: "utf8",
12142
- 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
12143
12043
  });
12144
- const currentBranch = branchCheck.stdout.trim();
12145
- if (branchCheck.status !== 0 || currentBranch !== record.branch) {
12146
- return `batch_head_lineage_broken: current branch ${currentBranch} does not match expected batch branch ${record.branch}`;
12147
- }
12148
- 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
+ };
12149
12055
  }
12150
12056
  async function validatePersistedRun(input, record) {
12151
12057
  if (!record.commits.length && record.children[0]?.state === "settled" && existsSync9(join14(input.root, ".git"))) {
12152
- const head = spawnSync8("git", ["-C", input.root, "rev-parse", "HEAD"], { encoding: "utf8" });
12153
- if (head.status !== 0 || head.stdout.trim() !== record.base_head && !ownUnpersistedBatchHead(input.root, record, head.stdout.trim()))
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())
12154
12069
  throw new Error("first unpersisted batch commit provenance is invalid");
12155
12070
  }
12156
12071
  const plan = input.registry.children(input.capability);
@@ -12174,7 +12089,7 @@ async function validatePersistedRun(input, record) {
12174
12089
  });
12175
12090
  if (!evidence || evidence.commit !== child.commit) {
12176
12091
  if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync9(join14(input.root, ".git"))) {
12177
- const reach = spawnSync8("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"] });
12178
12093
  if (reach.status !== 0) {
12179
12094
  throw new Error(`batch_head_lineage_broken: recorded commit ${child.commit} for ${child.task_id} is no longer reachable from HEAD`);
12180
12095
  }
@@ -12198,8 +12113,7 @@ function validateRunAuthorization(input, existing) {
12198
12113
  plan_digest: existing?.plan_digest ?? input.plan_digest,
12199
12114
  base_head: existing?.base_head ?? input.base_head,
12200
12115
  initiative_slug: existing?.initiative_slug ?? input.initiative_slug,
12201
- budget: input.budget,
12202
- expires_at: input.authorization_expires_at
12116
+ budget: input.budget
12203
12117
  }
12204
12118
  });
12205
12119
  if (authorized.issued_at !== input.confirmation_time || existing && Date.parse(authorized.issued_at) <= Date.parse(existing.confirmation_time))
@@ -12221,7 +12135,6 @@ function applyPlanReconfirmation(input, existing) {
12221
12135
  ...existing,
12222
12136
  plan_digest: input.plan_digest,
12223
12137
  confirmation_time: input.confirmation_time,
12224
- authorization_expires_at: input.authorization_expires_at,
12225
12138
  budget: input.budget
12226
12139
  };
12227
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)
@@ -12299,8 +12212,7 @@ async function startBatchLocked(input) {
12299
12212
  if (existing?.batch_state === "needs_human") {
12300
12213
  const priorConfirmation = Date.parse(existing.confirmation_time);
12301
12214
  const nextConfirmation = Date.parse(input.confirmation_time);
12302
- const nextExpiry = Date.parse(input.authorization_expires_at);
12303
- const freshAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation && Number.isFinite(nextExpiry) && nextExpiry > Date.now();
12215
+ const freshAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation;
12304
12216
  if (!freshAuthorization && !reconfirmed) {
12305
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.");
12306
12218
  }
@@ -12327,7 +12239,6 @@ async function startBatchLocked(input) {
12327
12239
  existing = writeBatchRunState(input.root, {
12328
12240
  ...reparked,
12329
12241
  confirmation_time: input.confirmation_time,
12330
- authorization_expires_at: input.authorization_expires_at,
12331
12242
  budget: input.budget,
12332
12243
  batch_state: "needs_human"
12333
12244
  });
@@ -12337,28 +12248,11 @@ async function startBatchLocked(input) {
12337
12248
  existing = writeBatchRunState(input.root, {
12338
12249
  ...existing,
12339
12250
  confirmation_time: input.confirmation_time,
12340
- authorization_expires_at: input.authorization_expires_at,
12341
12251
  budget: input.budget,
12342
12252
  batch_state: "running",
12343
12253
  children: resumedChildren
12344
12254
  });
12345
12255
  }
12346
- if (existing?.batch_state === "running") {
12347
- const nextConfirmation = Date.parse(input.confirmation_time);
12348
- const nextExpiry = Date.parse(input.authorization_expires_at);
12349
- const priorConfirmation = Date.parse(existing.confirmation_time);
12350
- const persistedExpiry = Date.parse(existing.authorization_expires_at);
12351
- const renewedAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation && Number.isFinite(nextExpiry) && nextExpiry > Date.now() && nextExpiry > persistedExpiry;
12352
- if (renewedAuthorization) {
12353
- validateRunAuthorization(input, existing);
12354
- existing = writeBatchRunState(input.root, {
12355
- ...existing,
12356
- confirmation_time: input.confirmation_time,
12357
- authorization_expires_at: input.authorization_expires_at,
12358
- budget: input.budget
12359
- });
12360
- }
12361
- }
12362
12256
  if (existing) {
12363
12257
  const interruptedChild = existing.children.find((child) => child.state === "enrolled" || child.state === "settled");
12364
12258
  if (interruptedChild) {
@@ -12396,7 +12290,6 @@ async function startBatchLocked(input) {
12396
12290
  plan_digest: input.plan_digest,
12397
12291
  base_head: input.base_head,
12398
12292
  confirmation_time: input.confirmation_time,
12399
- authorization_expires_at: input.authorization_expires_at,
12400
12293
  budget: input.budget,
12401
12294
  now: input.now
12402
12295
  });
@@ -12407,13 +12300,14 @@ async function startBatchLocked(input) {
12407
12300
  record.batch_state = "running";
12408
12301
  persist();
12409
12302
  }
12410
- let head = record.commits.length ? record.commits[record.commits.length - 1] : record.base_head;
12411
- const driftMessage = externalHeadDriftMessage(input.root, record);
12412
- if (driftMessage) {
12303
+ const lineage = reconcileLineage(input.root, record);
12304
+ record = lineage.record;
12305
+ if (lineage.failure) {
12413
12306
  record.batch_state = "failed";
12414
12307
  persist();
12415
- return finalize(input.root, record, driftMessage, "");
12308
+ return finalize(input.root, record, lineage.failure, "");
12416
12309
  }
12310
+ let head = expectedBatchHead(record);
12417
12311
  while (record.batch_state === "running") {
12418
12312
  const child = nextEnrollableChild(record);
12419
12313
  if (!child) {
@@ -12440,8 +12334,7 @@ async function startBatchLocked(input) {
12440
12334
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "enrolled", reason: "adopted existing batch claim after interruption" } : c);
12441
12335
  persist();
12442
12336
  } else {
12443
- const now = Date.now();
12444
- if (now >= Date.parse(record.authorization_expires_at) || budgetStopReason(record, now)) {
12337
+ if (budgetStopReason(record)) {
12445
12338
  record.batch_state = record.children.some((c) => c.state === "needs_human" || c.state === "skipped_blocked") ? "needs_human" : "budget_stopped";
12446
12339
  persist();
12447
12340
  break;
@@ -12456,8 +12349,7 @@ async function startBatchLocked(input) {
12456
12349
  plan_digest: record.plan_digest,
12457
12350
  base_head: record.base_head,
12458
12351
  initiative_slug: record.initiative_slug,
12459
- budget: record.budget,
12460
- expires_at: record.authorization_expires_at
12352
+ budget: record.budget
12461
12353
  }
12462
12354
  });
12463
12355
  for (const committed of record.children) {
@@ -12477,12 +12369,6 @@ async function startBatchLocked(input) {
12477
12369
  persist();
12478
12370
  } catch (error) {
12479
12371
  const message = error instanceof Error ? error.message : String(error);
12480
- if (isAuthorizationExpiryError(error)) {
12481
- record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "pending", reason: message } : c);
12482
- record.batch_state = "budget_stopped";
12483
- persist();
12484
- break;
12485
- }
12486
12372
  if (isLineageBreakError(message)) {
12487
12373
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
12488
12374
  skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
@@ -12505,7 +12391,7 @@ async function startBatchLocked(input) {
12505
12391
  function stopReasonFor(record) {
12506
12392
  switch (record.batch_state) {
12507
12393
  case "budget_stopped":
12508
- return budgetStopReason(record, Date.now()) ?? "budget, deadline, or authorization expiry stopped new enrollments";
12394
+ return budgetStopReason(record) ?? "the child budget stopped new enrollments";
12509
12395
  case "failed": {
12510
12396
  const failedChild = record.children.find((c) => c.state === "needs_human" && c.reason);
12511
12397
  return failedChild?.reason ?? "a commit or lineage failure stopped the batch";
@@ -12540,10 +12426,11 @@ async function resumeBatch(input, projection) {
12540
12426
  const driven = existing.children.find((child) => child.state === "enrolled" || child.state === "settled");
12541
12427
  if (driven) {
12542
12428
  if (driven.state === "enrolled") {
12543
- const drivenDrift = externalHeadDriftMessage(input.root, existing);
12544
- if (drivenDrift) {
12545
- const failed = failPersistedLineage(input.root, existing, drivenDrift);
12546
- 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, "");
12547
12434
  }
12548
12435
  const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, driven.task_id), driven.task_id);
12549
12436
  const holdsClaim = fresh.claim !== null && fresh.claim.task_id === driven.task_id && input.kernel.ownsTaskClaim(driven.task_id);
@@ -12622,7 +12509,7 @@ async function driveInterruptedChild(input, child) {
12622
12509
  record.batch_state = "running";
12623
12510
  persist();
12624
12511
  }
12625
- let head = record.commits.length ? record.commits[record.commits.length - 1] : record.base_head;
12512
+ let head = expectedBatchHead(record);
12626
12513
  while (child.state === "enrolled" || child.state === "settled") {
12627
12514
  if (child.state === "settled") {
12628
12515
  let existing = null;
@@ -12643,6 +12530,10 @@ async function driveInterruptedChild(input, child) {
12643
12530
  persist();
12644
12531
  throw new BatchCommitAbortError(`commit lookup failed: ${message}`);
12645
12532
  }
12533
+ if (!existing) {
12534
+ record = reconcileLineage(input.root, record).record;
12535
+ head = expectedBatchHead(record);
12536
+ }
12646
12537
  const planChild = input.children.find((c) => c.task_id === child.task_id);
12647
12538
  const intentPath = planChild?.intent_path ?? undefined;
12648
12539
  const doCommit = async () => {
@@ -12938,7 +12829,6 @@ async function mintCapability(registry, input) {
12938
12829
  diff_hash: input.diff_hash,
12939
12830
  actor_id: input.actor_id,
12940
12831
  confirmation_ref: input.confirmation_ref,
12941
- expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
12942
12832
  findings_digest: input.action_kind === "request_rework" ? findingsDigestV2(input.findings) : null
12943
12833
  };
12944
12834
  return registry.issue(binding);
@@ -13075,7 +12965,6 @@ class ClaudeRuntime {
13075
12965
  preparation_digest: preparation.digest,
13076
12966
  actor_id: LITERAL_USER_ACTOR_ID,
13077
12967
  confirmation_ref: gate.confirmation_ref,
13078
- expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
13079
12968
  nonce
13080
12969
  };
13081
12970
  const capability = this.enrollmentRegistry.issue(binding);
@@ -13099,11 +12988,41 @@ class ClaudeRuntime {
13099
12988
  throw error;
13100
12989
  }
13101
12990
  }
13102
- async advance(taskId, signal) {
13103
- 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);
13104
12996
  }
13105
- async submitReview(taskId, verdictInput) {
13106
- 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
+ }
13107
13026
  }
13108
13027
  async withTerminalTracker(taskId, result) {
13109
13028
  if (result === null || typeof result !== "object")
@@ -13405,15 +13324,16 @@ class ClaudeRuntime {
13405
13324
  throw error;
13406
13325
  }
13407
13326
  }
13408
- async startUnattendedBatch(initiativeSlug, meta) {
13327
+ async startUnattendedBatch(initiativeSlug, meta, options = {}) {
13409
13328
  throwIfCancelled(meta.signal);
13329
+ const reuseOnly = options.reuseOnly === true;
13410
13330
  const probe = probeHost(this.env, process.platform, this.hostVersion);
13411
13331
  if (!probe.ok)
13412
13332
  throw new NativeAuthorityError("unsupported_host", probe.reason);
13413
13333
  const interactive = meta.interactive ?? this.interactive;
13414
- if (!interactive)
13334
+ if (!reuseOnly && !interactive)
13415
13335
  throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
13416
- if (!this.requestConfirmation)
13336
+ if (!reuseOnly && !this.requestConfirmation)
13417
13337
  throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
13418
13338
  let now = new Date().toISOString();
13419
13339
  const preflight = await projectBatchPreflight({
@@ -13443,7 +13363,15 @@ class ClaudeRuntime {
13443
13363
  readInitiative: this.readInitiative ?? observeGithubInitiative,
13444
13364
  nonce: enrollmentNonce(),
13445
13365
  gate: async (facts) => {
13446
- 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
+ };
13447
13375
  let confirmationResult;
13448
13376
  try {
13449
13377
  confirmationResult = await this.requestConfirmation({
@@ -13465,17 +13393,14 @@ class ClaudeRuntime {
13465
13393
  reason: child.reason
13466
13394
  })),
13467
13395
  budget: facts.budget,
13468
- expires_at: facts.expires_at,
13469
13396
  ...facts.reuse_blockers.length > 0 ? {
13470
13397
  re_confirmation_required: facts.reuse_blockers,
13471
13398
  recovery: "confirm to issue a fresh authorization bound to the current plan and HEAD"
13472
13399
  } : {}
13473
13400
  },
13474
- signal: deadline.signal
13401
+ signal: meta.signal
13475
13402
  });
13476
13403
  } catch (err) {
13477
- if (deadline.timedOut())
13478
- return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
13479
13404
  if (meta.signal?.aborted)
13480
13405
  return { kind: "host_rejection", value: batchReason("cancelled_before_execution") };
13481
13406
  if (err instanceof NativeAuthorityError) {
@@ -13496,11 +13421,7 @@ class ClaudeRuntime {
13496
13421
  kind: "host_rejection",
13497
13422
  value: batchReason("confirmation_failed", err instanceof Error ? err.message : String(err))
13498
13423
  };
13499
- } finally {
13500
- deadline.clear();
13501
13424
  }
13502
- if (deadline.timedOut())
13503
- return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
13504
13425
  if (confirmationResult.decision === "cancel" && meta.signal?.aborted)
13505
13426
  return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
13506
13427
  if (meta.signal?.aborted)
@@ -13526,12 +13447,10 @@ class ClaudeRuntime {
13526
13447
  return authorization.value;
13527
13448
  if (authorization.outcome === "rejected")
13528
13449
  return authorization.rejection;
13529
- const { binding, batch_id: batchId, expires_at: expiresAt } = authorization;
13450
+ const { binding, batch_id: batchId } = authorization;
13530
13451
  if (meta.signal?.aborted)
13531
13452
  return batchReason("cancelled_before_execution");
13532
13453
  now = new Date().toISOString();
13533
- if (Date.parse(expiresAt) <= Date.parse(now))
13534
- return batchReason("confirmation_failed", "confirmed batch deadline expired before authorization issuance");
13535
13454
  const capability = this.batchRegistry.issue(binding, recoveryChildren, now);
13536
13455
  const basePort = this.createBatchKernelPort(this.batchRegistry, capability, binding);
13537
13456
  const kernelPort = {
@@ -13553,7 +13472,6 @@ class ClaudeRuntime {
13553
13472
  plan_digest: planDigest,
13554
13473
  base_head: binding.base_head,
13555
13474
  confirmation_time: now,
13556
- authorization_expires_at: expiresAt,
13557
13475
  budget,
13558
13476
  now,
13559
13477
  kernel: kernelPort,
@@ -13764,11 +13682,11 @@ function createMcpRuntime(options = {}) {
13764
13682
  if (name === "enroll")
13765
13683
  return runtime.enroll(taskId, toolMeta);
13766
13684
  if (name === "advance_assurance")
13767
- return runtime.advance(taskId, toolMeta.signal);
13685
+ return runtime.advance(taskId, toolMeta.signal, toolMeta);
13768
13686
  if (name === "submit_review") {
13769
13687
  if (!Object.hasOwn(args, "verdict"))
13770
13688
  throw new Error("verdict is required");
13771
- return runtime.submitReview(taskId, args.verdict);
13689
+ return runtime.submitReview(taskId, args.verdict, toolMeta);
13772
13690
  }
13773
13691
  if (name === "revise_intent") {
13774
13692
  if (!Object.hasOwn(args, "next_intent"))
@@ -13964,8 +13882,7 @@ ${b.children.map((c) => ` - ${c.task_id} (${c.slice_id}) [risk: ${c.risk ?? "un
13964
13882
  b?.excluded && b.excluded.length > 0 ? `Excluded children (${b.excluded.length}):
13965
13883
  ${b.excluded.map((e) => ` - ${e.task_id} (${e.slice_id}): ${e.reason}`).join(`
13966
13884
  `)}` : null,
13967
- b?.budget ? `Budget: max_children=${b.budget.max_children}, deadline_at=${b.budget.deadline_at}, qa_failure_limit=${b.budget.qa_failure_limit}` : null,
13968
- 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
13969
13886
  ].filter(Boolean);
13970
13887
  return {
13971
13888
  mode: "form",