immune-brain 4.4.1 → 4.5.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 (25) hide show
  1. package/package.json +1 -1
  2. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  3. package/plugins/immune-brain/dist/BASELINE.md +18 -1
  4. package/plugins/immune-brain/dist/claude/mcp-server.mjs +675 -292
  5. package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -0
  6. package/plugins/immune-brain/dist/imm-brainstorm.md +21 -0
  7. package/plugins/immune-brain/dist/imm-loop.md +41 -2
  8. package/plugins/immune-brain/dist/imm-planner.md +65 -1
  9. package/plugins/immune-brain/dist/role-prompts/code-review.md +19 -0
  10. package/plugins/immune-brain/dist/role-prompts/qa.md +13 -1
  11. package/plugins/immune-brain/runtime/github_issue_tracker.ts +321 -60
  12. package/plugins/immune-brain/runtime/kernel/storage.ts +8 -3
  13. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  14. package/plugins/immune-brain/runtime/prompts/code-review.md +19 -0
  15. package/plugins/immune-brain/runtime/prompts/qa.md +13 -1
  16. package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +35 -18
  17. package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +194 -0
  18. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +42 -5
  19. package/plugins/immune-brain/runtime/unattended/batch_state.ts +23 -4
  20. package/plugins/immune-brain/runtime/v4_runtime.ts +54 -0
  21. package/plugins/immune-brain/runtime/workspace_scope.ts +31 -4
  22. package/plugins/immune-brain/skills/BASELINE.md +18 -1
  23. package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +5 -0
  24. package/plugins/immune-brain/skills/imm-loop/SKILL.md +4 -2
  25. package/plugins/immune-brain/skills/imm-planner/SKILL.md +10 -1
@@ -39,7 +39,7 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
39
39
  }
40
40
 
41
41
  // plugins/immune-brain/runtime/plugin_version.ts
42
- var PLUGIN_VERSION = "4.4.1";
42
+ var PLUGIN_VERSION = "4.5.0";
43
43
 
44
44
  // plugins/immune-brain/runtime/claude/interaction.ts
45
45
  import { createHash, randomUUID } from "node:crypto";
@@ -2323,11 +2323,13 @@ import {
2323
2323
  } from "node:fs";
2324
2324
  import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
2325
2325
  function git(root, args) {
2326
- const result = spawnSync("git", ["-C", root, ...args], {
2326
+ const diff = args[0] === "diff";
2327
+ const command = diff ? ["diff", "--exit-code", ...args.slice(1)] : args;
2328
+ const result = spawnSync("git", ["--no-optional-locks", "-c", "diff.autoRefreshIndex=false", "-C", root, ...command], {
2327
2329
  encoding: "utf8",
2328
2330
  stdio: ["ignore", "pipe", "pipe"]
2329
2331
  });
2330
- return result.status === 0 ? result.stdout : null;
2332
+ return result.status === 0 || diff && result.status === 1 ? result.stdout : null;
2331
2333
  }
2332
2334
  function splitNull(value) {
2333
2335
  return value.split("\x00").filter((entry) => entry.length > 0);
@@ -2446,12 +2448,14 @@ var portablePathCollator = new Intl.Collator("und", {
2446
2448
  });
2447
2449
  var gitTaskSnapshotTestHook;
2448
2450
  function gitBytes(root, args) {
2449
- const result = spawnSync("git", ["-C", root, ...args], {
2451
+ const diff = args[0] === "diff";
2452
+ const command = diff ? ["diff", "--exit-code", ...args.slice(1)] : args;
2453
+ const result = spawnSync("git", ["--no-optional-locks", "-c", "diff.autoRefreshIndex=false", "-C", root, ...command], {
2450
2454
  encoding: null,
2451
2455
  stdio: ["ignore", "pipe", "pipe"],
2452
2456
  maxBuffer: 8 * 1024 * 1024
2453
2457
  });
2454
- if (result.status !== 0 || !Buffer.isBuffer(result.stdout)) {
2458
+ if (result.status !== 0 && !(diff && result.status === 1) || !Buffer.isBuffer(result.stdout)) {
2455
2459
  throw new Error(`Git task snapshot command failed: git ${args.join(" ")}`);
2456
2460
  }
2457
2461
  return result.stdout;
@@ -2741,6 +2745,38 @@ function taskRevisionIdentity(projectRoot, scopeHint, baseHead, taskId) {
2741
2745
  changed_paths: Object.keys(snapshot.changed_paths).sort(comparePaths)
2742
2746
  };
2743
2747
  }
2748
+ function taskCommitRevisionIdentity(projectRoot, scopeHint, baseHead, commit) {
2749
+ const requested = resolve2(projectRoot);
2750
+ if (lstatSync2(requested).isSymbolicLink())
2751
+ throw new Error("task commit root must be a real directory");
2752
+ const root = realpathSync3(requested), scope = assertCanonicalTaskScope(scopeHint);
2753
+ if (!GIT_OBJECT_ID.test(baseHead) || !GIT_OBJECT_ID.test(commit))
2754
+ throw new Error("invalid task commit identity");
2755
+ if (gitRequired(root, ["cat-file", "-t", baseHead], "unreadable task base") !== "commit" || gitRequired(root, ["cat-file", "-t", commit], "unreadable task commit") !== "commit" || git(root, ["merge-base", "--is-ancestor", baseHead, commit]) === null)
2756
+ throw new Error("invalid task commit ancestry");
2757
+ const paths = decodeNullPaths(gitBytes(root, ["diff", "--no-renames", "--name-only", "-z", baseHead, commit, "--"]), "task commit paths").filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope)).sort(comparePaths);
2758
+ assertNoCaseFoldCollisions(paths, "task commit paths");
2759
+ const changed_paths = {};
2760
+ for (const path of paths) {
2761
+ const current = headEntry(root, commit, path), base = headEntry(root, baseHead, path);
2762
+ changed_paths[path] = {
2763
+ status: !base ? "added" : !current ? "deleted" : "modified",
2764
+ mode: current?.mode ?? null,
2765
+ oid: current?.oid ?? null,
2766
+ base_mode: base?.mode ?? null,
2767
+ base_oid: base?.oid ?? null
2768
+ };
2769
+ }
2770
+ const snapshot = {
2771
+ kind: "git-task-revision-v1",
2772
+ repository_root: root,
2773
+ base_head: baseHead,
2774
+ base_tree: gitRequired(root, ["rev-parse", `${baseHead}^{tree}`], "unreadable task base tree"),
2775
+ scope,
2776
+ changed_paths
2777
+ };
2778
+ return { diff_hash: hashTaskSnapshot(snapshot), changed_paths: paths };
2779
+ }
2744
2780
  function isGitWorkspaceSnapshot(value) {
2745
2781
  return typeof value === "object" && value !== null && !Array.isArray(value) && value.kind === "git-workspace-v1" && typeof value.repository_root === "string" && typeof value.head === "string" && typeof value.dirty_files === "object" && value.dirty_files !== null;
2746
2782
  }
@@ -5728,6 +5764,9 @@ function ensureSecureDirectory(root, relativePath) {
5728
5764
  return target.path;
5729
5765
  }
5730
5766
  function readSecureProjectFile(root, relativePath) {
5767
+ return readSecureProjectBytes(root, relativePath).toString("utf8");
5768
+ }
5769
+ function readSecureProjectBytes(root, relativePath) {
5731
5770
  const candidate = safeCandidate(root, relativePath);
5732
5771
  assertNoSymlinkSegments(candidate.root, candidate.path);
5733
5772
  const before = pathStatOrNull(candidate.path);
@@ -5743,7 +5782,7 @@ function readSecureProjectFile(root, relativePath) {
5743
5782
  const opened = fstatSync4(fd);
5744
5783
  if (opened.dev !== before.dev || opened.ino !== before.ino)
5745
5784
  throw new KernelStoreSecurityError(`source identity changed: ${relativePath}`);
5746
- const content = readFileSync8(fd, "utf8");
5785
+ const content = readFileSync8(fd);
5747
5786
  const after = lstatSync7(candidate.path);
5748
5787
  if (after.dev !== opened.dev || after.ino !== opened.ino)
5749
5788
  throw new KernelStoreSecurityError(`source identity changed: ${relativePath}`);
@@ -8846,6 +8885,17 @@ function createGhTransport(binary = "gh") {
8846
8885
  return new Promise((complete) => {
8847
8886
  let stdout = Buffer.alloc(0);
8848
8887
  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)) {
8890
+ complete({
8891
+ exit_code: 1,
8892
+ stdout: "",
8893
+ stderr: options.budget.signal?.aborted ? "operation cancelled by the caller" : "publication deadline exceeded before the next remote call",
8894
+ timed_out: true,
8895
+ output_exceeded: false
8896
+ });
8897
+ return;
8898
+ }
8849
8899
  let timedOut = false;
8850
8900
  let outputExceeded = false;
8851
8901
  let timer;
@@ -8903,7 +8953,7 @@ function createGhTransport(binary = "gh") {
8903
8953
  timer = setTimeout(() => {
8904
8954
  timedOut = true;
8905
8955
  child.kill("SIGKILL");
8906
- }, GH_TIMEOUT_MS);
8956
+ }, remaining);
8907
8957
  child.once("close", (code) => {
8908
8958
  finish(code ?? 1);
8909
8959
  });
@@ -9007,6 +9057,33 @@ function initiativeLookup(issues, repositoryId, initiativeId) {
9007
9057
  const initiative = marker("initiative-id", initiativeId);
9008
9058
  return findIssue(issues, [initiative, KIND_INITIATIVE_MARKER], [marker("repo-id", repositoryId), initiative]);
9009
9059
  }
9060
+ function publishedIntentHashMarkers(body) {
9061
+ return [...body.matchAll(/<!-- immune-brain:intent-hash=([A-Za-z0-9][A-Za-z0-9._:-]{0,127}) -->/g)].map((match) => match[1]);
9062
+ }
9063
+ function publishedIntentIdentity(body) {
9064
+ const hashes = publishedIntentHashMarkers(body);
9065
+ if (hashes.length === 0)
9066
+ return { kind: "absent" };
9067
+ if (hashes.length === 1)
9068
+ return { kind: "bound", hash: hashes[0] };
9069
+ return { kind: "ambiguous", hashes };
9070
+ }
9071
+ function publishedIntentMismatch(body, taskId, expected) {
9072
+ const identity = publishedIntentIdentity(body);
9073
+ if (identity.kind === "ambiguous")
9074
+ return `published Task ${taskId} carries ${identity.hashes.length} conflicting intent-hash markers; its published Intent identity is ambiguous`;
9075
+ if (identity.kind === "absent" || expected === undefined)
9076
+ return null;
9077
+ if (identity.hash !== expected)
9078
+ return `published Task ${taskId} was published from a different TaskIntent revision; the published identity no longer matches the Intent being published`;
9079
+ return null;
9080
+ }
9081
+ function sameTrackedBody(current, expected) {
9082
+ return stripPublishedIntentHash(current) === stripPublishedIntentHash(expected);
9083
+ }
9084
+ function stripPublishedIntentHash(body) {
9085
+ return body.replace(/<!-- immune-brain:intent-hash=[A-Za-z0-9][A-Za-z0-9._:-]{0,127} -->\n?/g, "");
9086
+ }
9010
9087
  function ownershipMarkerValue(body, name) {
9011
9088
  const values = [...body.matchAll(new RegExp(`<!-- immune-brain:${name}=([A-Za-z0-9][A-Za-z0-9._-]{0,127}) -->`, "g"))];
9012
9089
  return values.length === 1 ? values[0][1] : null;
@@ -9307,7 +9384,8 @@ function childBody(repository, operation, parent) {
9307
9384
  marker("repo-id", repository.id),
9308
9385
  marker("initiative-id", operation.initiative_id),
9309
9386
  marker("slice-id", operation.slice_id),
9310
- marker("task-id", operation.task_id)
9387
+ marker("task-id", operation.task_id),
9388
+ ...operation.intent_hash ? [marker("intent-hash", operation.intent_hash)] : []
9311
9389
  ].join(`
9312
9390
  `)}
9313
9391
 
@@ -9476,8 +9554,11 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
9476
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);
9477
9555
  return updatePendingChild(root, gh, source, operation, child, undefined, blockers, approvedFinal, amendmentContext);
9478
9556
  }
9479
- if (found.issue.body !== body || found.issue.title !== title)
9557
+ if (!sameTrackedBody(found.issue.body, body) || found.issue.title !== title)
9480
9558
  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
+ const intentMismatch = publishedIntentMismatch(found.issue.body, operation.task_id, operation.intent_hash);
9560
+ if (intentMismatch)
9561
+ return result(operation.op, "ambiguous_remote_state", intentMismatch, found.issue);
9481
9562
  const observedLabelArgs = labelMutationArgs(child.labels, desiredTaskLabels(operation));
9482
9563
  if (observedLabelArgs.length) {
9483
9564
  const edited = await gh.run([
@@ -9513,8 +9594,11 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
9513
9594
  if ("contract" in finalSource)
9514
9595
  return finalSource;
9515
9596
  const finalChild = ownedTaskLookup(finalSource.issues, finalSource.repository.id, operation.task_id, operation.initiative_id, operation.slice_id);
9516
- if (finalChild.kind !== "found" || finalChild.issue.id !== child.id || finalChild.issue.title !== title || finalChild.issue.body !== body)
9597
+ if (finalChild.kind !== "found" || finalChild.issue.id !== child.id || finalChild.issue.title !== title || !sameTrackedBody(finalChild.issue.body, body))
9517
9598
  return result(operation.op, "ambiguous_remote_state", "Task Issue changed identity, title, or body during dependency publication", child);
9599
+ const finalIdentityMismatch = publishedIntentMismatch(finalChild.issue.body, operation.task_id, operation.intent_hash);
9600
+ if (finalIdentityMismatch)
9601
+ return result(operation.op, "ambiguous_remote_state", finalIdentityMismatch, child);
9518
9602
  const finalChildOwnership = await confirmTerminalOwnership(root, gh, operation.op, finalSource, finalChild.issue);
9519
9603
  if (!("owned" in finalChildOwnership))
9520
9604
  return finalChildOwnership;
@@ -9966,7 +10050,7 @@ async function revalidatePendingChildBeforeWrite(root, gh, childNumber, childTas
9966
10050
  import { existsSync as existsSync7, readdirSync as readdirSync6, readFileSync as readFileSync13 } from "node:fs";
9967
10051
  import { randomUUID as randomUUID7 } from "node:crypto";
9968
10052
  import { join as join12 } from "node:path";
9969
- import { spawnSync as spawnSync5 } from "node:child_process";
10053
+ import { spawnSync as spawnSync6 } from "node:child_process";
9970
10054
 
9971
10055
  // plugins/immune-brain/runtime/kernel/batch_authority.ts
9972
10056
  import { createHash as createHash18 } from "node:crypto";
@@ -10215,211 +10299,8 @@ function deriveChildEnrollment(root, registry, input) {
10215
10299
  };
10216
10300
  }
10217
10301
 
10218
- // plugins/immune-brain/runtime/unattended/batch_plan.ts
10219
- import { createHash as createHash19 } from "node:crypto";
10220
- var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10221
- var DEFAULT_DEADLINE_MS = 8 * 60 * 60 * 1000;
10222
- var DEFAULT_QA_FAILURE_LIMIT = 2;
10223
- function specBindingReason(inspection) {
10224
- if (inspection.missing.length > 0)
10225
- return `spec_binding_incomplete: ${inspection.missing.join(", ")}`;
10226
- return inspection.code === "binding_ambiguous" ? "spec_binding_ambiguous" : "spec_binding_missing";
10227
- }
10228
- function compareIds(left, right) {
10229
- return left < right ? -1 : left > right ? 1 : 0;
10230
- }
10231
- function positiveSafeInteger(value, name) {
10232
- if (!Number.isSafeInteger(value) || value <= 0)
10233
- throw new Error(`${name} must be a positive safe integer`);
10234
- return value;
10235
- }
10236
- var ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
10237
- function timestamp(value, name) {
10238
- if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN.test(value))
10239
- throw new Error(`${name} must be an ISO timestamp`);
10240
- const milliseconds = Date.parse(value);
10241
- if (!Number.isFinite(milliseconds))
10242
- throw new Error(`${name} must be an ISO timestamp`);
10243
- return { milliseconds, iso: new Date(milliseconds).toISOString() };
10244
- }
10245
- function normalizeObservation(value, initiativeSlug) {
10246
- if (!value || value.contract !== "immune_brain/github_initiative_observation/v1")
10247
- throw new Error("tracker returned an invalid Initiative observation contract");
10248
- if (value.initiative_id !== initiativeSlug)
10249
- throw new Error("tracker returned an observation for another Initiative");
10250
- if (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0)
10251
- throw new Error("tracker returned an invalid Initiative Issue number");
10252
- if (!Array.isArray(value.tasks))
10253
- throw new Error("tracker returned an invalid Initiative Task list");
10254
- const taskIds = new Set;
10255
- const sliceIds = new Set;
10256
- const tasks = value.tasks.map((task, index) => {
10257
- if (!task || typeof task !== "object")
10258
- throw new Error(`tracker Task ${index} is invalid`);
10259
- if (!ID_PATTERN2.test(task.task_id))
10260
- throw new Error(`tracker Task ${index} has an invalid task_id`);
10261
- if (!ID_PATTERN2.test(task.slice_id))
10262
- throw new Error(`tracker Task ${index} has an invalid slice_id`);
10263
- if (!Number.isSafeInteger(task.issue_number) || task.issue_number <= 0)
10264
- throw new Error(`tracker Task ${task.task_id} has an invalid Issue number`);
10265
- if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN2.test(id)))
10266
- throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
10267
- if (taskIds.has(task.task_id))
10268
- throw new Error(`tracker returned duplicate Task ${task.task_id}`);
10269
- if (sliceIds.has(task.slice_id))
10270
- throw new Error(`tracker returned duplicate Slice ${task.slice_id}`);
10271
- taskIds.add(task.task_id);
10272
- sliceIds.add(task.slice_id);
10273
- const blockedBy = [...task.blocked_by].sort();
10274
- if (new Set(blockedBy).size !== blockedBy.length || blockedBy.includes(task.task_id))
10275
- throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
10276
- return { ...task, blocked_by: blockedBy };
10277
- });
10278
- for (const task of tasks) {
10279
- const unknown = task.blocked_by.find((id) => !taskIds.has(id));
10280
- if (unknown)
10281
- throw new Error(`tracker Task ${task.task_id} depends on unknown Task ${unknown}`);
10282
- }
10283
- return { ...value, tasks: tasks.sort((left, right) => compareIds(left.task_id, right.task_id)) };
10284
- }
10285
- function dependencyOrder(observation) {
10286
- const remaining = new Map(observation.tasks.map((task) => [task.task_id, task]));
10287
- const done = new Set;
10288
- const order = [];
10289
- while (remaining.size) {
10290
- const ready = [...remaining.values()].filter((task) => task.blocked_by.every((id) => done.has(id))).sort((left, right) => compareIds(left.task_id, right.task_id));
10291
- if (!ready.length)
10292
- throw new Error("Initiative Task dependencies must form an acyclic graph");
10293
- for (const task of ready) {
10294
- remaining.delete(task.task_id);
10295
- done.add(task.task_id);
10296
- order.push(task);
10297
- }
10298
- }
10299
- return order;
10300
- }
10301
- function dependencyClosures(order) {
10302
- const closures = new Map;
10303
- for (const task of order) {
10304
- const closure = new Set;
10305
- for (const dependency of task.blocked_by) {
10306
- closure.add(dependency);
10307
- for (const transitive of closures.get(dependency) ?? [])
10308
- closure.add(transitive);
10309
- }
10310
- closures.set(task.task_id, [...closure].sort());
10311
- }
10312
- return closures;
10313
- }
10314
- function budget(input, enrollableCount, confirmationTime) {
10315
- const maxChildren = input.budget?.max_children === undefined ? positiveSafeInteger(enrollableCount, "budget.max_children") : positiveSafeInteger(input.budget.max_children, "budget.max_children");
10316
- if (maxChildren > enrollableCount)
10317
- throw new Error("budget.max_children exceeds the enrollable Child count");
10318
- const qaFailureLimit = input.budget?.qa_failure_limit === undefined ? DEFAULT_QA_FAILURE_LIMIT : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
10319
- const deadline = input.budget?.deadline_at === undefined ? new Date(confirmationTime.milliseconds + DEFAULT_DEADLINE_MS).toISOString() : timestamp(input.budget.deadline_at, "budget.deadline_at").iso;
10320
- if (Date.parse(deadline) <= confirmationTime.milliseconds)
10321
- throw new Error("budget.deadline_at must be later than confirmation_time");
10322
- return { max_children: maxChildren, deadline_at: deadline, qa_failure_limit: qaFailureLimit };
10323
- }
10324
- async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeGithubInitiative) {
10325
- if (!ID_PATTERN2.test(initiativeSlug))
10326
- throw new Error("initiative_slug is invalid");
10327
- if (!input || typeof input !== "object")
10328
- throw new Error("batch plan input is required");
10329
- const confirmationTime = timestamp(input.confirmation_time, "confirmation_time");
10330
- const trackerObservation = normalizeObservation(await readInitiative(root, initiativeSlug), initiativeSlug);
10331
- const order = dependencyOrder(trackerObservation);
10332
- const closures = dependencyClosures(order);
10333
- const children = [];
10334
- for (const task of order) {
10335
- const blockedBy = closures.get(task.task_id);
10336
- const child = { task_id: task.task_id, slice_id: task.slice_id, blocked_by: blockedBy };
10337
- const tombstone = readTaskTombstone(root, task.task_id);
10338
- const record = readTaskRecordRaw(root, task.task_id).record;
10339
- if (tombstone && record)
10340
- throw new Error(`Task ${task.task_id} has conflicting settled and active state`);
10341
- if (tombstone) {
10342
- children.push({ ...child, status: "already_settled", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
10343
- continue;
10344
- }
10345
- if (record) {
10346
- children.push({ ...child, status: "already_owned", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
10347
- continue;
10348
- }
10349
- const intentPath = `docs/plans/${task.task_id}.intent.json`;
10350
- try {
10351
- const read = observeTaskIntent(root, task.task_id, intentPath);
10352
- if (read.intent.risk === "critical") {
10353
- children.push({
10354
- ...child,
10355
- status: "needs_human",
10356
- reason: "critical",
10357
- intent_path: read.intent_ref.path,
10358
- intent_revision: read.intent_ref.revision,
10359
- intent_content_hash: read.content_hash
10360
- });
10361
- continue;
10362
- }
10363
- const binding = inspectSpecBinding(read.intent);
10364
- if (!binding.ok) {
10365
- children.push({
10366
- ...child,
10367
- status: "needs_human",
10368
- reason: specBindingReason(binding),
10369
- intent_path: read.intent_ref.path,
10370
- intent_revision: read.intent_ref.revision,
10371
- intent_content_hash: read.content_hash
10372
- });
10373
- continue;
10374
- }
10375
- children.push({
10376
- ...child,
10377
- status: "enrollable",
10378
- reason: null,
10379
- intent_path: read.intent_ref.path,
10380
- intent_revision: read.intent_ref.revision,
10381
- intent_content_hash: read.content_hash
10382
- });
10383
- } catch (error) {
10384
- if (!(error instanceof TaskIntentObservationError))
10385
- throw error;
10386
- children.push({ ...child, status: "needs_human", reason: "invalid_intent", intent_path: null, intent_revision: null, intent_content_hash: null });
10387
- }
10388
- }
10389
- const directDependencies = new Map(order.map((task) => [task.task_id, task.blocked_by]));
10390
- const childById = new Map(children.map((child) => [child.task_id, child]));
10391
- for (const child of children) {
10392
- if (child.status !== "enrollable")
10393
- continue;
10394
- if (directDependencies.get(child.task_id).some((id) => {
10395
- const status = childById.get(id).status;
10396
- return status === "already_owned" || status === "needs_human" || status === "blocked";
10397
- })) {
10398
- child.status = "blocked";
10399
- child.reason = "dependency_unavailable";
10400
- }
10401
- }
10402
- const enrollable = children.filter((child) => child.status === "enrollable").map((child) => ({
10403
- task_id: child.task_id,
10404
- intent_path: child.intent_path,
10405
- intent_revision: child.intent_revision,
10406
- intent_content_hash: child.intent_content_hash,
10407
- blocked_by: child.blocked_by.filter((id) => childById.get(id)?.status === "enrollable")
10408
- }));
10409
- if (!enrollable.length)
10410
- throw new Error("batch plan has no enrollable children; nothing to confirm");
10411
- const planDigest = `sha256:${createHash19("sha256").update(stableStringify(enrollable)).digest("hex")}`;
10412
- return {
10413
- contract: "assurance_kernel/batch_plan/v1",
10414
- initiative_slug: initiativeSlug,
10415
- confirmation_time: confirmationTime.iso,
10416
- tracker_observation: trackerObservation,
10417
- children,
10418
- enrollable,
10419
- plan_digest: planDigest,
10420
- budget: budget(input, enrollable.length, confirmationTime)
10421
- };
10422
- }
10302
+ // plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts
10303
+ import { spawnSync as spawnSync5 } from "node:child_process";
10423
10304
 
10424
10305
  // plugins/immune-brain/runtime/unattended/batch_state.ts
10425
10306
  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";
@@ -10443,9 +10324,9 @@ var BATCH_RUN_STATES = new Set([
10443
10324
  "failed",
10444
10325
  "rejected"
10445
10326
  ]);
10446
- var ISO_TIMESTAMP_PATTERN2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
10327
+ var ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
10447
10328
  function isCanonicalTimestamp(value) {
10448
- if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN2.test(value))
10329
+ if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN.test(value))
10449
10330
  return false;
10450
10331
  const milliseconds = Date.parse(value);
10451
10332
  return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === value;
@@ -10550,13 +10431,17 @@ function prepareBatchRunState(input) {
10550
10431
  };
10551
10432
  return prepared;
10552
10433
  }
10434
+ function parseBatchRunState(raw, batchId) {
10435
+ validateBatchId(batchId);
10436
+ const parsed = JSON.parse(raw);
10437
+ validateRecordShape(parsed, batchId);
10438
+ return parsed;
10439
+ }
10553
10440
  function readBatchRunState(root, batchId) {
10554
10441
  const path = statePath(batchId);
10555
10442
  if (!existsSync6(join11(root, path)))
10556
10443
  return null;
10557
- const parsed = JSON.parse(readSecureProjectFile(root, path));
10558
- validateRecordShape(parsed, batchId);
10559
- return parsed;
10444
+ return parseBatchRunState(readSecureProjectFile(root, path), batchId);
10560
10445
  }
10561
10446
  function ensureSecureDirectory2(root, relative) {
10562
10447
  const target = join11(root, relative);
@@ -10596,6 +10481,18 @@ function writeFileAtomically(root, relative, bytes) {
10596
10481
  }
10597
10482
  }
10598
10483
  }
10484
+ function replaceBatchRunState(root, expected, next, validate) {
10485
+ const path = statePath(next.batch_id);
10486
+ validateRecordShape(next, next.batch_id);
10487
+ return withKernelStoreLock(root, () => {
10488
+ if (!readSecureProjectBytes(root, path).equals(expected))
10489
+ throw new Error("batch state CAS mismatch");
10490
+ validate();
10491
+ const stored = { ...next, updated_at: new Date().toISOString() };
10492
+ writeFileAtomically(root, path, canonicalBytes(stored));
10493
+ return stored;
10494
+ });
10495
+ }
10599
10496
  function writeBatchRunState(root, record) {
10600
10497
  const path = statePath(record.batch_id);
10601
10498
  validateRecordShape(record, record.batch_id);
@@ -10643,17 +10540,439 @@ function isTerminalBatchState(state) {
10643
10540
  return state === "completed" || state === "budget_stopped" || state === "failed" || state === "rejected";
10644
10541
  }
10645
10542
 
10646
- // plugins/immune-brain/runtime/unattended/batch_preflight.ts
10647
- var INITIATIVE_SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10648
- function batchRejection(key, detail = "") {
10649
- const resolved = batchReason(key, detail);
10650
- return {
10651
- state: resolved.state === "blocked" ? "blocked" : "rejected",
10652
- reason: resolved.reason,
10653
- recovery_action: resolved.recovery_action
10654
- };
10543
+ // plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts
10544
+ var oid = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
10545
+ var id = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10546
+ var batchAuthor = process.env.GIT_AUTHOR_NAME || "Immune-Brain Batch";
10547
+ var decode = (bytes) => new TextDecoder("utf-8", { fatal: true }).decode(bytes);
10548
+ function refuse() {
10549
+ throw new Error("plan_digest mismatch: batch plan reconfirmation is not eligible");
10655
10550
  }
10656
- function reject(key, detail = "") {
10551
+ function matchesLocalProof(localJson, exported) {
10552
+ return localJson !== null && JSON.stringify(Object.entries(JSON.parse(localJson)).sort()) === JSON.stringify(Object.entries(exported).sort());
10553
+ }
10554
+ function git4(root, args) {
10555
+ const r = spawnSync5("git", ["-C", root, ...args], { encoding: "buffer", maxBuffer: 262144, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
10556
+ if (r.status !== 0)
10557
+ refuse();
10558
+ return decode(r.stdout);
10559
+ }
10560
+ function ownUnpersistedBatchHead(root, record, head) {
10561
+ try {
10562
+ if (!oid.test(head) || !oid.test(record.base_head) || !id.test(record.batch_id) || record.commits.length || record.children[0]?.state !== "settled" || record.children.some((c) => c.commit !== null))
10563
+ return false;
10564
+ const task = record.children[0].task_id;
10565
+ if (!id.test(task))
10566
+ return false;
10567
+ const evidence = JSON.parse(decode(readSecureProjectBytes(root, `.imm/state/batches/commits/${record.batch_id}-${task}.json`)));
10568
+ if (evidence.contract !== "assurance_kernel/batch_commit_evidence/v1" || evidence.batch_id !== record.batch_id || evidence.task_id !== task || evidence.commit !== head || evidence.parent_head !== record.base_head)
10569
+ return false;
10570
+ if (git4(root, ["symbolic-ref", "--short", "HEAD"]).trim() !== record.branch)
10571
+ return false;
10572
+ const parents = git4(root, ["rev-list", "--parents", "-n", "1", head]).trim().split(/\s+/).slice(1);
10573
+ if (parents.length !== 1 || parents[0] !== record.base_head)
10574
+ return false;
10575
+ const metadata = git4(root, ["show", "-s", "--format=%an%x00%s%x00%(trailers:key=Immune-Brain-Batch,valueonly)", head]).split("\x00");
10576
+ if (metadata[0] !== batchAuthor || !metadata[1]?.startsWith(`imm(${task}):`) || metadata[2]?.trim() !== record.batch_id)
10577
+ return false;
10578
+ const run = localRunId(root, task);
10579
+ if (!run || !id.test(run))
10580
+ return false;
10581
+ const pair = readAuditTaskPair(root, task, run);
10582
+ if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4" || pair.record.lifecycle !== "done" || pair.proof.terminal_lifecycle !== "done")
10583
+ return false;
10584
+ const local = withKernelRead(root, (db) => readRunRowByTask(db, task));
10585
+ if (!local || local.run_id !== run || local.state !== "done" || !matchesLocalProof(local.terminal_proof_json, pair.proof))
10586
+ return false;
10587
+ for (const file of ["task-record.json", "terminal-proof.json"]) {
10588
+ const path = `.imm/audit/${task}/${run}/${file}`;
10589
+ if (!readSecureProjectBytes(root, path).equals(Buffer.from(git4(root, ["show", `${head}:${path}`]))))
10590
+ return false;
10591
+ }
10592
+ const paths = git4(root, ["diff-tree", "--no-commit-id", "--name-only", "--no-renames", "-z", "-r", head]).split("\x00").filter(Boolean);
10593
+ if (!paths.length || paths.some((path) => !path.startsWith(`.imm/audit/${task}/${run}/`) && !pair.record.intent_snapshot.scope_hint.some((s) => pathMatchesScope(path, s))))
10594
+ return false;
10595
+ const identity = taskCommitRevisionIdentity(root, pair.record.intent_snapshot.scope_hint, pair.record.git_base_head, head);
10596
+ if (!projectTask(pair.record.intent_snapshot, pair.record, identity.diff_hash, pair.record.intent_ref.content_hash, identity.changed_paths).complete)
10597
+ return false;
10598
+ return git4(root, ["rev-parse", "HEAD"]).trim() === head;
10599
+ } catch {
10600
+ return false;
10601
+ }
10602
+ }
10603
+ 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);
10608
+ if (confirmations.has(nonce))
10609
+ refuse();
10610
+ confirmations.set(nonce, { snapshot, expires: Date.parse(expires) });
10611
+ }
10612
+ function takeReconfirmation(nonce) {
10613
+ const found = confirmations.get(nonce);
10614
+ confirmations.delete(nonce);
10615
+ if (!found || found.expires <= Date.now())
10616
+ refuse();
10617
+ return found.snapshot;
10618
+ }
10619
+ async function captureBatchReconfirmation(root, record, children) {
10620
+ 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)
10621
+ refuse();
10622
+ const assertNoCommitEvidence = () => {
10623
+ for (const child of record.children) {
10624
+ if (!id.test(child.task_id))
10625
+ refuse();
10626
+ const path = `.imm/state/batches/commits/${record.batch_id}-${child.task_id}.json`;
10627
+ let missing = false;
10628
+ try {
10629
+ readSecureProjectBytes(root, path);
10630
+ } catch (error) {
10631
+ if (error instanceof Error && error.message === `source_missing: ${path}`)
10632
+ missing = true;
10633
+ else
10634
+ refuse();
10635
+ }
10636
+ if (!missing)
10637
+ refuse();
10638
+ }
10639
+ };
10640
+ assertNoCommitEvidence();
10641
+ const statePath = `.imm/state/batches/${record.batch_id}.json`;
10642
+ const stateBytes = readSecureProjectBytes(root, statePath);
10643
+ if (JSON.stringify(JSON.parse(decode(stateBytes))) !== JSON.stringify(record))
10644
+ refuse();
10645
+ const files = new Map;
10646
+ const capture = (path) => {
10647
+ const bytes = readSecureProjectBytes(root, path);
10648
+ files.set(path, bytes);
10649
+ return bytes;
10650
+ };
10651
+ const oldChildren = [];
10652
+ const changed = [];
10653
+ let scope = null;
10654
+ const workspace = readWorkspaceStateRaw(root);
10655
+ if (workspace.state.current_working !== null)
10656
+ refuse();
10657
+ for (const [index, child] of children.entries()) {
10658
+ const previous = record.children[index];
10659
+ if (!id.test(child.task_id) || child.task_id !== previous.task_id || child.slice_id !== previous.slice_id || JSON.stringify(child.blocked_by) !== JSON.stringify(previous.blocked_by) || child.intent_path !== `docs/plans/${child.task_id}.intent.json`)
10660
+ refuse();
10661
+ const old = parseTaskIntentV1(JSON.parse(git4(root, ["show", `${record.base_head}:${child.intent_path}`])));
10662
+ if (old.task_id !== child.task_id)
10663
+ refuse();
10664
+ const oldHash = canonicalIntentHash(old);
10665
+ oldChildren.push({ task_id: child.task_id, blocked_by: [...child.blocked_by], intent_path: child.intent_path, intent_revision: old.revision, intent_content_hash: oldHash });
10666
+ const currentBytes = capture(child.intent_path);
10667
+ const current = parseTaskIntentV1(JSON.parse(decode(currentBytes)));
10668
+ const currentHash = canonicalIntentHash(current);
10669
+ const tracked = readTaskIntent(root, child.task_id, child.intent_path);
10670
+ if (currentHash !== tracked.content_hash || currentHash !== child.intent_content_hash || current.revision !== child.intent_revision)
10671
+ refuse();
10672
+ if (currentHash === oldHash)
10673
+ continue;
10674
+ if (scope !== null || previous.state === "pending" || previous.state === "skipped_blocked" || current.revision <= old.revision || current.goal !== old.goal || current.owner !== old.owner)
10675
+ refuse();
10676
+ const run = localRunId(root, child.task_id);
10677
+ if (!run || !id.test(run))
10678
+ refuse();
10679
+ const local = withKernelRead(root, (db) => readRunRowByTask(db, child.task_id));
10680
+ if (!local || local.run_id !== run || local.state !== "done" || local.enrollment_event_id !== `enroll-${child.task_id}-${local.created_at}` || Date.parse(local.created_at) > Date.parse(record.updated_at))
10681
+ refuse();
10682
+ const pair = readAuditTaskPair(root, child.task_id, run);
10683
+ if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4")
10684
+ refuse();
10685
+ const r = pair.record;
10686
+ if (r.lifecycle !== "done" || r.intent_ref.content_hash !== currentHash || canonicalIntentHash(r.intent_snapshot) !== currentHash || r.git_base_head !== record.base_head || pair.proof.terminal_lifecycle !== "done")
10687
+ refuse();
10688
+ const terminal = r.history.at(-1);
10689
+ if (terminal?.type !== "complete" || terminal.from_state !== "active:frozen" || terminal.to_state !== "done:frozen" || terminal.id !== pair.proof.terminal_event_id)
10690
+ refuse();
10691
+ const recordPath = `.imm/audit/${child.task_id}/${run}/task-record.json`;
10692
+ const proofPath = `.imm/audit/${child.task_id}/${run}/terminal-proof.json`;
10693
+ if (!capture(recordPath).equals(Buffer.from(local.record_json)))
10694
+ refuse();
10695
+ const capturedProof = JSON.parse(decode(capture(proofPath)));
10696
+ if (!matchesLocalProof(local.terminal_proof_json, capturedProof) || !matchesLocalProof(local.terminal_proof_json, pair.proof))
10697
+ refuse();
10698
+ const identity = taskRevisionIdentity(root, r.intent_snapshot.scope_hint, r.git_base_head, r.task_id);
10699
+ const projection = await projectAssurance(root, child.task_id, (cwd, task) => {
10700
+ if (task.contract !== "assurance_kernel/task_record/v4" || !task.git_base_head)
10701
+ refuse();
10702
+ return taskRevisionIdentity(cwd, task.intent_snapshot.scope_hint, task.git_base_head, task.task_id);
10703
+ });
10704
+ if (projection.error || projection.claim || projection.projection.run_id !== run || projection.projection.lifecycle !== "done" || !projection.projection.completion_ready || projection.projection.intent_content_hash !== currentHash || projection.projection.diff_hash !== identity.diff_hash || !projection.projection.fresh_approval_kinds.includes("qa") || projection.projection.risk !== "routine" && !projection.projection.fresh_approval_kinds.includes("review"))
10705
+ refuse();
10706
+ scope = r.intent_snapshot.scope_hint;
10707
+ changed.push({ task: child.task_id, run, identity: JSON.stringify(identity), local: JSON.stringify(local) });
10708
+ }
10709
+ if (computeBatchPlanDigest(oldChildren) !== record.plan_digest || changed.length !== 1 || scope === null)
10710
+ refuse();
10711
+ const confirmedScope = scope;
10712
+ const owner = changed[0];
10713
+ const status = () => git4(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignore-submodules=none"]);
10714
+ const statusBytes = status();
10715
+ const entries = statusBytes.split("\x00").filter(Boolean);
10716
+ for (const entry of entries) {
10717
+ const code = entry.slice(0, 2), path = entry.slice(3);
10718
+ if (code[1] !== " " || !["A", "M", "D"].includes(code[0]) || !path.startsWith(`.imm/audit/${owner.task}/${owner.run}/`) && !confirmedScope.some((s) => pathMatchesScope(path, s)))
10719
+ refuse();
10720
+ }
10721
+ const flagsBytes = git4(root, ["ls-files", "-v"]);
10722
+ const flags = flagsBytes.split(`
10723
+ `).filter(Boolean);
10724
+ if (flags.some((line) => line[0] === "S" || line[0] !== line[0]?.toUpperCase()))
10725
+ refuse();
10726
+ const head = () => git4(root, ["rev-parse", "HEAD"]).trim();
10727
+ const branch = () => git4(root, ["symbolic-ref", "--short", "HEAD"]).trim();
10728
+ if (head() !== record.base_head || branch() !== record.branch)
10729
+ refuse();
10730
+ const assertDeliveryUnchanged = () => {
10731
+ for (const c of changed) {
10732
+ const pair = readAuditTaskPair(root, c.task, c.run);
10733
+ if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4" || !pair.record.git_base_head)
10734
+ refuse();
10735
+ const local = withKernelRead(root, (db) => readRunRowByTask(db, c.task));
10736
+ const diff = taskRevisionIdentity(root, pair.record.intent_snapshot.scope_hint, pair.record.git_base_head, c.task);
10737
+ if (localRunId(root, c.task) !== c.run || JSON.stringify(local) !== c.local || JSON.stringify(diff) !== c.identity)
10738
+ refuse();
10739
+ }
10740
+ };
10741
+ const assertUnchanged = () => {
10742
+ assertNoCommitEvidence();
10743
+ if (!readSecureProjectBytes(root, statePath).equals(stateBytes) || head() !== record.base_head || branch() !== record.branch || status() !== statusBytes || git4(root, ["ls-files", "-v"]) !== flagsBytes || readWorkspaceStateRaw(root).revision !== workspace.revision)
10744
+ refuse();
10745
+ for (const [path, bytes] of files)
10746
+ if (!readSecureProjectBytes(root, path).equals(bytes))
10747
+ refuse();
10748
+ assertDeliveryUnchanged();
10749
+ };
10750
+ assertUnchanged();
10751
+ const plan_digest = computeBatchPlanDigest(oldChildren.map((old, index) => ({
10752
+ ...old,
10753
+ intent_revision: children[index].intent_revision,
10754
+ intent_content_hash: children[index].intent_content_hash
10755
+ })));
10756
+ return { root, plan_digest, stateBytes, assertUnchanged };
10757
+ }
10758
+
10759
+ // plugins/immune-brain/runtime/unattended/batch_plan.ts
10760
+ import { createHash as createHash19 } from "node:crypto";
10761
+ var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10762
+ var DEFAULT_DEADLINE_MS = 8 * 60 * 60 * 1000;
10763
+ var DEFAULT_QA_FAILURE_LIMIT = 2;
10764
+ function specBindingReason(inspection) {
10765
+ if (inspection.missing.length > 0)
10766
+ return `spec_binding_incomplete: ${inspection.missing.join(", ")}`;
10767
+ return inspection.code === "binding_ambiguous" ? "spec_binding_ambiguous" : "spec_binding_missing";
10768
+ }
10769
+ function compareIds(left, right) {
10770
+ return left < right ? -1 : left > right ? 1 : 0;
10771
+ }
10772
+ function positiveSafeInteger(value, name) {
10773
+ if (!Number.isSafeInteger(value) || value <= 0)
10774
+ throw new Error(`${name} must be a positive safe integer`);
10775
+ return value;
10776
+ }
10777
+ var ISO_TIMESTAMP_PATTERN2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
10778
+ function timestamp(value, name) {
10779
+ if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN2.test(value))
10780
+ throw new Error(`${name} must be an ISO timestamp`);
10781
+ const milliseconds = Date.parse(value);
10782
+ if (!Number.isFinite(milliseconds))
10783
+ throw new Error(`${name} must be an ISO timestamp`);
10784
+ return { milliseconds, iso: new Date(milliseconds).toISOString() };
10785
+ }
10786
+ function normalizeObservation(value, initiativeSlug) {
10787
+ if (!value || value.contract !== "immune_brain/github_initiative_observation/v1")
10788
+ throw new Error("tracker returned an invalid Initiative observation contract");
10789
+ if (value.initiative_id !== initiativeSlug)
10790
+ throw new Error("tracker returned an observation for another Initiative");
10791
+ if (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0)
10792
+ throw new Error("tracker returned an invalid Initiative Issue number");
10793
+ if (!Array.isArray(value.tasks))
10794
+ throw new Error("tracker returned an invalid Initiative Task list");
10795
+ const taskIds = new Set;
10796
+ const sliceIds = new Set;
10797
+ const tasks = value.tasks.map((task, index) => {
10798
+ if (!task || typeof task !== "object")
10799
+ throw new Error(`tracker Task ${index} is invalid`);
10800
+ if (!ID_PATTERN2.test(task.task_id))
10801
+ throw new Error(`tracker Task ${index} has an invalid task_id`);
10802
+ if (!ID_PATTERN2.test(task.slice_id))
10803
+ throw new Error(`tracker Task ${index} has an invalid slice_id`);
10804
+ if (!Number.isSafeInteger(task.issue_number) || task.issue_number <= 0)
10805
+ throw new Error(`tracker Task ${task.task_id} has an invalid Issue number`);
10806
+ if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN2.test(id)))
10807
+ throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
10808
+ if (taskIds.has(task.task_id))
10809
+ throw new Error(`tracker returned duplicate Task ${task.task_id}`);
10810
+ if (sliceIds.has(task.slice_id))
10811
+ throw new Error(`tracker returned duplicate Slice ${task.slice_id}`);
10812
+ taskIds.add(task.task_id);
10813
+ sliceIds.add(task.slice_id);
10814
+ const blockedBy = [...task.blocked_by].sort();
10815
+ if (new Set(blockedBy).size !== blockedBy.length || blockedBy.includes(task.task_id))
10816
+ throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
10817
+ return { ...task, blocked_by: blockedBy };
10818
+ });
10819
+ for (const task of tasks) {
10820
+ const unknown = task.blocked_by.find((id) => !taskIds.has(id));
10821
+ if (unknown)
10822
+ throw new Error(`tracker Task ${task.task_id} depends on unknown Task ${unknown}`);
10823
+ }
10824
+ return { ...value, tasks: tasks.sort((left, right) => compareIds(left.task_id, right.task_id)) };
10825
+ }
10826
+ function dependencyOrder(observation) {
10827
+ const remaining = new Map(observation.tasks.map((task) => [task.task_id, task]));
10828
+ const done = new Set;
10829
+ const order = [];
10830
+ while (remaining.size) {
10831
+ const ready = [...remaining.values()].filter((task) => task.blocked_by.every((id) => done.has(id))).sort((left, right) => compareIds(left.task_id, right.task_id));
10832
+ if (!ready.length)
10833
+ throw new Error("Initiative Task dependencies must form an acyclic graph");
10834
+ for (const task of ready) {
10835
+ remaining.delete(task.task_id);
10836
+ done.add(task.task_id);
10837
+ order.push(task);
10838
+ }
10839
+ }
10840
+ return order;
10841
+ }
10842
+ function dependencyClosures(order) {
10843
+ const closures = new Map;
10844
+ for (const task of order) {
10845
+ const closure = new Set;
10846
+ for (const dependency of task.blocked_by) {
10847
+ closure.add(dependency);
10848
+ for (const transitive of closures.get(dependency) ?? [])
10849
+ closure.add(transitive);
10850
+ }
10851
+ closures.set(task.task_id, [...closure].sort());
10852
+ }
10853
+ return closures;
10854
+ }
10855
+ function budget(input, enrollableCount, confirmationTime) {
10856
+ const maxChildren = input.budget?.max_children === undefined ? positiveSafeInteger(enrollableCount, "budget.max_children") : positiveSafeInteger(input.budget.max_children, "budget.max_children");
10857
+ if (maxChildren > enrollableCount)
10858
+ throw new Error("budget.max_children exceeds the enrollable Child count");
10859
+ 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 };
10864
+ }
10865
+ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeGithubInitiative) {
10866
+ if (!ID_PATTERN2.test(initiativeSlug))
10867
+ throw new Error("initiative_slug is invalid");
10868
+ if (!input || typeof input !== "object")
10869
+ throw new Error("batch plan input is required");
10870
+ const confirmationTime = timestamp(input.confirmation_time, "confirmation_time");
10871
+ const trackerObservation = normalizeObservation(await readInitiative(root, initiativeSlug), initiativeSlug);
10872
+ const order = dependencyOrder(trackerObservation);
10873
+ const closures = dependencyClosures(order);
10874
+ const children = [];
10875
+ for (const task of order) {
10876
+ const blockedBy = closures.get(task.task_id);
10877
+ const child = { task_id: task.task_id, slice_id: task.slice_id, blocked_by: blockedBy };
10878
+ const tombstone = readTaskTombstone(root, task.task_id);
10879
+ const record = readTaskRecordRaw(root, task.task_id).record;
10880
+ if (tombstone && record)
10881
+ throw new Error(`Task ${task.task_id} has conflicting settled and active state`);
10882
+ if (tombstone) {
10883
+ children.push({ ...child, status: "already_settled", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
10884
+ continue;
10885
+ }
10886
+ if (record) {
10887
+ children.push({ ...child, status: "already_owned", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
10888
+ continue;
10889
+ }
10890
+ const intentPath = `docs/plans/${task.task_id}.intent.json`;
10891
+ try {
10892
+ const read = observeTaskIntent(root, task.task_id, intentPath);
10893
+ if (read.intent.risk === "critical") {
10894
+ children.push({
10895
+ ...child,
10896
+ status: "needs_human",
10897
+ reason: "critical",
10898
+ intent_path: read.intent_ref.path,
10899
+ intent_revision: read.intent_ref.revision,
10900
+ intent_content_hash: read.content_hash
10901
+ });
10902
+ continue;
10903
+ }
10904
+ const binding = inspectSpecBinding(read.intent);
10905
+ if (!binding.ok) {
10906
+ children.push({
10907
+ ...child,
10908
+ status: "needs_human",
10909
+ reason: specBindingReason(binding),
10910
+ intent_path: read.intent_ref.path,
10911
+ intent_revision: read.intent_ref.revision,
10912
+ intent_content_hash: read.content_hash
10913
+ });
10914
+ continue;
10915
+ }
10916
+ children.push({
10917
+ ...child,
10918
+ status: "enrollable",
10919
+ reason: null,
10920
+ intent_path: read.intent_ref.path,
10921
+ intent_revision: read.intent_ref.revision,
10922
+ intent_content_hash: read.content_hash
10923
+ });
10924
+ } catch (error) {
10925
+ if (!(error instanceof TaskIntentObservationError))
10926
+ throw error;
10927
+ children.push({ ...child, status: "needs_human", reason: "invalid_intent", intent_path: null, intent_revision: null, intent_content_hash: null });
10928
+ }
10929
+ }
10930
+ const directDependencies = new Map(order.map((task) => [task.task_id, task.blocked_by]));
10931
+ const childById = new Map(children.map((child) => [child.task_id, child]));
10932
+ for (const child of children) {
10933
+ if (child.status !== "enrollable")
10934
+ continue;
10935
+ if (directDependencies.get(child.task_id).some((id) => {
10936
+ const status = childById.get(id).status;
10937
+ return status === "already_owned" || status === "needs_human" || status === "blocked";
10938
+ })) {
10939
+ child.status = "blocked";
10940
+ child.reason = "dependency_unavailable";
10941
+ }
10942
+ }
10943
+ const enrollable = children.filter((child) => child.status === "enrollable").map((child) => ({
10944
+ task_id: child.task_id,
10945
+ intent_path: child.intent_path,
10946
+ intent_revision: child.intent_revision,
10947
+ intent_content_hash: child.intent_content_hash,
10948
+ blocked_by: child.blocked_by.filter((id) => childById.get(id)?.status === "enrollable")
10949
+ }));
10950
+ if (!enrollable.length)
10951
+ throw new Error("batch plan has no enrollable children; nothing to confirm");
10952
+ const planDigest = `sha256:${createHash19("sha256").update(stableStringify(enrollable)).digest("hex")}`;
10953
+ return {
10954
+ contract: "assurance_kernel/batch_plan/v1",
10955
+ initiative_slug: initiativeSlug,
10956
+ confirmation_time: confirmationTime.iso,
10957
+ tracker_observation: trackerObservation,
10958
+ children,
10959
+ enrollable,
10960
+ plan_digest: planDigest,
10961
+ budget: budget(input, enrollable.length, confirmationTime)
10962
+ };
10963
+ }
10964
+
10965
+ // plugins/immune-brain/runtime/unattended/batch_preflight.ts
10966
+ var INITIATIVE_SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
10967
+ function batchRejection(key, detail = "") {
10968
+ const resolved = batchReason(key, detail);
10969
+ return {
10970
+ state: resolved.state === "blocked" ? "blocked" : "rejected",
10971
+ reason: resolved.reason,
10972
+ recovery_action: resolved.recovery_action
10973
+ };
10974
+ }
10975
+ function reject(key, detail = "") {
10657
10976
  return { ok: false, ...batchRejection(key, detail) };
10658
10977
  }
10659
10978
  function readActiveClaimTaskId(root) {
@@ -10748,7 +11067,7 @@ function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
10748
11067
  const currentTaskId = workspaceOwner || (claim?.lifecycle_status === "active" ? claim?.task_id : null);
10749
11068
  if (currentTaskId !== taskId || !claim)
10750
11069
  return false;
10751
- const branch = spawnSync5("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" }).stdout.trim();
11070
+ const branch = spawnSync6("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" }).stdout.trim();
10752
11071
  if (branch !== batchBranch)
10753
11072
  return false;
10754
11073
  const childInBatch = existingBatch.children.find((c) => c.task_id === taskId);
@@ -10805,7 +11124,7 @@ function authorizedScopeOf(root, taskId, state) {
10805
11124
  return scope;
10806
11125
  }
10807
11126
  function porcelainEntries(root) {
10808
- const statusProc = spawnSync5("git", ["-C", root, "status", "--porcelain=v1", "-z", "--no-renames", "--untracked-files=all"], { encoding: "utf8" });
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" } });
10809
11128
  if (statusProc.status !== 0)
10810
11129
  return null;
10811
11130
  const entries = [];
@@ -10943,7 +11262,7 @@ async function projectBatchPreflight(options) {
10943
11262
  } catch (err) {
10944
11263
  return reject("git_head_unreadable", err instanceof Error ? err.message : String(err));
10945
11264
  }
10946
- const branchExists = spawnSync5("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${batchBranch}`]);
11265
+ const branchExists = spawnSync6("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${batchBranch}`]);
10947
11266
  if (branchExists.status === 0 && !existingBatch)
10948
11267
  return reject("branch_already_exists", batchBranch);
10949
11268
  const statusEntries = porcelainEntries(root);
@@ -10992,21 +11311,29 @@ async function projectBatchPreflight(options) {
10992
11311
  });
10993
11312
  if (!planSurface.ok)
10994
11313
  return reject(planSurface.key, planSurface.detail);
10995
- return {
10996
- ok: true,
10997
- projection: {
10998
- initiative_slug: initiativeSlug,
10999
- batch_branch: batchBranch,
11000
- is_resuming: isResuming,
11001
- existing_batch: existingBatch,
11002
- base_head: baseHead,
11003
- budget: planSurface.surface.budget,
11004
- plan_digest: planSurface.surface.plan_digest,
11005
- recovery_children: planSurface.surface.recovery_children,
11006
- risk_by_task: planSurface.surface.risk_by_task,
11007
- excluded: planSurface.surface.excluded
11314
+ let reconfirmation;
11315
+ if (activeRecord && activeRecord.plan_digest !== planSurface.surface.plan_digest) {
11316
+ try {
11317
+ reconfirmation = await captureBatchReconfirmation(root, activeRecord, planSurface.surface.recovery_children);
11318
+ } catch {
11319
+ return reject("plan_projection_failed", "batch plan reconfirmation is not eligible");
11008
11320
  }
11321
+ }
11322
+ const projection = {
11323
+ initiative_slug: initiativeSlug,
11324
+ batch_branch: batchBranch,
11325
+ is_resuming: isResuming,
11326
+ existing_batch: existingBatch,
11327
+ base_head: baseHead,
11328
+ budget: planSurface.surface.budget,
11329
+ plan_digest: planSurface.surface.plan_digest,
11330
+ recovery_children: planSurface.surface.recovery_children,
11331
+ risk_by_task: planSurface.surface.risk_by_task,
11332
+ excluded: planSurface.surface.excluded
11009
11333
  };
11334
+ if (reconfirmation)
11335
+ Object.defineProperty(projection, "reconfirmation", { value: reconfirmation });
11336
+ return { ok: true, projection };
11010
11337
  }
11011
11338
  async function projectBatchDrift(options) {
11012
11339
  const { root, initiative_slug: initiativeSlug, readInitiative } = options;
@@ -11047,9 +11374,12 @@ async function authorizeBatch(options) {
11047
11374
  } = projection;
11048
11375
  const existingBatch = projection.existing_batch;
11049
11376
  const isExistingExpired = isResuming && Date.parse(existingBatch.authorization_expires_at) <= Date.now();
11050
- const branchBefore = spawnSync5("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11377
+ const branchBefore = spawnSync6("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11051
11378
  if (branchBefore.status !== 0)
11052
11379
  return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
11380
+ 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)
11382
+ return { outcome: "rejected", rejection: batchRejection("head_moved", "first unpersisted commit provenance is invalid") };
11053
11383
  const reuseBlockers = [];
11054
11384
  if (isResuming && existingBatch) {
11055
11385
  if (isExistingExpired)
@@ -11062,7 +11392,7 @@ async function authorizeBatch(options) {
11062
11392
  reuseBlockers.push("batch_plan_digest_changed");
11063
11393
  if (existingBatch.branch !== batchBranch)
11064
11394
  reuseBlockers.push("batch_branch_changed");
11065
- if (expectedBatchHead(existingBatch) !== baseHead)
11395
+ if (expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead)
11066
11396
  reuseBlockers.push("batch_head_lineage_moved");
11067
11397
  }
11068
11398
  const reuseAuthorization = isResuming && reuseBlockers.length === 0;
@@ -11104,7 +11434,7 @@ async function authorizeBatch(options) {
11104
11434
  return { outcome: "rejected", rejection: batchRejection("plan_changed", drift.plan_digest) };
11105
11435
  if (drift.base_head === null)
11106
11436
  return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
11107
- if (drift.base_head !== baseHead)
11437
+ if (drift.base_head !== baseHead || ownsUnpersistedHead && !ownUnpersistedBatchHead(root, existingBatch, baseHead))
11108
11438
  return { outcome: "rejected", rejection: batchRejection("head_moved", drift.base_head) };
11109
11439
  const finalClaimTaskId = readActiveClaimTaskId(root);
11110
11440
  const finalOwnClaim = isResuming && finalClaimTaskId !== null && isOwnBatchClaim(root, existingBatch, finalClaimTaskId, batchBranch);
@@ -11115,11 +11445,18 @@ async function authorizeBatch(options) {
11115
11445
  if (!current || current.corrupt || JSON.stringify(current.record) !== JSON.stringify(existingBatch))
11116
11446
  return { outcome: "rejected", rejection: batchRejection("plan_changed") };
11117
11447
  }
11118
- const branchAfter = spawnSync5("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11448
+ const branchAfter = spawnSync6("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
11119
11449
  if (branchAfter.status !== 0 || branchAfter.stdout !== branchBefore.stdout)
11120
11450
  return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "Git branch moved after native confirmation") };
11121
11451
  if (Date.parse(expiresAt) <= Date.now())
11122
11452
  return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "confirmed batch deadline expired before authorization issuance") };
11453
+ if (projection.reconfirmation) {
11454
+ try {
11455
+ projection.reconfirmation.assertUnchanged();
11456
+ } catch {
11457
+ return { outcome: "rejected", rejection: batchRejection("plan_changed") };
11458
+ }
11459
+ }
11123
11460
  const binding = {
11124
11461
  batch_id: batchId,
11125
11462
  initiative_slug: initiativeSlug,
@@ -11132,6 +11469,8 @@ async function authorizeBatch(options) {
11132
11469
  expires_at: expiresAt,
11133
11470
  nonce: options.nonce
11134
11471
  };
11472
+ if (projection.reconfirmation)
11473
+ retainReconfirmation(binding.nonce, binding.expires_at, projection.reconfirmation);
11135
11474
  return {
11136
11475
  outcome: "authorized",
11137
11476
  batch_id: batchId,
@@ -11143,12 +11482,12 @@ async function authorizeBatch(options) {
11143
11482
  }
11144
11483
 
11145
11484
  // plugins/immune-brain/runtime/unattended/batch_runner.ts
11146
- import { spawnSync as spawnSync7 } from "node:child_process";
11485
+ import { spawnSync as spawnSync8 } from "node:child_process";
11147
11486
  import { existsSync as existsSync9 } from "node:fs";
11148
11487
  import { join as join14 } from "node:path";
11149
11488
 
11150
11489
  // plugins/immune-brain/runtime/unattended/batch_git.ts
11151
- import { spawnSync as spawnSync6 } from "node:child_process";
11490
+ import { spawnSync as spawnSync7 } from "node:child_process";
11152
11491
  import { createHash as createHash20, randomUUID as randomUUID8 } from "node:crypto";
11153
11492
  import {
11154
11493
  constants as constants6,
@@ -11173,7 +11512,7 @@ var DEFAULT_GIT_ENV = {
11173
11512
  function runBatchGitPreflight(input) {
11174
11513
  const { root, initiative_slug: initiativeSlug, base_head: baseHead } = input;
11175
11514
  const branch = `imm/${initiativeSlug}`;
11176
- const toplevelResult = spawnSync6("git", ["-C", root, "rev-parse", "--show-toplevel"], {
11515
+ const toplevelResult = spawnSync7("git", ["-C", root, "rev-parse", "--show-toplevel"], {
11177
11516
  encoding: "utf8",
11178
11517
  stdio: ["ignore", "pipe", "pipe"]
11179
11518
  });
@@ -11211,7 +11550,7 @@ function runBatchGitPreflight(input) {
11211
11550
  message: `unsupported index flags (assume-unchanged/skip-worktree) detected: ${flaggedPreflight.join(", ")}`
11212
11551
  };
11213
11552
  }
11214
- const statusResult = spawnSync6("git", ["-C", root, "status", "--porcelain", "--untracked-files=all", "--ignore-submodules=none"], {
11553
+ const statusResult = spawnSync7("git", ["-C", root, "status", "--porcelain", "--untracked-files=all", "--ignore-submodules=none"], {
11215
11554
  encoding: "utf8",
11216
11555
  stdio: ["ignore", "pipe", "pipe"]
11217
11556
  });
@@ -11229,7 +11568,7 @@ function runBatchGitPreflight(input) {
11229
11568
  message: "working tree is dirty before batch preflight"
11230
11569
  };
11231
11570
  }
11232
- const headResult = spawnSync6("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
11571
+ const headResult = spawnSync7("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
11233
11572
  encoding: "utf8",
11234
11573
  stdio: ["ignore", "pipe", "pipe"]
11235
11574
  });
@@ -11240,11 +11579,11 @@ function runBatchGitPreflight(input) {
11240
11579
  message: "HEAD is uncommitted or not a valid commit"
11241
11580
  };
11242
11581
  }
11243
- const branchCheck = spawnSync6("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { stdio: ["ignore", "ignore", "ignore"] });
11582
+ const branchCheck = spawnSync7("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { stdio: ["ignore", "ignore", "ignore"] });
11244
11583
  if (branchCheck.status === 0) {
11245
11584
  const settled = findSettledBatchRecord(root, initiativeSlug);
11246
- const currentBranch = spawnSync6("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], { encoding: "utf8" });
11247
- if (findExistingActiveBatch(root, initiativeSlug) === null && settled?.branch === branch && currentBranch.status === 0 && currentBranch.stdout.trim() === branch && headResult.stdout.trim() === baseHead && spawnSync6("git", ["-C", root, "merge-base", "--is-ancestor", expectedBatchHead(settled), baseHead]).status === 0)
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)
11248
11587
  return { ok: true, branch };
11249
11588
  return {
11250
11589
  ok: false,
@@ -11252,25 +11591,25 @@ function runBatchGitPreflight(input) {
11252
11591
  message: `branch refs/heads/${branch} already exists`
11253
11592
  };
11254
11593
  }
11255
- const originalBranchResult = spawnSync6("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11594
+ const originalBranchResult = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11256
11595
  encoding: "utf8",
11257
11596
  stdio: ["ignore", "pipe", "pipe"]
11258
11597
  });
11259
11598
  const originalBranch = originalBranchResult.stdout.trim();
11260
- const checkoutResult = spawnSync6("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", "-b", branch, baseHead], {
11599
+ const checkoutResult = spawnSync7("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", "-b", branch, baseHead], {
11261
11600
  encoding: "utf8",
11262
11601
  stdio: ["ignore", "pipe", "pipe"],
11263
11602
  env: DEFAULT_GIT_ENV
11264
11603
  });
11265
11604
  if (checkoutResult.status !== 0) {
11266
11605
  const stderr = checkoutResult.stderr?.trim() || "";
11267
- const branchExists = stderr.includes("already exists") || spawnSync6("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
11268
- const currentBranchCheck = spawnSync6("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
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"], {
11269
11608
  encoding: "utf8",
11270
11609
  stdio: ["ignore", "pipe", "pipe"]
11271
11610
  }).stdout.trim();
11272
11611
  if (currentBranchCheck === branch && originalBranch && originalBranch !== branch) {
11273
- spawnSync6("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", originalBranch], {
11612
+ spawnSync7("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", originalBranch], {
11274
11613
  stdio: ["ignore", "ignore", "ignore"]
11275
11614
  });
11276
11615
  }
@@ -11293,7 +11632,7 @@ function hasBoundaryWhitespace(path) {
11293
11632
  return path.split("/").some((segment) => segment.trim() !== segment || segment.length === 0);
11294
11633
  }
11295
11634
  function getUnsupportedIndexFlags(root) {
11296
- const result = spawnSync6("git", ["-C", root, "ls-files", "-v", "-z", "--"], {
11635
+ const result = spawnSync7("git", ["-C", root, "ls-files", "-v", "-z", "--"], {
11297
11636
  encoding: "utf8",
11298
11637
  stdio: ["ignore", "pipe", "pipe"]
11299
11638
  });
@@ -11327,13 +11666,13 @@ function isPathAllowedForChild(path, taskId, scopeHint) {
11327
11666
  });
11328
11667
  }
11329
11668
  function getChangedProjectPaths(root) {
11330
- const tracked = spawnSync6("git", ["-C", root, "diff-index", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
11669
+ const tracked = spawnSync7("git", ["-C", root, "diff-index", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
11331
11670
  encoding: "utf8",
11332
11671
  stdio: ["ignore", "pipe", "pipe"]
11333
11672
  });
11334
11673
  if (tracked.status !== 0)
11335
11674
  throw new Error("failed to inspect tracked diff vs HEAD");
11336
- const untracked = spawnSync6("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
11675
+ const untracked = spawnSync7("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
11337
11676
  encoding: "utf8",
11338
11677
  stdio: ["ignore", "pipe", "pipe"]
11339
11678
  });
@@ -11343,7 +11682,7 @@ function getChangedProjectPaths(root) {
11343
11682
  return [...new Set([...splitZ(tracked.stdout), ...splitZ(untracked.stdout)])];
11344
11683
  }
11345
11684
  function getStagedProjectPaths(root) {
11346
- const staged = spawnSync6("git", ["-C", root, "diff-index", "--cached", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
11685
+ const staged = spawnSync7("git", ["-C", root, "diff-index", "--cached", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
11347
11686
  encoding: "utf8",
11348
11687
  stdio: ["ignore", "pipe", "pipe"]
11349
11688
  });
@@ -11352,13 +11691,13 @@ function getStagedProjectPaths(root) {
11352
11691
  return staged.stdout.split("\x00").filter((p) => p.length > 0);
11353
11692
  }
11354
11693
  function getUnstagedProjectPaths(root) {
11355
- const diffFiles = spawnSync6("git", ["-C", root, "diff-files", "--name-only", "-z", "--ignore-submodules=none", "--"], {
11694
+ const diffFiles = spawnSync7("git", ["-C", root, "diff-files", "--name-only", "-z", "--ignore-submodules=none", "--"], {
11356
11695
  encoding: "utf8",
11357
11696
  stdio: ["ignore", "pipe", "pipe"]
11358
11697
  });
11359
11698
  if (diffFiles.status !== 0)
11360
11699
  throw new Error("failed to inspect unstaged tracked changes");
11361
- const untracked = spawnSync6("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
11700
+ const untracked = spawnSync7("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
11362
11701
  encoding: "utf8",
11363
11702
  stdio: ["ignore", "pipe", "pipe"]
11364
11703
  });
@@ -11368,7 +11707,7 @@ function getUnstagedProjectPaths(root) {
11368
11707
  return [...new Set([...splitZ(diffFiles.stdout), ...splitZ(untracked.stdout)])];
11369
11708
  }
11370
11709
  function getCommittedDeltaPaths(root) {
11371
- const delta = spawnSync6("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", "HEAD~1", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
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"] });
11372
11711
  if (delta.status !== 0)
11373
11712
  throw new Error("failed to inspect committed tree delta");
11374
11713
  return delta.stdout.split("\x00").filter((p) => p.length > 0);
@@ -11474,7 +11813,7 @@ async function commitBatchChild(input) {
11474
11813
  const goal = typeof intentSnapshot.goal === "string" ? intentSnapshot.goal : "";
11475
11814
  const scopeHint = Array.isArray(intentSnapshot.scope_hint) ? intentSnapshot.scope_hint.filter((s) => typeof s === "string") : [];
11476
11815
  if (expectedBranch !== undefined) {
11477
- const currentBranchResult = spawnSync6("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11816
+ const currentBranchResult = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11478
11817
  encoding: "utf8",
11479
11818
  stdio: ["ignore", "pipe", "pipe"]
11480
11819
  });
@@ -11483,7 +11822,7 @@ async function commitBatchChild(input) {
11483
11822
  throw new Error(`batch_head_lineage_broken: current branch ${currentBranch} does not match expected branch ${expectedBranch}`);
11484
11823
  }
11485
11824
  }
11486
- const currentHeadResult = spawnSync6("git", ["-C", root, "rev-parse", "HEAD"], {
11825
+ const currentHeadResult = spawnSync7("git", ["-C", root, "rev-parse", "HEAD"], {
11487
11826
  encoding: "utf8",
11488
11827
  stdio: ["ignore", "pipe", "pipe"]
11489
11828
  });
@@ -11502,7 +11841,7 @@ async function commitBatchChild(input) {
11502
11841
  }
11503
11842
  const pathsToStage = getUnstagedProjectPaths(root);
11504
11843
  if (pathsToStage.length > 0) {
11505
- const addResult = spawnSync6("git", ["-C", root, "--literal-pathspecs", "add", "-A", "--", ...pathsToStage], {
11844
+ const addResult = spawnSync7("git", ["-C", root, "--literal-pathspecs", "add", "-A", "--", ...pathsToStage], {
11506
11845
  encoding: "utf8",
11507
11846
  stdio: ["ignore", "pipe", "pipe"]
11508
11847
  });
@@ -11517,7 +11856,7 @@ async function commitBatchChild(input) {
11517
11856
  const staged = getStagedProjectPaths(root);
11518
11857
  const stagedOutside = staged.filter((p) => !isPathAllowedForChild(p, taskId, scopeHint));
11519
11858
  if (stagedOutside.length > 0) {
11520
- spawnSync6("git", ["-C", root, "reset", "--quiet"], { stdio: ["ignore", "ignore", "ignore"] });
11859
+ spawnSync7("git", ["-C", root, "reset", "--quiet"], { stdio: ["ignore", "ignore", "ignore"] });
11521
11860
  throw new Error("dirty_outside_scope");
11522
11861
  }
11523
11862
  const record = auditPair.record;
@@ -11535,7 +11874,7 @@ async function commitBatchChild(input) {
11535
11874
 
11536
11875
  Immune-Brain-Batch: ${batchId}
11537
11876
  `;
11538
- const commitResult = spawnSync6("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "commit", "--no-verify", "-F", "-"], {
11877
+ const commitResult = spawnSync7("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "commit", "--no-verify", "-F", "-"], {
11539
11878
  input: commitMessage,
11540
11879
  encoding: "utf8",
11541
11880
  stdio: ["pipe", "pipe", "pipe"],
@@ -11544,7 +11883,7 @@ Immune-Brain-Batch: ${batchId}
11544
11883
  if (commitResult.status !== 0) {
11545
11884
  throw new Error(`commit failed for child ${taskId}: ${commitResult.stderr?.trim() || "git commit failed"}`);
11546
11885
  }
11547
- const newHeadResult = spawnSync6("git", ["-C", root, "rev-parse", "HEAD"], {
11886
+ const newHeadResult = spawnSync7("git", ["-C", root, "rev-parse", "HEAD"], {
11548
11887
  encoding: "utf8",
11549
11888
  stdio: ["ignore", "pipe", "pipe"]
11550
11889
  });
@@ -11552,7 +11891,7 @@ Immune-Brain-Batch: ${batchId}
11552
11891
  if (newHeadResult.status !== 0 || !newHead || newHead === expectedHead) {
11553
11892
  throw new Error("commit_failed");
11554
11893
  }
11555
- const parentsResult = spawnSync6("git", ["-C", root, "rev-parse", "HEAD^@"], {
11894
+ const parentsResult = spawnSync7("git", ["-C", root, "rev-parse", "HEAD^@"], {
11556
11895
  encoding: "utf8",
11557
11896
  stdio: ["ignore", "pipe", "pipe"]
11558
11897
  });
@@ -11585,7 +11924,7 @@ async function lookupBatchCommit(input) {
11585
11924
  let candidates = [];
11586
11925
  let evidenceBacked = false;
11587
11926
  if (evidence && evidence.commit) {
11588
- const direct = spawnSync6("git", ["-C", root, "log", "-n", "1", `--format=${FORMAT}`, evidence.commit, "--"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
11927
+ const direct = spawnSync7("git", ["-C", root, "log", "-n", "1", `--format=${FORMAT}`, evidence.commit, "--"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
11589
11928
  if (direct.status !== 0) {
11590
11929
  throw new Error(`failed to inspect batch commit for ${taskId}: ${direct.stderr?.trim() || "git log failed"}`);
11591
11930
  }
@@ -11598,7 +11937,7 @@ async function lookupBatchCommit(input) {
11598
11937
  }
11599
11938
  }
11600
11939
  if (!candidates.length) {
11601
- const result = spawnSync6("git", [
11940
+ const result = spawnSync7("git", [
11602
11941
  "-C",
11603
11942
  root,
11604
11943
  "log",
@@ -11635,7 +11974,7 @@ async function lookupBatchCommit(input) {
11635
11974
  throw new Error(`batch_head_lineage_broken: adopted commit author ${match.authorName} does not match batch authority ${DEFAULT_GIT_ENV.GIT_AUTHOR_NAME}`);
11636
11975
  }
11637
11976
  if (expectedBranch !== undefined) {
11638
- const currentBranchResult = spawnSync6("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11977
+ const currentBranchResult = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11639
11978
  encoding: "utf8",
11640
11979
  stdio: ["ignore", "pipe", "pipe"]
11641
11980
  });
@@ -11645,7 +11984,7 @@ async function lookupBatchCommit(input) {
11645
11984
  }
11646
11985
  }
11647
11986
  if (expectedHead !== undefined) {
11648
- const currentHeadResult = spawnSync6("git", ["-C", root, "rev-parse", "HEAD"], {
11987
+ const currentHeadResult = spawnSync7("git", ["-C", root, "rev-parse", "HEAD"], {
11649
11988
  encoding: "utf8",
11650
11989
  stdio: ["ignore", "pipe", "pipe"]
11651
11990
  });
@@ -11653,7 +11992,7 @@ async function lookupBatchCommit(input) {
11653
11992
  if (currentHead !== commit) {
11654
11993
  throw new Error(`batch_head_lineage_broken: current HEAD ${currentHead} diverged from adopted commit ${commit}`);
11655
11994
  }
11656
- const parentsResult = spawnSync6("git", ["-C", root, "rev-parse", `${commit}^@`], {
11995
+ const parentsResult = spawnSync7("git", ["-C", root, "rev-parse", `${commit}^@`], {
11657
11996
  encoding: "utf8",
11658
11997
  stdio: ["ignore", "pipe", "pipe"]
11659
11998
  });
@@ -11671,7 +12010,7 @@ async function lookupBatchCommit(input) {
11671
12010
  throw new Error(`batch_head_lineage_broken: adopted commit task lifecycle is not done: ${lifecycle}`);
11672
12011
  }
11673
12012
  const scopeHint = Array.isArray(auditPair.record.intent_snapshot.scope_hint) ? auditPair.record.intent_snapshot.scope_hint.filter((s) => typeof s === "string") : [];
11674
- const deltaResult = spawnSync6("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", expectedHead, commit], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
12013
+ const deltaResult = spawnSync7("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", expectedHead, commit], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
11675
12014
  if (deltaResult.status !== 0) {
11676
12015
  throw new Error("batch_head_lineage_broken: failed to inspect adopted commit delta");
11677
12016
  }
@@ -11791,14 +12130,14 @@ function externalHeadDriftMessage(root, record) {
11791
12130
  if (!existsSync9(join14(root, ".git")))
11792
12131
  return null;
11793
12132
  const head = record.commits.length ? record.commits[record.commits.length - 1] : record.base_head;
11794
- const headCheck = spawnSync7("git", ["-C", root, "rev-parse", "HEAD"], {
12133
+ const headCheck = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
11795
12134
  encoding: "utf8",
11796
12135
  stdio: ["ignore", "pipe", "pipe"]
11797
12136
  });
11798
12137
  if (headCheck.status === 0 && headCheck.stdout.trim() && headCheck.stdout.trim() !== head) {
11799
12138
  return `batch_head_lineage_broken: current HEAD ${headCheck.stdout.trim()} does not match expected batch head ${head}`;
11800
12139
  }
11801
- const branchCheck = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
12140
+ const branchCheck = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
11802
12141
  encoding: "utf8",
11803
12142
  stdio: ["ignore", "pipe", "pipe"]
11804
12143
  });
@@ -11809,6 +12148,11 @@ function externalHeadDriftMessage(root, record) {
11809
12148
  return null;
11810
12149
  }
11811
12150
  async function validatePersistedRun(input, record) {
12151
+ 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()))
12154
+ throw new Error("first unpersisted batch commit provenance is invalid");
12155
+ }
11812
12156
  const plan = input.registry.children(input.capability);
11813
12157
  if (record.plan_digest !== computeBatchPlanDigest(plan) || record.children.length !== plan.length || record.children.some((child, index) => {
11814
12158
  const expected = plan[index];
@@ -11830,7 +12174,7 @@ async function validatePersistedRun(input, record) {
11830
12174
  });
11831
12175
  if (!evidence || evidence.commit !== child.commit) {
11832
12176
  if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync9(join14(input.root, ".git"))) {
11833
- const reach = spawnSync7("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
12177
+ const reach = spawnSync8("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
11834
12178
  if (reach.status !== 0) {
11835
12179
  throw new Error(`batch_head_lineage_broken: recorded commit ${child.commit} for ${child.task_id} is no longer reachable from HEAD`);
11836
12180
  }
@@ -11868,6 +12212,41 @@ function validateRunAuthorization(input, existing) {
11868
12212
  });
11869
12213
  if (computeBatchPlanDigest(children) !== authorized.plan_digest || input.plan_digest !== authorized.plan_digest || input.base_head !== authorized.base_head)
11870
12214
  throw new Error("batch run input does not match the authorized plan or base_head");
12215
+ return authorized;
12216
+ }
12217
+ function applyPlanReconfirmation(input, existing) {
12218
+ if (existing.plan_digest === input.plan_digest)
12219
+ return existing;
12220
+ const next = {
12221
+ ...existing,
12222
+ plan_digest: input.plan_digest,
12223
+ confirmation_time: input.confirmation_time,
12224
+ authorization_expires_at: input.authorization_expires_at,
12225
+ budget: input.budget
12226
+ };
12227
+ 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)
12228
+ throw new Error("plan_digest mismatch: reconfirmation binding changed");
12229
+ const validate = () => {
12230
+ const plan = input.registry.children(input.capability);
12231
+ if (existing.children.length !== plan.length || plan.some((child, index) => {
12232
+ const old = existing.children[index], nextChild = input.children[index];
12233
+ return !nextChild || old.task_id !== child.task_id || old.slice_id !== nextChild.slice_id || JSON.stringify(old.blocked_by) !== JSON.stringify(child.blocked_by) || JSON.stringify(nextChild.blocked_by) !== JSON.stringify(child.blocked_by);
12234
+ }))
12235
+ throw new Error("plan_digest mismatch: reconfirmation topology changed");
12236
+ const { issued_at: _issuedAt, ...binding } = validateRunAuthorization(input, { ...existing, plan_digest: input.plan_digest });
12237
+ if (binding.branch !== existing.branch)
12238
+ throw new Error("plan_digest mismatch: reconfirmation branch changed");
12239
+ prepareBatchRunState(input);
12240
+ return input.registry.inspect(input.capability, binding);
12241
+ };
12242
+ const authority = validate();
12243
+ const captured = takeReconfirmation(authority.nonce);
12244
+ if (captured.root !== input.root || captured.plan_digest !== input.plan_digest)
12245
+ throw new Error("plan_digest mismatch: captured reconfirmation does not match run input");
12246
+ return replaceBatchRunState(input.root, captured.stateBytes, next, () => {
12247
+ captured.assertUnchanged();
12248
+ validate();
12249
+ });
11871
12250
  }
11872
12251
  async function startBatch(input) {
11873
12252
  try {
@@ -11898,7 +12277,9 @@ async function startBatchLocked(input) {
11898
12277
  return reportFor({ ...rejected, batch_state: "rejected" }, "batch plan is empty", "Provide a non-empty enrollable child plan.");
11899
12278
  }
11900
12279
  let existing = readBatchRunState(input.root, input.batch_id);
12280
+ const reconfirmed = existing !== null && existing.plan_digest !== input.plan_digest;
11901
12281
  if (existing) {
12282
+ existing = applyPlanReconfirmation(input, existing);
11902
12283
  try {
11903
12284
  await validatePersistedRun(input, existing);
11904
12285
  } catch (error) {
@@ -11920,10 +12301,11 @@ async function startBatchLocked(input) {
11920
12301
  const nextConfirmation = Date.parse(input.confirmation_time);
11921
12302
  const nextExpiry = Date.parse(input.authorization_expires_at);
11922
12303
  const freshAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation && Number.isFinite(nextExpiry) && nextExpiry > Date.now();
11923
- if (!freshAuthorization) {
12304
+ if (!freshAuthorization && !reconfirmed) {
11924
12305
  return finalize(input.root, existing, "the parked batch requires a fresh literal-user confirmation", "Resolve the parked child, then re-confirm the batch to continue.");
11925
12306
  }
11926
- validateRunAuthorization(input, existing);
12307
+ if (!reconfirmed)
12308
+ validateRunAuthorization(input, existing);
11927
12309
  prepareBatchRunState({ ...input, now: input.now });
11928
12310
  const remapped = await Promise.all(existing.children.map(async (child) => {
11929
12311
  if (child.state !== "needs_human")
@@ -12138,8 +12520,9 @@ function stopReasonFor(record) {
12138
12520
  }
12139
12521
  async function resumeBatch(input, projection) {
12140
12522
  let existing = readBatchRunState(input.root, input.batch_id);
12141
- if (!existing)
12523
+ if (!existing || existing.batch_state === "needs_human")
12142
12524
  return startBatch(input);
12525
+ existing = applyPlanReconfirmation(input, existing);
12143
12526
  try {
12144
12527
  await validatePersistedRun(input, existing);
12145
12528
  } catch (error) {
@@ -12154,8 +12537,6 @@ async function resumeBatch(input, projection) {
12154
12537
  }
12155
12538
  if (isTerminalBatchState(existing.batch_state))
12156
12539
  return replayTerminal(input.root, existing);
12157
- if (existing.batch_state === "needs_human")
12158
- return startBatch(input);
12159
12540
  const driven = existing.children.find((child) => child.state === "enrolled" || child.state === "settled");
12160
12541
  if (driven) {
12161
12542
  if (driven.state === "enrolled") {
@@ -12280,6 +12661,8 @@ async function driveInterruptedChild(input, child) {
12280
12661
  intentPath
12281
12662
  });
12282
12663
  };
12664
+ if (existing && !record.commits.length && existsSync9(join14(input.root, ".git")) && !ownUnpersistedBatchHead(input.root, record, existing.commit))
12665
+ throw new Error("first unpersisted batch commit provenance is invalid");
12283
12666
  const adopted = existing ?? await doCommit().catch((error) => {
12284
12667
  const message = error instanceof Error ? error.message : String(error);
12285
12668
  record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);