immune-brain 3.6.6 → 3.6.8

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 (30) hide show
  1. package/README.md +110 -51
  2. package/README.zh-CN.md +153 -49
  3. package/package.json +2 -1
  4. package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
  5. package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +29 -19
  6. package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +932 -0
  7. package/plugins/immune-brain/.pi-extension/package.json +3 -2
  8. package/plugins/immune-brain/.pi-extension/pi-canary-interaction.ts +149 -4
  9. package/plugins/immune-brain/.pi-extension/runtime-stub.ts +200 -2
  10. package/plugins/immune-brain/dist/claude/mcp-server.mjs +3404 -227
  11. package/plugins/immune-brain/dist/imm-loop.md +25 -0
  12. package/plugins/immune-brain/dist/role-prompts/code-review.md +9 -1
  13. package/plugins/immune-brain/runtime/assurance/coordinator.ts +101 -3
  14. package/plugins/immune-brain/runtime/claude/interaction.ts +16 -3
  15. package/plugins/immune-brain/runtime/claude/kernel_ports.ts +840 -17
  16. package/plugins/immune-brain/runtime/claude/mcp_server.ts +72 -10
  17. package/plugins/immune-brain/runtime/kernel/canary_application.ts +9 -0
  18. package/plugins/immune-brain/runtime/kernel/completion.ts +19 -1
  19. package/plugins/immune-brain/runtime/kernel/reducer.ts +162 -11
  20. package/plugins/immune-brain/runtime/kernel/refutation.ts +82 -0
  21. package/plugins/immune-brain/runtime/kernel/types.ts +34 -1
  22. package/plugins/immune-brain/runtime/kernel/validation.ts +202 -9
  23. package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
  24. package/plugins/immune-brain/runtime/prompts/code-review.md +9 -1
  25. package/plugins/immune-brain/runtime/unattended/batch_git.ts +775 -0
  26. package/plugins/immune-brain/runtime/unattended/batch_plan.ts +203 -0
  27. package/plugins/immune-brain/runtime/unattended/batch_runner.ts +1224 -0
  28. package/plugins/immune-brain/runtime/unattended/batch_state.ts +360 -0
  29. package/plugins/immune-brain/runtime/unattended/types.ts +60 -0
  30. package/plugins/immune-brain/skills/imm-loop/SKILL.md +1 -0
@@ -2,7 +2,7 @@ import { createRequire } from "node:module";
2
2
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
3
 
4
4
  // plugins/immune-brain/runtime/claude/mcp_server.ts
5
- import { randomUUID as randomUUID6 } from "node:crypto";
5
+ import { randomUUID as randomUUID8 } from "node:crypto";
6
6
  import { createInterface } from "node:readline";
7
7
  import { stdin, stdout } from "node:process";
8
8
 
@@ -42,7 +42,7 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
42
42
  }
43
43
 
44
44
  // plugins/immune-brain/runtime/plugin_version.ts
45
- var PLUGIN_VERSION = "3.6.6";
45
+ var PLUGIN_VERSION = "3.6.8";
46
46
 
47
47
  // plugins/immune-brain/runtime/claude/interaction.ts
48
48
  import { createHash, randomUUID } from "node:crypto";
@@ -50,7 +50,8 @@ var PRIVILEGED_OPERATIONS = [
50
50
  "enroll",
51
51
  "request_authorization",
52
52
  "approve_breaking_intent_revision",
53
- "stop"
53
+ "stop",
54
+ "start_unattended_batch"
54
55
  ];
55
56
  var RECOVERY_ACTIONS = {
56
57
  interaction_not_opened: "retry through a fresh native gate in the current Host",
@@ -91,7 +92,7 @@ function evaluateNativeGate(input) {
91
92
  return { ok: true };
92
93
  }
93
94
  function confirmationRef(input) {
94
- return `claude-confirm-${createHash("sha256").update(`${input.connectionId}\x00${input.toolCallId}\x00${input.requestId}\x00${input.operation}\x00${input.taskId}\x00${input.intentRevision ?? ""}\x00${input.intentContentHash ?? ""}\x00${input.bindingDigest ?? ""}`).digest("hex").slice(0, 16)}`;
95
+ return `claude-confirm-${createHash("sha256").update(`${input.connectionId}\x00${input.toolCallId}\x00${input.requestId}\x00${input.operation}\x00${input.taskId ?? ""}\x00${input.initiativeSlug ?? ""}\x00${input.intentRevision ?? ""}\x00${input.intentContentHash ?? ""}\x00${input.bindingDigest ?? ""}\x00${input.planDigest ?? ""}`).digest("hex").slice(0, 16)}`;
95
96
  }
96
97
  function enrollmentNonce() {
97
98
  return randomUUID();
@@ -678,16 +679,64 @@ function parseHookStdin(raw) {
678
679
  }
679
680
 
680
681
  // plugins/immune-brain/runtime/claude/kernel_ports.ts
681
- import { randomUUID as randomUUID5 } from "node:crypto";
682
- import { existsSync as existsSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
683
- import { execFileSync as execFileSync4 } from "node:child_process";
684
- import { join as join7 } from "node:path";
682
+ import { randomUUID as randomUUID7 } from "node:crypto";
683
+ import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
684
+ import { execFileSync as execFileSync4, spawnSync as spawnSync6 } from "node:child_process";
685
+ import { join as join10 } from "node:path";
685
686
 
686
687
  // plugins/immune-brain/runtime/assurance/coordinator.ts
687
- import { createHash as createHash5, randomUUID as randomUUID2 } from "node:crypto";
688
+ import { createHash as createHash6, randomUUID as randomUUID2 } from "node:crypto";
688
689
 
689
- // plugins/immune-brain/runtime/assurance/verification.ts
690
+ // plugins/immune-brain/runtime/kernel/refutation.ts
690
691
  import { createHash as createHash3 } from "node:crypto";
692
+
693
+ // plugins/immune-brain/runtime/canonical_json.ts
694
+ function stableStringify(value) {
695
+ if (value === null || value === undefined)
696
+ return "null";
697
+ if (typeof value !== "object")
698
+ return JSON.stringify(value);
699
+ if (Array.isArray(value)) {
700
+ return `[${value.map(stableStringify).join(", ")}]`;
701
+ }
702
+ const obj = value;
703
+ return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}: ${stableStringify(obj[key])}`).join(", ")}}`;
704
+ }
705
+
706
+ // plugins/immune-brain/runtime/kernel/refutation.ts
707
+ function refutationIdentity(record, diffHash) {
708
+ return {
709
+ intent_revision: record.intent_snapshot.revision,
710
+ intent_content_hash: record.intent_ref.content_hash,
711
+ diff_hash: diffHash
712
+ };
713
+ }
714
+ function anchorForEvidence(evidence) {
715
+ return `sha256:${createHash3("sha256").update(stableStringify({
716
+ violated: evidence.violated,
717
+ caller_chain: evidence.caller_chain
718
+ })).digest("hex")}`;
719
+ }
720
+ function isFreshPassingQaAttestation(attestation, acceptanceId, identity) {
721
+ if (!attestation || attestation.kind !== "qa")
722
+ return false;
723
+ if (attestation.task_revision !== identity.intent_revision)
724
+ return false;
725
+ if (attestation.intent_content_hash !== identity.intent_content_hash)
726
+ return false;
727
+ if (attestation.diff_hash !== identity.diff_hash)
728
+ return false;
729
+ return attestation.acceptance_results.some((result) => result.acceptance_id === acceptanceId && result.status === "passed");
730
+ }
731
+ function refutationIsLive(finding, attestations, identity) {
732
+ const counterevidence = finding.counterevidence ?? null;
733
+ if (!counterevidence?.attestation_id || !counterevidence.acceptance_id)
734
+ return false;
735
+ return isFreshPassingQaAttestation(attestations.find((item) => item.id === counterevidence.attestation_id), counterevidence.acceptance_id, identity);
736
+ }
737
+
738
+ // plugins/immune-brain/runtime/assurance/verification.ts
739
+ import { createHash as createHash4 } from "node:crypto";
691
740
  import { execFileSync, spawn, spawnSync } from "node:child_process";
692
741
  import { realpathSync as realpathSync2, statSync } from "node:fs";
693
742
  import { isAbsolute as isAbsolute2, resolve, sep as sep2, relative } from "node:path";
@@ -813,7 +862,7 @@ function resolveBunRunner() {
813
862
  if (!stat.isFile())
814
863
  throw new VerificationDescriptorError("bun runner is not a regular file");
815
864
  const { readFileSync } = __require("node:fs");
816
- const contentHash = `sha256:${createHash3("sha256").update(readFileSync(real)).digest("hex")}`;
865
+ const contentHash = `sha256:${createHash4("sha256").update(readFileSync(real)).digest("hex")}`;
817
866
  let version = "";
818
867
  try {
819
868
  version = execFileSync(real, ["--version"], { encoding: "utf8" }).trim();
@@ -971,7 +1020,7 @@ function findingsDigest(findings) {
971
1020
  kind: f.kind,
972
1021
  summary: f.summary
973
1022
  }));
974
- return `sha256:${createHash3("sha256").update(`[${normalized.join(",")}]`).digest("hex")}`;
1023
+ return `sha256:${createHash4("sha256").update(`[${normalized.join(",")}]`).digest("hex")}`;
975
1024
  }
976
1025
 
977
1026
  // plugins/immune-brain/runtime/assurance/invocations.ts
@@ -1025,25 +1074,10 @@ function createInvocationRegistry() {
1025
1074
  }
1026
1075
 
1027
1076
  // plugins/immune-brain/runtime/role_prompt_bridge.ts
1028
- import { createHash as createHash4 } from "node:crypto";
1077
+ import { createHash as createHash5 } from "node:crypto";
1029
1078
  import { existsSync, readFileSync } from "node:fs";
1030
1079
  import { join as join3, dirname as dirname2 } from "node:path";
1031
1080
  import { fileURLToPath } from "node:url";
1032
-
1033
- // plugins/immune-brain/runtime/canonical_json.ts
1034
- function stableStringify(value) {
1035
- if (value === null || value === undefined)
1036
- return "null";
1037
- if (typeof value !== "object")
1038
- return JSON.stringify(value);
1039
- if (Array.isArray(value)) {
1040
- return `[${value.map(stableStringify).join(", ")}]`;
1041
- }
1042
- const obj = value;
1043
- return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}: ${stableStringify(obj[key])}`).join(", ")}}`;
1044
- }
1045
-
1046
- // plugins/immune-brain/runtime/role_prompt_bridge.ts
1047
1081
  var RUNTIME_DIR = dirname2(fileURLToPath(import.meta.url));
1048
1082
  var INTERNAL_ROLE_PROMPTS = {
1049
1083
  qa: { file: "qa.md", authority: "qa", tool_policy: "no tools" },
@@ -1131,7 +1165,7 @@ function buildRoleDelegationPacket(input) {
1131
1165
  ].join(`
1132
1166
 
1133
1167
  `);
1134
- const promptDigest = `sha256:${createHash4("sha256").update(prompt).digest("hex")}`;
1168
+ const promptDigest = `sha256:${createHash5("sha256").update(prompt).digest("hex")}`;
1135
1169
  return {
1136
1170
  contract: "immune_brain/role_delegation/v1",
1137
1171
  role: input.role,
@@ -1144,6 +1178,22 @@ function buildRoleDelegationPacket(input) {
1144
1178
  }
1145
1179
 
1146
1180
  // plugins/immune-brain/runtime/assurance/coordinator.ts
1181
+ function reviewReworkFindings(verdict) {
1182
+ if (verdict.decision !== "rework" || !verdict.findings?.length)
1183
+ throw new Error("review rework findings require a rework verdict");
1184
+ return verdict.findings.map((finding) => ({
1185
+ id: finding.id,
1186
+ kind: finding.kind,
1187
+ status: "open",
1188
+ acceptance_id: finding.acceptance_id,
1189
+ source: "review",
1190
+ review_round: null,
1191
+ summary: finding.summary,
1192
+ anchor: finding.anchor ?? null,
1193
+ evidence: finding.evidence ?? null,
1194
+ counterevidence: null
1195
+ }));
1196
+ }
1147
1197
  var QA_MIN_JOB_TIMEOUT_SECONDS = 15 * 60;
1148
1198
  var QA_MAX_JOB_TIMEOUT_SECONDS = 60 * 60;
1149
1199
  var QA_JOB_OVERHEAD_SECONDS = 2 * 60;
@@ -1171,7 +1221,7 @@ function reviewTurnBudget(workload) {
1171
1221
  return workload === "quick" ? 12 : workload === "standard" ? 16 : 24;
1172
1222
  }
1173
1223
  function snapshotDigest(snapshot) {
1174
- return `sha256:${createHash5("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
1224
+ return `sha256:${createHash6("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
1175
1225
  }
1176
1226
  function buildReviewPrompt(snapshot, evidencePath) {
1177
1227
  if (snapshot.role !== "review")
@@ -1211,12 +1261,45 @@ function buildReviewPrompt(snapshot, evidencePath) {
1211
1261
  "Acceptance assertions:",
1212
1262
  acceptance,
1213
1263
  "Reserve the final turn for exactly one strict JSON verdict. Reply with ONLY that object, without markdown fences or commentary.",
1264
+ `Every rework finding must carry machine-checkable provenance: evidence.trigger (the concrete inputs or state that reach the defect), a non-empty evidence.caller_chain (ordered repository paths or symbols), and evidence.violated {kind: "acceptance"|"security_boundary", ref}. The anchor is derived from that evidence; a finding without it is rejected and the correction must be resubmitted.`,
1214
1265
  `PASS shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"pass","approval":{"kind":"review","authority_role":"reviewer","summary":"<one line>"}}`,
1215
- `REWORK shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"rework","findings":[{"id":"review-1","kind":"blocking|advisory","acceptance_id":"<id|null>","summary":"<one line>"}]}`,
1266
+ `REWORK shape: {"contract":"assurance_kernel/assurance_verdict/v2","role":"review","task_id":"${snapshot.task_id}","snapshot_digest":"${digest}","decision":"rework","findings":[{"id":"review-1","kind":"blocking|advisory","acceptance_id":"<id|null>","summary":"<one line>","evidence":{"trigger":"<concrete inputs or state>","caller_chain":["<path-or-symbol>"],"violated":{"kind":"acceptance|security_boundary","ref":"<acceptance id or boundary>"}}}]}`,
1216
1267
  `REWORK verdicts must omit the approval field entirely; do not emit "approval": null.`
1217
1268
  ].join(`
1218
1269
  `);
1219
1270
  }
1271
+ function parseVerdictEvidence(value, index) {
1272
+ if (!value || typeof value !== "object" || Array.isArray(value))
1273
+ throw new Error(`finding ${index} evidence is required`);
1274
+ const evidence = value;
1275
+ const unknown = Object.keys(evidence).find((key) => !["trigger", "caller_chain", "violated"].includes(key));
1276
+ if (unknown)
1277
+ throw new Error(`finding ${index} evidence has unknown field: ${unknown}`);
1278
+ if (typeof evidence.trigger !== "string" || !evidence.trigger.trim())
1279
+ throw new Error(`finding ${index} evidence.trigger must be a non-empty string`);
1280
+ const chain = evidence.caller_chain;
1281
+ if (!Array.isArray(chain) || chain.length === 0 || chain.some((entry) => typeof entry !== "string" || !entry.trim()))
1282
+ throw new Error(`finding ${index} evidence.caller_chain must be a non-empty list of paths or symbols`);
1283
+ const violated = evidence.violated;
1284
+ if (!violated || typeof violated !== "object" || Array.isArray(violated))
1285
+ throw new Error(`finding ${index} evidence.violated is required`);
1286
+ const violatedRecord = violated;
1287
+ const unknownViolated = Object.keys(violatedRecord).find((key) => !["kind", "ref"].includes(key));
1288
+ if (unknownViolated)
1289
+ throw new Error(`finding ${index} evidence.violated has unknown field: ${unknownViolated}`);
1290
+ if (violatedRecord.kind !== "acceptance" && violatedRecord.kind !== "security_boundary")
1291
+ throw new Error(`finding ${index} evidence.violated.kind must be acceptance or security_boundary`);
1292
+ if (typeof violatedRecord.ref !== "string" || !violatedRecord.ref.trim())
1293
+ throw new Error(`finding ${index} evidence.violated.ref must be a non-empty string`);
1294
+ return {
1295
+ trigger: evidence.trigger,
1296
+ caller_chain: chain,
1297
+ violated: {
1298
+ kind: violatedRecord.kind,
1299
+ ref: violatedRecord.ref
1300
+ }
1301
+ };
1302
+ }
1220
1303
  function parseAssuranceVerdict(input, snapshot) {
1221
1304
  let raw;
1222
1305
  if (typeof input === "string") {
@@ -1267,14 +1350,24 @@ function parseAssuranceVerdict(input, snapshot) {
1267
1350
  throw new Error("rework verdict must omit approval");
1268
1351
  const findings = raw.findings.map((item, index) => {
1269
1352
  const finding = item;
1270
- const unknownFinding = Object.keys(finding).find((key) => !["id", "kind", "acceptance_id", "summary"].includes(key));
1353
+ const allowedFindingKeys = snapshot.role === "review" ? ["id", "kind", "acceptance_id", "summary", "evidence"] : ["id", "kind", "acceptance_id", "summary"];
1354
+ const unknownFinding = Object.keys(finding).find((key) => !allowedFindingKeys.includes(key));
1271
1355
  if (unknownFinding)
1272
1356
  throw new Error(`finding ${index} has unknown field: ${unknownFinding}`);
1273
1357
  if (typeof finding.id !== "string" || !finding.id.trim() || finding.kind !== "blocking" && finding.kind !== "advisory" || finding.acceptance_id !== null && typeof finding.acceptance_id !== "string" || typeof finding.summary !== "string" || !finding.summary.trim())
1274
1358
  throw new Error(`finding ${index} is invalid`);
1275
1359
  const id = `review-${snapshotDigest(snapshot).slice(7, 19)}-${index + 1}-${finding.id.replace(/[^A-Za-z0-9._-]/g, "-").slice(0, 48)}`;
1276
1360
  const normalized = { id, kind: finding.kind, acceptance_id: finding.acceptance_id, summary: finding.summary };
1277
- return { ...normalized, findings_digest: findingsDigest([normalized]) };
1361
+ if (snapshot.role !== "review")
1362
+ return { ...normalized, findings_digest: findingsDigest([normalized]) };
1363
+ const evidence = parseVerdictEvidence(finding.evidence, index);
1364
+ const anchor = anchorForEvidence(evidence);
1365
+ return {
1366
+ ...normalized,
1367
+ anchor,
1368
+ evidence,
1369
+ findings_digest: findingsDigest([normalized])
1370
+ };
1278
1371
  });
1279
1372
  return { contract: "assurance_kernel/assurance_verdict/v2", role: snapshot.role, task_id: snapshot.task_id, snapshot_digest: snapshotDigest(snapshot), decision: "rework", findings };
1280
1373
  }
@@ -1817,7 +1910,7 @@ function boundedAssuranceError(error) {
1817
1910
 
1818
1911
  // plugins/immune-brain/runtime/assurance/review_evidence.ts
1819
1912
  import { execFileSync as execFileSync2 } from "node:child_process";
1820
- import { createHash as createHash7 } from "node:crypto";
1913
+ import { createHash as createHash8 } from "node:crypto";
1821
1914
  import {
1822
1915
  mkdtempSync,
1823
1916
  rmSync as rmSync2,
@@ -1832,7 +1925,7 @@ import { join as join4 } from "node:path";
1832
1925
 
1833
1926
  // plugins/immune-brain/runtime/workspace_scope.ts
1834
1927
  import { spawnSync as spawnSync2 } from "node:child_process";
1835
- import { createHash as createHash6 } from "node:crypto";
1928
+ import { createHash as createHash7 } from "node:crypto";
1836
1929
  import {
1837
1930
  existsSync as existsSync2,
1838
1931
  lstatSync as lstatSync2,
@@ -2059,7 +2152,7 @@ function captureGitTaskSnapshot(projectRoot, scopeHint) {
2059
2152
  return before;
2060
2153
  }
2061
2154
  function hashTaskSnapshot(snapshot) {
2062
- return `sha256:${createHash6("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
2155
+ return `sha256:${createHash7("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
2063
2156
  }
2064
2157
  function taskDiffIdentity(projectRoot, scopeHint) {
2065
2158
  const snapshot = captureGitTaskSnapshot(projectRoot, scopeHint);
@@ -2204,7 +2297,7 @@ function pathMatchesScope(path, scopePath) {
2204
2297
  // plugins/immune-brain/runtime/assurance/review_evidence.ts
2205
2298
  var MAX_REVIEW_BUNDLE_BYTES = 2 * 1024 * 1024;
2206
2299
  function bundleDigest(bundle) {
2207
- return `sha256:${createHash7("sha256").update(JSON.stringify(bundle)).digest("hex")}`;
2300
+ return `sha256:${createHash8("sha256").update(JSON.stringify(bundle)).digest("hex")}`;
2208
2301
  }
2209
2302
  var reviewUtf8 = new TextDecoder("utf-8", { fatal: true });
2210
2303
  function readIndexBlob(root, path, entry) {
@@ -2354,7 +2447,7 @@ var SNAPSHOT_IDENTITY = {
2354
2447
  date: "1970-01-01T00:00:00 +0000"
2355
2448
  };
2356
2449
  function manifestDigest(manifest) {
2357
- return `sha256:${createHash7("sha256").update(JSON.stringify(manifest)).digest("hex")}`;
2450
+ return `sha256:${createHash8("sha256").update(JSON.stringify(manifest)).digest("hex")}`;
2358
2451
  }
2359
2452
  function gitEvidenceBytes(root, args, extraEnv = {}) {
2360
2453
  return execFileSync2("git", args, {
@@ -2480,7 +2573,7 @@ function publishInput(root, input) {
2480
2573
  if (!REVISION_DIFF_HASH.test(input.expectedDiffHash))
2481
2574
  throw new Error("review task revision hash has invalid identity");
2482
2575
  const snapshot = captureGitTaskRevisionSnapshot(root, input.scopeHint, input.baseHead);
2483
- const recomputed = `sha256:${createHash7("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
2576
+ const recomputed = `sha256:${createHash8("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
2484
2577
  if (recomputed !== input.expectedDiffHash)
2485
2578
  throw new Error("review task revision does not match assurance snapshot");
2486
2579
  return { snapshot, revision: publishReviewRevision(snapshot.repository_root, snapshot, recomputed, input.taskId) };
@@ -2713,7 +2806,7 @@ function serializeTaskTombstone(tombstone) {
2713
2806
  }
2714
2807
 
2715
2808
  // plugins/immune-brain/runtime/kernel/storage.ts
2716
- import { createHash as createHash9, randomUUID as randomUUID3 } from "node:crypto";
2809
+ import { createHash as createHash10, randomUUID as randomUUID3 } from "node:crypto";
2717
2810
  import {
2718
2811
  closeSync as closeSync3,
2719
2812
  constants as constants2,
@@ -2747,7 +2840,7 @@ var REDUCED_MUTATION_BRAND = Symbol("assurance-kernel-reduced-mutation-v2");
2747
2840
  var MUTATION_AUTHORITY_CAPABILITY_BRAND = Symbol("assurance-kernel-mutation-authority-capability");
2748
2841
 
2749
2842
  // plugins/immune-brain/runtime/kernel/intent.ts
2750
- import { createHash as createHash8 } from "node:crypto";
2843
+ import { createHash as createHash9 } from "node:crypto";
2751
2844
  import {
2752
2845
  closeSync as closeSync2,
2753
2846
  constants as fsConstants,
@@ -2878,7 +2971,7 @@ var portablePathCollator2 = new Intl.Collator("und", {
2878
2971
  ignorePunctuation: false
2879
2972
  });
2880
2973
  function sha256Hex(bytes) {
2881
- return createHash8("sha256").update(bytes).digest("hex");
2974
+ return createHash9("sha256").update(bytes).digest("hex");
2882
2975
  }
2883
2976
  function objectAt(value, path, violations) {
2884
2977
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -3178,6 +3271,10 @@ function readTaskIntentSource(root, taskId, requestedPath) {
3178
3271
  }
3179
3272
  };
3180
3273
  }
3274
+ function observeTaskIntent(root, taskId, requestedPath) {
3275
+ const { identity: _identity, ...observed } = readTaskIntentSource(root, taskId, requestedPath);
3276
+ return observed;
3277
+ }
3181
3278
  function readTaskIntent(root, taskId, requestedPath) {
3182
3279
  const { identity, ...observed } = readTaskIntentSource(root, taskId, requestedPath);
3183
3280
  return { ...observed, token: mintToken(identity) };
@@ -3271,7 +3368,7 @@ var FINDING_KINDS = [
3271
3368
  "unresolved_user_decision",
3272
3369
  "replan_required"
3273
3370
  ];
3274
- var FINDING_STATUSES = ["open", "resolved"];
3371
+ var FINDING_STATUSES = ["open", "resolved", "refuted"];
3275
3372
  var FINDING_SOURCES = [
3276
3373
  "execution",
3277
3374
  "review",
@@ -3284,10 +3381,52 @@ var APPROVAL_AUTHORITY_ROLES = [
3284
3381
  "qa",
3285
3382
  "user"
3286
3383
  ];
3384
+ function nullableAnchor(value, path, violations) {
3385
+ if (value === null || value === undefined)
3386
+ return null;
3387
+ return stringAt(value, path, violations);
3388
+ }
3389
+ function parseFindingEvidence(value, path, violations) {
3390
+ if (value === null || value === undefined)
3391
+ return null;
3392
+ const item = objectAt2(value, path, violations);
3393
+ rejectUnknown2(item, ["trigger", "caller_chain", "violated"], path, violations);
3394
+ const trigger = stringAt(item.trigger, `${path}.trigger`, violations);
3395
+ const chain = arrayAt(item.caller_chain, `${path}.caller_chain`, violations);
3396
+ if (chain.length === 0)
3397
+ violations.push(`${path}.caller_chain must contain at least one entry`);
3398
+ const caller_chain = chain.map((entry, index) => stringAt(entry, `${path}.caller_chain[${index}]`, violations));
3399
+ const violatedRaw = objectAt2(item.violated, `${path}.violated`, violations);
3400
+ rejectUnknown2(violatedRaw, ["kind", "ref"], `${path}.violated`, violations);
3401
+ const kind = enumAt(violatedRaw.kind, ["acceptance", "security_boundary"], `${path}.violated.kind`, violations);
3402
+ const ref = stringAt(violatedRaw.ref, `${path}.violated.ref`, violations);
3403
+ return { trigger, caller_chain, violated: { kind, ref } };
3404
+ }
3405
+ function parseFindingCounterevidence(value, path, violations) {
3406
+ if (value === null || value === undefined)
3407
+ return null;
3408
+ const item = objectAt2(value, path, violations);
3409
+ rejectUnknown2(item, ["attestation_id", "acceptance_id"], path, violations);
3410
+ return {
3411
+ attestation_id: stringAt(item.attestation_id, `${path}.attestation_id`, violations),
3412
+ acceptance_id: stringAt(item.acceptance_id, `${path}.acceptance_id`, violations)
3413
+ };
3414
+ }
3287
3415
  function parseFinding(value, index, violations) {
3288
3416
  const path = `record.findings[${index}]`;
3289
3417
  const item = objectAt2(value, path, violations);
3290
- rejectUnknown2(item, ["id", "kind", "status", "acceptance_id", "source", "review_round", "summary"], path, violations);
3418
+ rejectUnknown2(item, [
3419
+ "id",
3420
+ "kind",
3421
+ "status",
3422
+ "acceptance_id",
3423
+ "source",
3424
+ "review_round",
3425
+ "summary",
3426
+ "anchor",
3427
+ "evidence",
3428
+ "counterevidence"
3429
+ ], path, violations);
3291
3430
  return {
3292
3431
  id: stringAt(item.id, `${path}.id`, violations),
3293
3432
  kind: enumAt(item.kind, FINDING_KINDS, `${path}.kind`, violations),
@@ -3295,7 +3434,12 @@ function parseFinding(value, index, violations) {
3295
3434
  acceptance_id: nullableString(item.acceptance_id, `${path}.acceptance_id`, violations),
3296
3435
  source: enumAt(item.source, FINDING_SOURCES, `${path}.source`, violations),
3297
3436
  review_round: nullablePositiveInteger(item.review_round, `${path}.review_round`, violations),
3298
- summary: stringAt(item.summary, `${path}.summary`, violations)
3437
+ summary: stringAt(item.summary, `${path}.summary`, violations),
3438
+ ...item.anchor !== undefined ? { anchor: nullableAnchor(item.anchor, `${path}.anchor`, violations) } : {},
3439
+ ...item.evidence !== undefined ? { evidence: parseFindingEvidence(item.evidence, `${path}.evidence`, violations) } : {},
3440
+ ...item.counterevidence !== undefined ? {
3441
+ counterevidence: parseFindingCounterevidence(item.counterevidence, `${path}.counterevidence`, violations)
3442
+ } : {}
3299
3443
  };
3300
3444
  }
3301
3445
  function parseHistoryV2(value, index, violations) {
@@ -3630,6 +3774,31 @@ function parseTaskRecordAtVersion(raw, version) {
3630
3774
  uniqueIds(attestations, "record.attestations", violations);
3631
3775
  uniqueIds(findings, "record.findings", violations);
3632
3776
  uniqueIds(history, "record.history", violations);
3777
+ for (const [index, finding] of findings.entries()) {
3778
+ const path = `record.findings[${index}]`;
3779
+ if (finding.anchor != null && !SHA256_HEX.test(finding.anchor))
3780
+ violations.push(`${path}.anchor must be sha256:<64 hex>`);
3781
+ const anchor = finding.anchor ?? null;
3782
+ const evidence = finding.evidence ?? null;
3783
+ if (anchor === null !== (evidence === null))
3784
+ violations.push(`${path} must carry anchor and evidence together`);
3785
+ if (anchor !== null && evidence !== null && anchor !== anchorForEvidence(evidence))
3786
+ violations.push(`${path}.anchor must equal the digest of its evidence`);
3787
+ if (finding.status === "refuted" && (finding.kind === "unresolved_user_decision" || finding.kind === "replan_required"))
3788
+ violations.push(`${path} refuted requires a blocking or advisory finding`);
3789
+ const counterevidence = finding.counterevidence ?? null;
3790
+ if (finding.status === "refuted" && !counterevidence)
3791
+ violations.push(`${path} status refuted requires counterevidence`);
3792
+ if (!counterevidence)
3793
+ continue;
3794
+ if (finding.status === "open")
3795
+ violations.push(`${path} open finding cannot carry counterevidence`);
3796
+ const attestation = attestations.find((item) => item.id === counterevidence.attestation_id);
3797
+ if (!attestation || attestation.kind !== "qa")
3798
+ violations.push(`${path}.counterevidence.attestation_id must reference a qa attestation`);
3799
+ else if (!attestation.acceptance_results.some((result) => result.acceptance_id === counterevidence.acceptance_id && result.status === "passed"))
3800
+ violations.push(`${path}.counterevidence.acceptance_id must be an acceptance its attestation passed`);
3801
+ }
3633
3802
  if (violations.length > 0)
3634
3803
  throw new KernelValidationError(violations);
3635
3804
  const record = {
@@ -3680,6 +3849,7 @@ function assertKernelInvariantsV3(intentRaw, recordRaw) {
3680
3849
  var ACTION_V2_TYPES = [
3681
3850
  "record_finding",
3682
3851
  "resolve_finding",
3852
+ "refute_finding",
3683
3853
  "record_approval",
3684
3854
  "revise_intent",
3685
3855
  "approve_breaking_intent_revision",
@@ -3744,6 +3914,16 @@ function parseTaskAction(raw) {
3744
3914
  };
3745
3915
  break;
3746
3916
  }
3917
+ case "refute_finding": {
3918
+ rejectUnknown2(value, [...ACTION_BASE_FIELDS, "finding_id", "attestation_id"], "action", violations);
3919
+ action = {
3920
+ ...base,
3921
+ type: "refute_finding",
3922
+ finding_id: stringAt(value.finding_id, "action.finding_id", violations),
3923
+ attestation_id: stringAt(value.attestation_id, "action.attestation_id", violations)
3924
+ };
3925
+ break;
3926
+ }
3747
3927
  case "record_approval": {
3748
3928
  rejectUnknown2(value, [...ACTION_BASE_FIELDS, "approval"], "action", violations);
3749
3929
  const approval = parseApprovalV2(value.approval, 0, violations, true);
@@ -3854,12 +4034,17 @@ function assertTaskRecordUpdateV3(previousRaw, nextRaw, action) {
3854
4034
  violations.push(`attestation ${prior.id} was rewritten`);
3855
4035
  }
3856
4036
  const resolvingFindingIds = action.type === "resolve_finding" ? [action.finding_id] : action.type === "resolve_user_decision" ? [action.finding_id] : action.type === "authorize_rework" || action.type === "approve_breaking_intent_revision" ? previous.findings.filter((item) => item.kind === "replan_required" && item.status === "open").map((item) => item.id) : [];
3857
- const reworkFindingIds = action.type === "request_rework" ? new Set(action.findings.map((item) => item.id)) : new Set;
4037
+ const withoutFields = (finding, fields) => {
4038
+ const copy = { ...finding };
4039
+ for (const field of fields)
4040
+ delete copy[field];
4041
+ return JSON.stringify(copy);
4042
+ };
3858
4043
  for (const prior of previous.findings) {
3859
4044
  const current = next.findings.find((item) => item.id === prior.id);
3860
4045
  if (!current)
3861
4046
  violations.push(`finding item ${prior.id} was removed`);
3862
- else if (JSON.stringify(current) !== JSON.stringify(prior) && !(resolvingFindingIds.includes(prior.id) && prior.status === "open" && current.status === "resolved") && !(reworkFindingIds.has(prior.id) && current.review_round !== null))
4047
+ else if (JSON.stringify(current) !== JSON.stringify(prior) && !(resolvingFindingIds.includes(prior.id) && prior.status !== "resolved" && current.status === "resolved" && (prior.status === "open" || !refutationIsLive(prior, previous.attestations, refutationIdentity(previous, action.diff_hash))) && withoutFields(prior, ["status"]) === withoutFields(current, ["status"])) && !(action.type === "refute_finding" && prior.id === action.finding_id && prior.status !== "resolved" && current.status === "refuted" && (prior.status === "open" || !refutationIsLive(prior, previous.attestations, refutationIdentity(previous, action.diff_hash))) && current.counterevidence?.attestation_id === action.attestation_id && current.counterevidence?.acceptance_id === prior.acceptance_id && isFreshPassingQaAttestation(previous.attestations.find((item) => item.id === action.attestation_id), current.counterevidence?.acceptance_id ?? "", refutationIdentity(previous, action.diff_hash)) && withoutFields(prior, ["status", "counterevidence"]) === withoutFields(current, ["status", "counterevidence"])))
3863
4048
  violations.push(`finding item ${prior.id} was rewritten`);
3864
4049
  }
3865
4050
  if (violations.length > 0)
@@ -3885,7 +4070,7 @@ class KernelStoreSecurityError extends Error {
3885
4070
  }
3886
4071
  }
3887
4072
  function revisionFor(content) {
3888
- return `sha256:${createHash9("sha256").update(content).digest("hex")}`;
4073
+ return `sha256:${createHash10("sha256").update(content).digest("hex")}`;
3889
4074
  }
3890
4075
  function canonicalRoot(root) {
3891
4076
  try {
@@ -5025,7 +5210,12 @@ function completionDecision(intent, record, currentDiffHash, currentIntentConten
5025
5210
  }
5026
5211
  }
5027
5212
  const independenceViolations = candidates.filter((item) => repeatedActors.has(item.actor_id)).map((item) => item.id);
5028
- const blockingFindingIds = record.findings.filter((item) => item.status === "open" && item.kind === "blocking").map((item) => item.id);
5213
+ const refutationState = {
5214
+ intent_revision: intent.revision,
5215
+ intent_content_hash: currentIntentContentHash,
5216
+ diff_hash: currentDiffHash
5217
+ };
5218
+ const blockingFindingIds = record.findings.filter((item) => item.kind === "blocking" && (item.status === "open" || item.status === "refuted" && !refutationIsLive(item, record.attestations, refutationState))).map((item) => item.id);
5029
5219
  const unresolvedUserDecisionIds = record.findings.filter((item) => item.status === "open" && item.kind === "unresolved_user_decision").map((item) => item.id);
5030
5220
  const replanRequiredIds = record.findings.filter((item) => item.status === "open" && item.kind === "replan_required").map((item) => item.id);
5031
5221
  return {
@@ -5253,7 +5443,7 @@ async function projectAssurance(root, taskId, diffProvider) {
5253
5443
  }
5254
5444
 
5255
5445
  // plugins/immune-brain/runtime/kernel/reducer.ts
5256
- import { createHash as createHash10 } from "node:crypto";
5446
+ import { createHash as createHash11 } from "node:crypto";
5257
5447
  var RISK_RANK2 = {
5258
5448
  routine: 0,
5259
5449
  material: 1,
@@ -5280,13 +5470,13 @@ function stableJson(value) {
5280
5470
  return primitive === undefined ? "null" : primitive;
5281
5471
  }
5282
5472
  function canonicalRecordHash(record) {
5283
- return `sha256:${createHash10("sha256").update(`${JSON.stringify(record, null, 2)}
5473
+ return `sha256:${createHash11("sha256").update(`${JSON.stringify(record, null, 2)}
5284
5474
  `).digest("hex")}`;
5285
5475
  }
5286
5476
  function actionFingerprint(action, intentRevision, intentContentHash, audit) {
5287
5477
  const { expected_record_hash: _r, expected_workspace_hash: _w, diff_hash: _d, ...payload } = action;
5288
5478
  const base = audit ? { action: payload, intentRevision, intentContentHash, audit } : { action: payload, intentRevision, intentContentHash };
5289
- return createHash10("sha256").update(stableJson(base)).digest("hex");
5479
+ return createHash11("sha256").update(stableJson(base)).digest("hex");
5290
5480
  }
5291
5481
  function historyReason(action, intentRevision, intentContentHash, audit, detail) {
5292
5482
  const fingerprint = `action_v2_sha256:${actionFingerprint(action, intentRevision, intentContentHash, audit)}`;
@@ -5343,9 +5533,20 @@ function findingsDigestV2(findings) {
5343
5533
  id: finding.id,
5344
5534
  kind: finding.kind,
5345
5535
  acceptance_id: finding.acceptance_id,
5346
- summary: finding.summary
5536
+ summary: finding.summary,
5537
+ ...finding.anchor !== undefined ? { anchor: finding.anchor ?? null } : {},
5538
+ ...finding.evidence !== undefined ? { evidence: finding.evidence ?? null } : {}
5347
5539
  }));
5348
- return `sha256:${createHash10("sha256").update(stableJson(normalized)).digest("hex")}`;
5540
+ return `sha256:${createHash11("sha256").update(stableJson(normalized)).digest("hex")}`;
5541
+ }
5542
+ function sharesAcceptanceBoundary(left, right) {
5543
+ if (left.acceptance_id !== null && right.acceptance_id !== null)
5544
+ return left.acceptance_id === right.acceptance_id;
5545
+ if (left.acceptance_id !== null || right.acceptance_id !== null)
5546
+ return false;
5547
+ const leftRef = left.evidence?.violated.ref ?? null;
5548
+ const rightRef = right.evidence?.violated.ref ?? null;
5549
+ return leftRef !== null && leftRef === rightRef;
5349
5550
  }
5350
5551
  function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
5351
5552
  const previous = parseTaskRecord(recordRaw);
@@ -5430,10 +5631,52 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
5430
5631
  throw new KernelInvariantError([
5431
5632
  `finding ${action.finding_id} is already resolved`
5432
5633
  ]);
5634
+ if (finding.status === "refuted" && refutationIsLive(finding, record.attestations, refutationIdentity(record, action.diff_hash)))
5635
+ throw new KernelInvariantError([
5636
+ `finding ${action.finding_id} is still refuted by live counterevidence`
5637
+ ]);
5433
5638
  finding.status = "resolved";
5434
5639
  appendHistory(record, action, from, action.finding_id, authorityAudit);
5435
5640
  break;
5436
5641
  }
5642
+ case "refute_finding": {
5643
+ if (record.lifecycle !== "active")
5644
+ throw new KernelInvariantError([
5645
+ `cannot refute findings while lifecycle is ${record.lifecycle}`
5646
+ ]);
5647
+ const finding = record.findings.find((item) => item.id === action.finding_id);
5648
+ if (!finding)
5649
+ throw new KernelInvariantError([
5650
+ `finding ${action.finding_id} does not exist`
5651
+ ]);
5652
+ if (finding.kind === "unresolved_user_decision" || finding.kind === "replan_required")
5653
+ throw new KernelInvariantError([
5654
+ "refute_finding cannot refute a user decision or replan boundary"
5655
+ ]);
5656
+ if (finding.status === "resolved")
5657
+ throw new KernelInvariantError([
5658
+ `finding ${action.finding_id} is already resolved`
5659
+ ]);
5660
+ if (!finding.acceptance_id)
5661
+ throw new KernelInvariantError([
5662
+ "refute_finding requires a finding bound to an acceptance id"
5663
+ ]);
5664
+ if (finding.status === "refuted" && refutationIsLive(finding, record.attestations, refutationIdentity(record, action.diff_hash)))
5665
+ throw new KernelInvariantError([
5666
+ `finding ${action.finding_id} is already refuted by live counterevidence`
5667
+ ]);
5668
+ if (!isFreshPassingQaAttestation(record.attestations.find((item) => item.id === action.attestation_id), finding.acceptance_id, refutationIdentity(record, action.diff_hash)))
5669
+ throw new KernelInvariantError([
5670
+ `refute_finding requires a fresh passing QA attestation covering ${finding.acceptance_id ?? "null"}`
5671
+ ]);
5672
+ finding.status = "refuted";
5673
+ finding.counterevidence = {
5674
+ attestation_id: action.attestation_id,
5675
+ acceptance_id: finding.acceptance_id
5676
+ };
5677
+ appendHistory(record, action, from, action.finding_id, authorityAudit);
5678
+ break;
5679
+ }
5437
5680
  case "record_approval": {
5438
5681
  if (record.lifecycle !== "active" || record.artifact_state !== "frozen")
5439
5682
  throw new KernelInvariantError([
@@ -5554,28 +5797,55 @@ function reduceTask(recordRaw, actionRaw, authorityAudit = null, changedPaths) {
5554
5797
  "request_rework requires review, qa, or user authority"
5555
5798
  ]);
5556
5799
  const round = reviewRound(record);
5557
- const hasPriorBlockingReviewRework = record.findings.some((finding) => finding.source === "review" && finding.kind === "blocking" && finding.review_round !== null);
5558
- const parkForReplan = authorityAudit.authority_kind === "review" && hasPriorBlockingReviewRework && action.findings.some((finding) => finding.kind === "blocking");
5800
+ for (const finding of action.findings) {
5801
+ const anchor = finding.anchor ?? null;
5802
+ const evidence = finding.evidence ?? null;
5803
+ if (anchor === null !== (evidence === null))
5804
+ throw new KernelInvariantError([
5805
+ `finding ${finding.id} must carry anchor and evidence together`
5806
+ ]);
5807
+ if (evidence && anchor !== anchorForEvidence(evidence))
5808
+ throw new KernelInvariantError([
5809
+ `finding ${finding.id} anchor must equal the digest of its evidence`
5810
+ ]);
5811
+ if (anchor !== null && authorityAudit.authority_kind !== "review")
5812
+ throw new KernelInvariantError([
5813
+ `${authorityAudit.authority_kind} rework cannot carry review provenance`
5814
+ ]);
5815
+ }
5816
+ const identity = refutationIdentity(record, action.diff_hash);
5817
+ const priorBlockingReviewFindings = record.findings.filter((finding) => finding.source === "review" && finding.kind === "blocking" && finding.review_round !== null && !(finding.status === "refuted" && refutationIsLive(finding, record.attestations, identity)));
5818
+ const admissions = action.findings.map((finding) => {
5819
+ const inherited = finding.anchor != null ? record.findings.find((prior) => prior.source === "review" && prior.status === "refuted" && prior.anchor === finding.anchor && refutationIsLive(prior, record.attestations, identity)) : undefined;
5820
+ return { finding, inherited };
5821
+ });
5822
+ const disputed = admissions.find(({ finding, inherited }) => finding.kind === "blocking" && inherited === undefined && priorBlockingReviewFindings.some((prior) => sharesAcceptanceBoundary(finding, prior)))?.finding;
5823
+ const parkForReplan = authorityAudit.authority_kind === "review" && disputed !== undefined;
5559
5824
  if (!parkForReplan) {
5560
5825
  record.artifact_state = "active";
5561
5826
  record.intent_ref.path = `docs/plans/${record.task_id}.intent.json`;
5562
5827
  }
5563
5828
  const findingIds = new Set(record.findings.map((item) => item.id));
5564
- for (const finding of action.findings) {
5829
+ for (const { finding, inherited } of admissions) {
5565
5830
  if (findingIds.has(finding.id))
5566
5831
  throw new KernelInvariantError([
5567
5832
  `findings contains duplicate id ${finding.id}`
5568
5833
  ]);
5569
5834
  findingIds.add(finding.id);
5570
5835
  record.findings.push({
5571
- ...finding,
5572
- status: "open",
5836
+ id: finding.id,
5837
+ kind: finding.kind,
5838
+ status: inherited ? "refuted" : "open",
5839
+ acceptance_id: finding.acceptance_id,
5573
5840
  source: authorityAudit.authority_kind === "review" ? "review" : "execution",
5574
- review_round: authorityAudit.authority_kind === "review" ? round : null
5841
+ review_round: authorityAudit.authority_kind === "review" ? round : null,
5842
+ summary: finding.summary,
5843
+ ...finding.anchor !== undefined ? { anchor: finding.anchor } : {},
5844
+ ...finding.evidence !== undefined ? { evidence: finding.evidence } : {},
5845
+ counterevidence: inherited ? { ...inherited.counterevidence } : null
5575
5846
  });
5576
5847
  }
5577
5848
  if (parkForReplan && !record.findings.some((item) => item.status === "open" && item.kind === "replan_required")) {
5578
- const disputed = action.findings.find((item) => item.kind === "blocking") ?? action.findings[0];
5579
5849
  const boundary = {
5580
5850
  id: `${action.event_id}:replan-required`,
5581
5851
  kind: "replan_required",
@@ -6047,6 +6317,14 @@ function createCanaryApplication(registry) {
6047
6317
  case "resolve_finding":
6048
6318
  action = { ...base, type: "resolve_finding", finding_id: operation.finding_id };
6049
6319
  break;
6320
+ case "refute_finding":
6321
+ action = {
6322
+ ...base,
6323
+ type: "refute_finding",
6324
+ finding_id: operation.finding_id,
6325
+ attestation_id: operation.attestation_id
6326
+ };
6327
+ break;
6050
6328
  case "request_rework":
6051
6329
  capability = operation.capability;
6052
6330
  action = { ...base, type: "request_rework", findings: operation.findings };
@@ -6173,7 +6451,7 @@ function createCanaryApplication(registry) {
6173
6451
  }
6174
6452
 
6175
6453
  // plugins/immune-brain/runtime/kernel/authority_port.ts
6176
- import { createHash as createHash11 } from "node:crypto";
6454
+ import { createHash as createHash12 } from "node:crypto";
6177
6455
 
6178
6456
  // plugins/immune-brain/runtime/kernel/capability_registry.ts
6179
6457
  function createCapabilityRegistry(capabilityBrand, hooks, domainLabel) {
@@ -6221,7 +6499,7 @@ function createCapabilityRegistry(capabilityBrand, hooks, domainLabel) {
6221
6499
  // plugins/immune-brain/runtime/kernel/authority_port.ts
6222
6500
  function digestOfAction(action) {
6223
6501
  const { expected_record_hash: _r, expected_workspace_hash: _w, diff_hash: _d, ...rest } = action;
6224
- return createHash11("sha256").update(JSON.stringify(rest)).digest("hex");
6502
+ return createHash12("sha256").update(JSON.stringify(rest)).digest("hex");
6225
6503
  }
6226
6504
  function createMutationAuthorityRegistry() {
6227
6505
  const inner = createCapabilityRegistry(MUTATION_AUTHORITY_CAPABILITY_BRAND, {
@@ -6345,7 +6623,7 @@ function createEnrollmentAuthorityRegistry() {
6345
6623
  }
6346
6624
 
6347
6625
  // plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts
6348
- import { createHash as createHash12 } from "node:crypto";
6626
+ import { createHash as createHash13 } from "node:crypto";
6349
6627
  import { spawnSync as spawnSync3 } from "node:child_process";
6350
6628
  import { resolve as resolve6 } from "node:path";
6351
6629
  var SOURCE_PATH = ".imm/state/workspace.json";
@@ -6360,7 +6638,7 @@ function readGitHead(root) {
6360
6638
  return head.toLowerCase();
6361
6639
  }
6362
6640
  function sha256Hex2(bytes) {
6363
- return createHash12("sha256").update(bytes).digest("hex");
6641
+ return createHash13("sha256").update(bytes).digest("hex");
6364
6642
  }
6365
6643
  function stableStringify2(value) {
6366
6644
  if (value === null || typeof value !== "object")
@@ -6683,121 +6961,2302 @@ async function runDeterministicQa(snapshot, descriptors, runner, options = {}) {
6683
6961
  };
6684
6962
  }
6685
6963
 
6686
- // plugins/immune-brain/runtime/claude/kernel_ports.ts
6687
- function diffSnapshotOf(root, record) {
6688
- if (record.contract === "assurance_kernel/task_record/v4") {
6689
- if (!record.git_base_head)
6690
- throw new Error("TaskRecord v4 is missing git_base_head");
6691
- return taskRevisionIdentity(root, record.intent_snapshot.scope_hint, record.git_base_head);
6964
+ // plugins/immune-brain/runtime/unattended/batch_plan.ts
6965
+ import { createHash as createHash14 } from "node:crypto";
6966
+
6967
+ // plugins/immune-brain/runtime/github_issue_tracker.ts
6968
+ import { spawn as spawn2 } from "node:child_process";
6969
+ import { basename as basename2, relative as relative3, resolve as resolve7, sep as sep5 } from "node:path";
6970
+ var CONTRACT = "immune_brain/github_issue_tracker_result/v1";
6971
+ var PROTOCOL_MARKER = "<!-- immune-brain-tracker:v1 -->";
6972
+ var KIND_INITIATIVE_MARKER = "<!-- immune-brain:kind=initiative -->";
6973
+ var KIND_TASK_MARKER = "<!-- immune-brain:kind=task -->";
6974
+ var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
6975
+ var MAX_GH_OUTPUT = 8 * 1024 * 1024;
6976
+ var MAX_DIAGNOSTIC = 512;
6977
+ var GH_TIMEOUT_MS = 20000;
6978
+ var MAX_SNAPSHOT_PAGES = 100;
6979
+ var MAX_TERMINAL_EVENT_ID = 500;
6980
+ function countLiteral(value, needle) {
6981
+ if (!needle)
6982
+ return 0;
6983
+ let count = 0;
6984
+ let index = 0;
6985
+ while ((index = value.indexOf(needle, index)) !== -1) {
6986
+ count += 1;
6987
+ index += needle.length;
6692
6988
  }
6693
- return taskDiffIdentity(root, record.intent_snapshot.scope_hint);
6694
- }
6695
- function diffHashOf(root, record) {
6696
- return diffSnapshotOf(root, record).diff_hash;
6989
+ return count;
6697
6990
  }
6698
- function readTaskIntentForRecord(root, taskId) {
6699
- const currentPath = readTaskRecordRaw(root, taskId).record?.intent_ref?.path;
6700
- return readTaskIntent(root, taskId, currentPath);
6701
- }
6702
- function extractVerdictJson(input) {
6703
- if (typeof input === "string") {
6704
- const cleaned = input.split(`
6705
- `).map((line) => line.trim()).filter((line) => line.startsWith("{") && line.endsWith("}")).join("");
6706
- if (!cleaned)
6707
- return null;
6708
- try {
6709
- return JSON.parse(cleaned);
6710
- } catch {
6711
- return null;
6712
- }
6713
- }
6714
- if (typeof input === "object" && input !== null && !Array.isArray(input))
6715
- return input;
6716
- return null;
6991
+ function marker(name, value) {
6992
+ return `<!-- immune-brain:${name}=${value} -->`;
6717
6993
  }
6718
- function verdictFingerprint(raw) {
6719
- return JSON.stringify({
6720
- contract: raw.contract ?? null,
6721
- role: raw.role ?? null,
6722
- task_id: raw.task_id ?? null,
6723
- snapshot_digest: raw.snapshot_digest ?? null,
6724
- decision: raw.decision ?? null,
6725
- approval: raw.approval ?? null,
6726
- findings: raw.findings ?? null
6727
- });
6994
+ function terminalMarker(eventId) {
6995
+ return `<!-- immune-brain:terminal-event=${eventId} -->`;
6728
6996
  }
6729
- async function submitClaudeReview(host, coordinator, ctx, taskId, verdictInput) {
6730
- if (verdictInput === undefined)
6731
- throw new Error("verdict is required");
6732
- const observed = host.inspectReviewForTask(taskId);
6733
- if (!observed.ok) {
6734
- if (observed.release)
6735
- return coordinator.abandonReview(taskId, observed.reason);
6736
- return { state: "blocked", reason: observed.reason };
6737
- }
6738
- const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
6739
- if (!parentValid)
6740
- return coordinator.submitReview(taskId, ctx, verdictInput);
6741
- const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
6742
- if (!receiptValid) {
6743
- return coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict");
6744
- }
6745
- const parentJson = extractVerdictJson(verdictInput);
6746
- const receiptJson = extractVerdictJson(observed.receipt.result);
6747
- if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
6748
- return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
6749
- }
6750
- return coordinator.submitReview(taskId, ctx, verdictInput);
6997
+ function terminalSuffix(eventId) {
6998
+ return `
6999
+
7000
+ ${terminalMarker(eventId)}
7001
+ Terminal event: \`${eventId}\`
7002
+ `;
6751
7003
  }
6752
- function stopReason(value) {
6753
- return typeof value === "string" && value.length > 0 ? value : "user stop";
7004
+ var MAX_TERMINAL_SUFFIX_BYTES = Buffer.byteLength(terminalSuffix("x".repeat(MAX_TERMINAL_EVENT_ID)), "utf8");
7005
+ function redactSecrets(value) {
7006
+ return value.replace(/\bgh[pousr]_[A-Za-z0-9_]+\b/g, "[REDACTED_GITHUB_TOKEN]").replace(/\bgithub_pat_[A-Za-z0-9_]+\b/g, "[REDACTED_GITHUB_TOKEN]").replace(/\bBearer\s+\S+/gi, "Bearer [REDACTED]").replace(/\b(?:token|secret|password)\s*[=:]\s*\S+/gi, "credential=[REDACTED]");
6754
7007
  }
6755
- function assertProjectionBinding(before, after, allowDiffChange = false) {
6756
- const fields = allowDiffChange ? ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash"] : ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash", "diff_hash"];
6757
- if (before.error || !before.claim || after.error || !after.claim || before.claim.task_id !== after.claim.task_id || fields.some((field) => before.projection[field] !== after.projection[field])) {
6758
- throw new Error("Task changed after native confirmation; authority aborted before capability issuance");
6759
- }
7008
+ function identifier(value, name) {
7009
+ if (typeof value !== "string" || !ID_PATTERN.test(value))
7010
+ throw new Error(`${name} must match ${ID_PATTERN}`);
7011
+ if (/^(?:gh[pousr]_|github_pat_)/i.test(value))
7012
+ throw new Error(`${name} must not contain a token-like value`);
7013
+ return value;
6760
7014
  }
6761
- function qaOutcomes(record) {
6762
- return Object.fromEntries(record.attestations.filter((item) => item.kind === "qa").flatMap((item) => item.acceptance_results).map((result) => [result.acceptance_id, { status: result.status, summary: result.summary }]));
7015
+ function redactGithubDiagnostic(value) {
7016
+ return redactSecrets(value).replace(/\s+/g, " ").trim().slice(0, MAX_DIAGNOSTIC);
6763
7017
  }
6764
- async function ensureClaudeReviewRevision(root, taskId, projection) {
6765
- const current = await readTaskRecord(root, taskId);
6766
- const record = current.record;
6767
- if (!record)
6768
- throw new Error(`task ${taskId} has no TaskRecord`);
6769
- if (current.revision !== projection.projection.record_revision)
6770
- throw new Error("TaskRecord changed before Review revision preparation");
6771
- if (record.contract !== "assurance_kernel/task_record/v4")
6772
- return null;
6773
- if (!record.git_base_head)
6774
- throw new Error("Review revision requires a TaskRecord v4 git_base_head");
6775
- const manifest = captureReviewManifest(root, {
6776
- taskId,
6777
- baseHead: record.git_base_head,
6778
- scopeHint: record.intent_snapshot.scope_hint,
6779
- expectedDiffHash: projection.projection.diff_hash,
6780
- intentRevision: projection.projection.intent_revision,
6781
- intentContentHash: projection.projection.intent_content_hash,
6782
- recordRevision: projection.projection.record_revision,
6783
- workspaceRevision: projection.projection.workspace_revision,
6784
- lifecycle: projection.projection.lifecycle,
6785
- artifactState: projection.projection.artifact_state,
6786
- risk: record.intent_snapshot.risk,
6787
- outcomes: qaOutcomes(record)
6788
- });
7018
+ function result(operation, status, message, issue) {
6789
7019
  return {
6790
- contract: "assurance_kernel/review_revision/v1",
6791
- base_head: manifest.base_head,
6792
- review_tree: manifest.review_tree,
6793
- review_commit: manifest.review_commit,
6794
- review_ref: manifest.review_ref,
6795
- diff_hash: manifest.diff_hash,
6796
- manifest_digest: manifest.manifest_digest
7020
+ contract: CONTRACT,
7021
+ operation,
7022
+ status,
7023
+ association_found: issue !== undefined,
7024
+ ...issue ? { issue_number: issue.number, issue_url: issue.url, node_id: String(issue.id) } : {},
7025
+ message: redactGithubDiagnostic(message)
6797
7026
  };
6798
7027
  }
6799
- async function buildAssuranceSnapshot(root, taskId, role, projection, runner) {
6800
- const read = await readTaskRecord(root, taskId);
7028
+ function ghFailure(operation, execution, message) {
7029
+ const retryable = execution.timed_out || /timeout|timed out|network|connection|temporar|rate limit|502|503|504/i.test(execution.stderr);
7030
+ return result(operation, retryable ? "retryable_failure" : "permanent_failure", `${message}: ${execution.output_exceeded ? "gh output limit exceeded" : execution.stderr || `gh exited ${execution.exit_code}`}`);
7031
+ }
7032
+ function createGhTransport(binary = "gh") {
7033
+ return {
7034
+ run(args, options = {}) {
7035
+ return new Promise((complete) => {
7036
+ let stdout = Buffer.alloc(0);
7037
+ let stderr = Buffer.alloc(0);
7038
+ let timedOut = false;
7039
+ let outputExceeded = false;
7040
+ let timer;
7041
+ let settled = false;
7042
+ const finish = (exitCode, spawnError = "") => {
7043
+ if (settled)
7044
+ return;
7045
+ settled = true;
7046
+ if (timer)
7047
+ clearTimeout(timer);
7048
+ complete({
7049
+ exit_code: exitCode,
7050
+ stdout: stdout.toString("utf8"),
7051
+ stderr: `${stderr.toString("utf8")}${spawnError}`,
7052
+ timed_out: timedOut,
7053
+ output_exceeded: outputExceeded
7054
+ });
7055
+ };
7056
+ let child;
7057
+ try {
7058
+ child = spawn2(binary, args, {
7059
+ cwd: options.cwd,
7060
+ stdio: ["pipe", "pipe", "pipe"],
7061
+ env: process.env
7062
+ });
7063
+ } catch (error) {
7064
+ finish(1, error instanceof Error ? error.message : String(error));
7065
+ return;
7066
+ }
7067
+ const append = (current, chunk) => {
7068
+ const available = Math.max(0, MAX_GH_OUTPUT - stdout.length - stderr.length);
7069
+ if (chunk.length > available) {
7070
+ outputExceeded = true;
7071
+ child.kill("SIGKILL");
7072
+ }
7073
+ return available > 0 ? Buffer.concat([current, chunk.subarray(0, available)]) : current;
7074
+ };
7075
+ const { stdout: childOut, stderr: childErr, stdin: childIn } = child;
7076
+ if (!childOut || !childErr || !childIn) {
7077
+ finish(1, "gh was spawned without the stdio pipes this reader requires");
7078
+ return;
7079
+ }
7080
+ childOut.on("data", (chunk) => {
7081
+ stdout = append(stdout, chunk);
7082
+ });
7083
+ childErr.on("data", (chunk) => {
7084
+ stderr = append(stderr, chunk);
7085
+ });
7086
+ child.once("error", (error) => {
7087
+ finish(1, error.message);
7088
+ });
7089
+ childIn.once("error", (error) => {
7090
+ finish(1, error.message);
7091
+ });
7092
+ timer = setTimeout(() => {
7093
+ timedOut = true;
7094
+ child.kill("SIGKILL");
7095
+ }, GH_TIMEOUT_MS);
7096
+ child.once("close", (code) => {
7097
+ finish(code ?? 1);
7098
+ });
7099
+ try {
7100
+ childIn.end(options.stdin ?? "");
7101
+ } catch (error) {
7102
+ finish(1, error instanceof Error ? error.message : String(error));
7103
+ }
7104
+ });
7105
+ }
7106
+ };
7107
+ }
7108
+ function parseRepository(raw) {
7109
+ const value = JSON.parse(raw);
7110
+ if (!Number.isSafeInteger(value.id) || typeof value.full_name !== "string" || !value.full_name.includes("/"))
7111
+ throw new Error("gh returned malformed repository identity");
7112
+ return { id: value.id, name_with_owner: value.full_name };
7113
+ }
7114
+ function parseIssues(raw) {
7115
+ const parsed = JSON.parse(raw);
7116
+ const pages = Array.isArray(parsed) && parsed.every(Array.isArray) ? parsed.flat() : parsed;
7117
+ if (!Array.isArray(pages))
7118
+ throw new Error("gh returned malformed Issue list");
7119
+ return pages.filter((item) => Boolean(item) && typeof item === "object" && !("pull_request" in item)).map((item) => {
7120
+ if (!Number.isSafeInteger(item.id) || !Number.isSafeInteger(item.number) || typeof item.html_url !== "string" || typeof item.title !== "string" || typeof item.body !== "string" && item.body !== null || item.state !== "open" && item.state !== "closed")
7121
+ throw new Error("gh returned a malformed Issue");
7122
+ return {
7123
+ id: item.id,
7124
+ number: item.number,
7125
+ url: item.html_url,
7126
+ title: item.title,
7127
+ body: typeof item.body === "string" ? item.body : "",
7128
+ state: item.state,
7129
+ state_reason: typeof item.state_reason === "string" ? item.state_reason.toLowerCase() : null
7130
+ };
7131
+ });
7132
+ }
7133
+ function parseSubIssueNumbers(raw) {
7134
+ const parsed = JSON.parse(raw);
7135
+ const pages = Array.isArray(parsed) && parsed.every(Array.isArray) ? parsed.flat() : parsed;
7136
+ if (!Array.isArray(pages))
7137
+ throw new Error("gh returned malformed Sub-issue list");
7138
+ return pages.map((item, index) => {
7139
+ const number = item?.number;
7140
+ if (typeof number !== "number" || !Number.isSafeInteger(number))
7141
+ throw new Error(`gh returned a malformed Sub-issue entry at ${index}`);
7142
+ return number;
7143
+ });
7144
+ }
7145
+ async function snapshot(root, gh, operation) {
7146
+ const repositoryExecution = await gh.run(["api", "repos/{owner}/{repo}"], { cwd: root });
7147
+ if (repositoryExecution.exit_code !== 0 || repositoryExecution.output_exceeded)
7148
+ return ghFailure(operation, repositoryExecution, "cannot resolve GitHub repository");
7149
+ let repository;
7150
+ try {
7151
+ repository = parseRepository(repositoryExecution.stdout);
7152
+ } catch (error) {
7153
+ return result(operation, "permanent_failure", error instanceof Error ? error.message : String(error));
7154
+ }
7155
+ const issues = [];
7156
+ for (let page = 1;page <= MAX_SNAPSHOT_PAGES; page += 1) {
7157
+ const issuesExecution = await gh.run(["api", `repos/${repository.name_with_owner}/issues?state=all&per_page=100&page=${page}`], { cwd: root });
7158
+ if (issuesExecution.exit_code !== 0 || issuesExecution.output_exceeded)
7159
+ return ghFailure(operation, issuesExecution, "cannot query GitHub Issues");
7160
+ let raw;
7161
+ try {
7162
+ raw = JSON.parse(issuesExecution.stdout);
7163
+ } catch (error) {
7164
+ return result(operation, "permanent_failure", error instanceof Error ? error.message : String(error));
7165
+ }
7166
+ if (!Array.isArray(raw))
7167
+ return result(operation, "permanent_failure", "gh returned malformed Issue list");
7168
+ const pageCount = raw.length;
7169
+ try {
7170
+ issues.push(...parseIssues(issuesExecution.stdout));
7171
+ } catch (error) {
7172
+ return result(operation, "permanent_failure", error instanceof Error ? error.message : String(error));
7173
+ }
7174
+ if (pageCount < 100)
7175
+ break;
7176
+ if (page === MAX_SNAPSHOT_PAGES)
7177
+ return result(operation, "permanent_failure", "too many GitHub Issues to snapshot");
7178
+ }
7179
+ return { repository, issues };
7180
+ }
7181
+ function findIssue(issues, primary, required) {
7182
+ const candidates = issues.filter((issue2) => primary.every((needle) => issue2.body.includes(needle)));
7183
+ if (candidates.length === 0)
7184
+ return { kind: "missing" };
7185
+ if (candidates.length !== 1)
7186
+ return { kind: "ambiguous", message: `multiple Issues contain identity marker ${primary[0]}` };
7187
+ const issue = candidates[0];
7188
+ for (const expected of [PROTOCOL_MARKER, ...required]) {
7189
+ if (countLiteral(issue.body, expected) !== 1)
7190
+ return { kind: "ambiguous", message: `Issue #${issue.number} has missing or duplicate identity markers` };
7191
+ }
7192
+ return { kind: "found", issue };
7193
+ }
7194
+ function initiativeLookup(issues, repositoryId, initiativeId) {
7195
+ const initiative = marker("initiative-id", initiativeId);
7196
+ return findIssue(issues, [initiative, KIND_INITIATIVE_MARKER], [marker("repo-id", repositoryId), initiative]);
7197
+ }
7198
+ function ownershipMarkerValue(body, name) {
7199
+ const values = [...body.matchAll(new RegExp(`<!-- immune-brain:${name}=([A-Za-z0-9][A-Za-z0-9._-]{0,127}) -->`, "g"))];
7200
+ return values.length === 1 ? values[0][1] : null;
7201
+ }
7202
+ function taskLookup(issues, repositoryId, taskId) {
7203
+ const task = marker("task-id", taskId);
7204
+ const base = findIssue(issues, [task, KIND_TASK_MARKER], [marker("repo-id", repositoryId), task]);
7205
+ if (base.kind !== "found")
7206
+ return base;
7207
+ for (const name of ["task-id", "initiative-id", "slice-id"]) {
7208
+ if (!ownershipMarkerValue(base.issue.body, name))
7209
+ return { kind: "ambiguous", message: `Issue #${base.issue.number} has missing or duplicate ${name} ownership markers` };
7210
+ }
7211
+ return base;
7212
+ }
7213
+ function ownedTaskLookup(issues, repositoryId, taskId, initiativeId, sliceId) {
7214
+ const base = taskLookup(issues, repositoryId, taskId);
7215
+ if (base.kind !== "found")
7216
+ return base;
7217
+ return base.issue.body.includes(marker("initiative-id", initiativeId)) && base.issue.body.includes(marker("slice-id", sliceId)) ? base : { kind: "ambiguous", message: `Issue #${base.issue.number} belongs to another Initiative or Slice; Task ownership is immutable` };
7218
+ }
7219
+ async function readSubIssueNumbers(root, gh, operation, repository, parentNumber) {
7220
+ const listed = await gh.run(["api", "--paginate", "--slurp", `repos/${repository.name_with_owner}/issues/${parentNumber}/sub_issues?per_page=100`], { cwd: root });
7221
+ if (listed.exit_code !== 0 || listed.output_exceeded)
7222
+ return ghFailure(operation, listed, "cannot read native Sub-issue relations");
7223
+ try {
7224
+ return parseSubIssueNumbers(listed.stdout);
7225
+ } catch (error) {
7226
+ return result(operation, "permanent_failure", error instanceof Error ? error.message : String(error));
7227
+ }
7228
+ }
7229
+ async function readBlockedByIds(root, gh, operation, repository, childNumber) {
7230
+ const listed = await gh.run(["api", "--paginate", "--slurp", `repos/${repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by?per_page=100`], { cwd: root });
7231
+ if (listed.exit_code !== 0 || listed.output_exceeded)
7232
+ return ghFailure(operation, listed, "cannot read native blocked_by relations");
7233
+ try {
7234
+ const pages = JSON.parse(listed.stdout);
7235
+ if (!Array.isArray(pages) || pages.some((page) => !Array.isArray(page)))
7236
+ throw new Error("gh returned malformed blocked_by pages");
7237
+ const ids = pages.flat().map((item, index) => {
7238
+ const id = item?.issue_id ?? item?.id;
7239
+ if (!Number.isSafeInteger(id))
7240
+ throw new Error(`gh returned malformed blocked_by entry at ${index}`);
7241
+ return id;
7242
+ });
7243
+ if (new Set(ids).size !== ids.length)
7244
+ return result(operation, "ambiguous_remote_state", `Issue #${childNumber} has duplicate native blocked_by relations`);
7245
+ return ids;
7246
+ } catch (error) {
7247
+ return result(operation, "permanent_failure", error instanceof Error ? error.message : String(error));
7248
+ }
7249
+ }
7250
+ async function observeGithubInitiative(root, initiativeId, gh = createGhTransport()) {
7251
+ const id = identifier(initiativeId, "initiative_id");
7252
+ const source = await snapshot(resolve7(root), gh, "create-initiative");
7253
+ if ("contract" in source)
7254
+ throw new Error(source.message);
7255
+ const parent = initiativeLookup(source.issues, source.repository.id, id);
7256
+ if (parent.kind === "missing")
7257
+ throw new Error(`Initiative ${id} is not published`);
7258
+ if (parent.kind === "ambiguous")
7259
+ throw new Error(parent.message);
7260
+ const subIssueNumbers = await readSubIssueNumbers(root, gh, "create-initiative", source.repository, parent.issue.number);
7261
+ if (!Array.isArray(subIssueNumbers))
7262
+ throw new Error(subIssueNumbers.message);
7263
+ if (new Set(subIssueNumbers).size !== subIssueNumbers.length)
7264
+ throw new Error(`Initiative ${id} has duplicate native Sub-issue relations`);
7265
+ const tasks = subIssueNumbers.map((issueNumber) => {
7266
+ const matches = source.issues.filter((issue2) => issue2.number === issueNumber);
7267
+ if (matches.length !== 1)
7268
+ throw new Error(`Initiative ${id} references an unreadable Sub-issue #${issueNumber}`);
7269
+ const issue = matches[0];
7270
+ const taskId = ownershipMarkerValue(issue.body, "task-id");
7271
+ const sliceId = ownershipMarkerValue(issue.body, "slice-id");
7272
+ if (!taskId || !sliceId || ownershipMarkerValue(issue.body, "initiative-id") !== id)
7273
+ throw new Error(`Sub-issue #${issueNumber} has invalid Initiative ownership markers`);
7274
+ const owned = ownedTaskLookup(source.issues, source.repository.id, taskId, id, sliceId);
7275
+ if (owned.kind !== "found" || owned.issue.number !== issueNumber)
7276
+ throw new Error(owned.kind === "ambiguous" ? owned.message : `Sub-issue #${issueNumber} has invalid Task ownership`);
7277
+ return { task_id: taskId, slice_id: sliceId, issue_number: issueNumber, issue_id: issue.id };
7278
+ });
7279
+ if (new Set(tasks.map((task) => task.task_id)).size !== tasks.length)
7280
+ throw new Error(`Initiative ${id} has duplicate Task identities`);
7281
+ if (new Set(tasks.map((task) => task.slice_id)).size !== tasks.length)
7282
+ throw new Error(`Initiative ${id} has duplicate Slice identities`);
7283
+ const taskByIssueId = new Map(tasks.map((task) => [task.issue_id, task.task_id]));
7284
+ const observed = [];
7285
+ for (const task of tasks.sort((left, right) => left.task_id < right.task_id ? -1 : left.task_id > right.task_id ? 1 : 0)) {
7286
+ const blockerIds = await readBlockedByIds(root, gh, "create-initiative", source.repository, task.issue_number);
7287
+ if (!Array.isArray(blockerIds))
7288
+ throw new Error(blockerIds.message);
7289
+ const blockedBy = blockerIds.map((blockerId) => {
7290
+ const blocker = taskByIssueId.get(blockerId);
7291
+ if (!blocker)
7292
+ throw new Error(`Task ${task.task_id} depends on an Issue outside Initiative ${id}`);
7293
+ return blocker;
7294
+ }).sort();
7295
+ observed.push({
7296
+ task_id: task.task_id,
7297
+ slice_id: task.slice_id,
7298
+ issue_number: task.issue_number,
7299
+ blocked_by: blockedBy
7300
+ });
7301
+ }
7302
+ return {
7303
+ contract: "immune_brain/github_initiative_observation/v1",
7304
+ initiative_id: id,
7305
+ issue_number: parent.issue.number,
7306
+ tasks: observed
7307
+ };
7308
+ }
7309
+
7310
+ // plugins/immune-brain/runtime/unattended/batch_plan.ts
7311
+ var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
7312
+ var DEFAULT_DEADLINE_MS = 8 * 60 * 60 * 1000;
7313
+ var DEFAULT_QA_FAILURE_LIMIT = 2;
7314
+ function compareIds(left, right) {
7315
+ return left < right ? -1 : left > right ? 1 : 0;
7316
+ }
7317
+ function positiveSafeInteger(value, name) {
7318
+ if (!Number.isSafeInteger(value) || value <= 0)
7319
+ throw new Error(`${name} must be a positive safe integer`);
7320
+ return value;
7321
+ }
7322
+ var ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
7323
+ function timestamp(value, name) {
7324
+ if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN.test(value))
7325
+ throw new Error(`${name} must be an ISO timestamp`);
7326
+ const milliseconds = Date.parse(value);
7327
+ if (!Number.isFinite(milliseconds))
7328
+ throw new Error(`${name} must be an ISO timestamp`);
7329
+ return { milliseconds, iso: new Date(milliseconds).toISOString() };
7330
+ }
7331
+ function normalizeObservation(value, initiativeSlug) {
7332
+ if (!value || value.contract !== "immune_brain/github_initiative_observation/v1")
7333
+ throw new Error("tracker returned an invalid Initiative observation contract");
7334
+ if (value.initiative_id !== initiativeSlug)
7335
+ throw new Error("tracker returned an observation for another Initiative");
7336
+ if (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0)
7337
+ throw new Error("tracker returned an invalid Initiative Issue number");
7338
+ if (!Array.isArray(value.tasks))
7339
+ throw new Error("tracker returned an invalid Initiative Task list");
7340
+ const taskIds = new Set;
7341
+ const sliceIds = new Set;
7342
+ const tasks = value.tasks.map((task, index) => {
7343
+ if (!task || typeof task !== "object")
7344
+ throw new Error(`tracker Task ${index} is invalid`);
7345
+ if (!ID_PATTERN2.test(task.task_id))
7346
+ throw new Error(`tracker Task ${index} has an invalid task_id`);
7347
+ if (!ID_PATTERN2.test(task.slice_id))
7348
+ throw new Error(`tracker Task ${index} has an invalid slice_id`);
7349
+ if (!Number.isSafeInteger(task.issue_number) || task.issue_number <= 0)
7350
+ throw new Error(`tracker Task ${task.task_id} has an invalid Issue number`);
7351
+ if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN2.test(id)))
7352
+ throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
7353
+ if (taskIds.has(task.task_id))
7354
+ throw new Error(`tracker returned duplicate Task ${task.task_id}`);
7355
+ if (sliceIds.has(task.slice_id))
7356
+ throw new Error(`tracker returned duplicate Slice ${task.slice_id}`);
7357
+ taskIds.add(task.task_id);
7358
+ sliceIds.add(task.slice_id);
7359
+ const blockedBy = [...task.blocked_by].sort();
7360
+ if (new Set(blockedBy).size !== blockedBy.length || blockedBy.includes(task.task_id))
7361
+ throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
7362
+ return { ...task, blocked_by: blockedBy };
7363
+ });
7364
+ for (const task of tasks) {
7365
+ const unknown = task.blocked_by.find((id) => !taskIds.has(id));
7366
+ if (unknown)
7367
+ throw new Error(`tracker Task ${task.task_id} depends on unknown Task ${unknown}`);
7368
+ }
7369
+ return { ...value, tasks: tasks.sort((left, right) => compareIds(left.task_id, right.task_id)) };
7370
+ }
7371
+ function dependencyOrder(observation) {
7372
+ const remaining = new Map(observation.tasks.map((task) => [task.task_id, task]));
7373
+ const done = new Set;
7374
+ const order = [];
7375
+ while (remaining.size) {
7376
+ const ready = [...remaining.values()].filter((task) => task.blocked_by.every((id) => done.has(id))).sort((left, right) => compareIds(left.task_id, right.task_id));
7377
+ if (!ready.length)
7378
+ throw new Error("Initiative Task dependencies must form an acyclic graph");
7379
+ for (const task of ready) {
7380
+ remaining.delete(task.task_id);
7381
+ done.add(task.task_id);
7382
+ order.push(task);
7383
+ }
7384
+ }
7385
+ return order;
7386
+ }
7387
+ function dependencyClosures(order) {
7388
+ const closures = new Map;
7389
+ for (const task of order) {
7390
+ const closure = new Set;
7391
+ for (const dependency of task.blocked_by) {
7392
+ closure.add(dependency);
7393
+ for (const transitive of closures.get(dependency) ?? [])
7394
+ closure.add(transitive);
7395
+ }
7396
+ closures.set(task.task_id, [...closure].sort());
7397
+ }
7398
+ return closures;
7399
+ }
7400
+ function budget(input, enrollableCount, confirmationTime) {
7401
+ const maxChildren = input.budget?.max_children === undefined ? positiveSafeInteger(enrollableCount, "budget.max_children") : positiveSafeInteger(input.budget.max_children, "budget.max_children");
7402
+ if (maxChildren > enrollableCount)
7403
+ throw new Error("budget.max_children exceeds the enrollable Child count");
7404
+ const qaFailureLimit = input.budget?.qa_failure_limit === undefined ? DEFAULT_QA_FAILURE_LIMIT : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
7405
+ const deadline = input.budget?.deadline_at === undefined ? new Date(confirmationTime.milliseconds + DEFAULT_DEADLINE_MS).toISOString() : timestamp(input.budget.deadline_at, "budget.deadline_at").iso;
7406
+ if (Date.parse(deadline) <= confirmationTime.milliseconds)
7407
+ throw new Error("budget.deadline_at must be later than confirmation_time");
7408
+ return { max_children: maxChildren, deadline_at: deadline, qa_failure_limit: qaFailureLimit };
7409
+ }
7410
+ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeGithubInitiative) {
7411
+ if (!ID_PATTERN2.test(initiativeSlug))
7412
+ throw new Error("initiative_slug is invalid");
7413
+ if (!input || typeof input !== "object")
7414
+ throw new Error("batch plan input is required");
7415
+ const confirmationTime = timestamp(input.confirmation_time, "confirmation_time");
7416
+ const trackerObservation = normalizeObservation(await readInitiative(root, initiativeSlug), initiativeSlug);
7417
+ const order = dependencyOrder(trackerObservation);
7418
+ const closures = dependencyClosures(order);
7419
+ const children = [];
7420
+ for (const task of order) {
7421
+ const blockedBy = closures.get(task.task_id);
7422
+ const child = { task_id: task.task_id, slice_id: task.slice_id, blocked_by: blockedBy };
7423
+ const tombstone = readTaskTombstone(root, task.task_id);
7424
+ const record = readTaskRecordRaw(root, task.task_id).record;
7425
+ if (tombstone && record)
7426
+ throw new Error(`Task ${task.task_id} has conflicting settled and active state`);
7427
+ if (tombstone) {
7428
+ children.push({ ...child, status: "already_settled", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
7429
+ continue;
7430
+ }
7431
+ if (record) {
7432
+ children.push({ ...child, status: "already_owned", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
7433
+ continue;
7434
+ }
7435
+ const intentPath = `docs/plans/${task.task_id}.intent.json`;
7436
+ try {
7437
+ const read = observeTaskIntent(root, task.task_id, intentPath);
7438
+ children.push({
7439
+ ...child,
7440
+ status: read.intent.risk === "critical" ? "needs_human" : "enrollable",
7441
+ reason: read.intent.risk === "critical" ? "critical" : null,
7442
+ intent_path: read.intent_ref.path,
7443
+ intent_revision: read.intent_ref.revision,
7444
+ intent_content_hash: read.content_hash
7445
+ });
7446
+ } catch (error) {
7447
+ if (!(error instanceof TaskIntentObservationError))
7448
+ throw error;
7449
+ children.push({ ...child, status: "needs_human", reason: "invalid_intent", intent_path: null, intent_revision: null, intent_content_hash: null });
7450
+ }
7451
+ }
7452
+ const directDependencies = new Map(order.map((task) => [task.task_id, task.blocked_by]));
7453
+ const childById = new Map(children.map((child) => [child.task_id, child]));
7454
+ for (const child of children) {
7455
+ if (child.status !== "enrollable")
7456
+ continue;
7457
+ if (directDependencies.get(child.task_id).some((id) => {
7458
+ const status = childById.get(id).status;
7459
+ return status === "already_owned" || status === "needs_human" || status === "blocked";
7460
+ })) {
7461
+ child.status = "blocked";
7462
+ child.reason = "dependency_unavailable";
7463
+ }
7464
+ }
7465
+ const enrollable = children.filter((child) => child.status === "enrollable").map((child) => ({
7466
+ task_id: child.task_id,
7467
+ intent_path: child.intent_path,
7468
+ intent_revision: child.intent_revision,
7469
+ intent_content_hash: child.intent_content_hash,
7470
+ blocked_by: child.blocked_by.filter((id) => childById.get(id)?.status === "enrollable")
7471
+ }));
7472
+ if (!enrollable.length)
7473
+ throw new Error("batch plan has no enrollable children; nothing to confirm");
7474
+ const planDigest = `sha256:${createHash14("sha256").update(stableStringify(enrollable)).digest("hex")}`;
7475
+ return {
7476
+ contract: "assurance_kernel/batch_plan/v1",
7477
+ initiative_slug: initiativeSlug,
7478
+ confirmation_time: confirmationTime.iso,
7479
+ tracker_observation: trackerObservation,
7480
+ children,
7481
+ enrollable,
7482
+ plan_digest: planDigest,
7483
+ budget: budget(input, enrollable.length, confirmationTime)
7484
+ };
7485
+ }
7486
+
7487
+ // plugins/immune-brain/runtime/unattended/batch_runner.ts
7488
+ import { spawnSync as spawnSync5 } from "node:child_process";
7489
+ import { existsSync as existsSync5 } from "node:fs";
7490
+ import { join as join9 } from "node:path";
7491
+
7492
+ // plugins/immune-brain/runtime/kernel/batch_authority.ts
7493
+ import { createHash as createHash15 } from "node:crypto";
7494
+ var BATCH_AUTHORITY_CAPABILITY_BRAND = Symbol.for("assurance-kernel.batch-authority-capability-brand");
7495
+ var GIT_COMMIT_ID2 = /^[a-f0-9]{40}$/;
7496
+
7497
+ class BatchAuthorizationExpiryError extends Error {
7498
+ constructor(message) {
7499
+ super(message);
7500
+ this.name = "BatchAuthorizationExpiryError";
7501
+ }
7502
+ }
7503
+ function sha256Hex3(bytes) {
7504
+ return createHash15("sha256").update(bytes).digest("hex");
7505
+ }
7506
+ function stableStringify3(value) {
7507
+ if (value === null || typeof value !== "object")
7508
+ return JSON.stringify(value);
7509
+ if (Array.isArray(value))
7510
+ return `[${value.map((entry) => stableStringify3(entry)).join(",")}]`;
7511
+ const record = value;
7512
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify3(record[key])}`).join(",")}}`;
7513
+ }
7514
+ function computeBatchPlanDigest(children) {
7515
+ const canonical = children.map((child) => ({
7516
+ blocked_by: [...child.blocked_by],
7517
+ intent_content_hash: child.intent_content_hash,
7518
+ intent_path: child.intent_path,
7519
+ intent_revision: child.intent_revision,
7520
+ task_id: child.task_id
7521
+ }));
7522
+ return `sha256:${sha256Hex3(stableStringify3(canonical))}`;
7523
+ }
7524
+ function requireNonEmpty(binding) {
7525
+ const missing = [];
7526
+ for (const key of [
7527
+ "batch_id",
7528
+ "initiative_slug",
7529
+ "plan_digest",
7530
+ "branch",
7531
+ "base_head",
7532
+ "actor_id",
7533
+ "confirmation_ref",
7534
+ "expires_at",
7535
+ "nonce"
7536
+ ]) {
7537
+ const value = binding[key];
7538
+ if (value === undefined || value === null || value === "")
7539
+ missing.push(key);
7540
+ }
7541
+ if (!binding.budget || typeof binding.budget !== "object")
7542
+ missing.push("budget");
7543
+ if (missing.length > 0)
7544
+ throw new Error(`batch authorization binding is incomplete: ${missing.join(", ")}`);
7545
+ }
7546
+ function validateBudget(budget2, issuedAt) {
7547
+ if (!Number.isInteger(budget2.max_children) || budget2.max_children <= 0)
7548
+ throw new Error("batch budget max_children must be a positive integer");
7549
+ if (!Number.isInteger(budget2.qa_failure_limit) || budget2.qa_failure_limit <= 0)
7550
+ throw new Error("batch budget qa_failure_limit must be a positive integer");
7551
+ const deadline = Date.parse(budget2.deadline_at);
7552
+ if (Number.isNaN(deadline) || deadline <= Date.parse(issuedAt))
7553
+ throw new Error("batch budget must have a future deadline_at");
7554
+ }
7555
+ function validateChildren(children, planDigest) {
7556
+ if (!Array.isArray(children) || children.length === 0)
7557
+ throw new Error("batch authorization requires a non-empty child plan");
7558
+ const seen = new Set;
7559
+ for (const child of children) {
7560
+ if (!child || typeof child !== "object")
7561
+ throw new Error("batch plan child must be an object");
7562
+ for (const key of ["task_id", "intent_path", "intent_content_hash"]) {
7563
+ if (typeof child[key] !== "string" || child[key] === "")
7564
+ throw new Error(`batch plan child ${key} must be a non-empty string`);
7565
+ }
7566
+ if (!Number.isInteger(child.intent_revision) || child.intent_revision <= 0)
7567
+ throw new Error("batch plan child intent_revision must be a positive integer");
7568
+ if (!Array.isArray(child.blocked_by) || child.blocked_by.some((id) => typeof id !== "string" || id === ""))
7569
+ throw new Error("batch plan child blocked_by must be an array of task ids");
7570
+ if (seen.has(child.task_id))
7571
+ throw new Error(`batch plan child ${child.task_id} appears more than once`);
7572
+ seen.add(child.task_id);
7573
+ }
7574
+ for (const child of children) {
7575
+ for (const blocker of child.blocked_by) {
7576
+ if (!seen.has(blocker))
7577
+ throw new Error(`batch plan child ${child.task_id} is blocked by ${blocker}, which is not in the confirmed plan`);
7578
+ }
7579
+ }
7580
+ const dependencies = new Map(children.map((child) => [child.task_id, child.blocked_by]));
7581
+ const visiting = new Set;
7582
+ const visited = new Set;
7583
+ function visit(taskId) {
7584
+ if (visiting.has(taskId))
7585
+ throw new Error(`batch plan dependency cycle at ${taskId}`);
7586
+ if (visited.has(taskId))
7587
+ return;
7588
+ visiting.add(taskId);
7589
+ for (const blocker of dependencies.get(taskId))
7590
+ visit(blocker);
7591
+ visiting.delete(taskId);
7592
+ visited.add(taskId);
7593
+ }
7594
+ for (const child of children)
7595
+ visit(child.task_id);
7596
+ if (computeBatchPlanDigest(children) !== planDigest)
7597
+ throw new Error("batch plan digest does not match the confirmed child plan");
7598
+ }
7599
+ function createBatchAuthorityRegistry() {
7600
+ const inner = createCapabilityRegistry(BATCH_AUTHORITY_CAPABILITY_BRAND, {
7601
+ validateBinding(binding, issuedAt) {
7602
+ requireNonEmpty(binding);
7603
+ if (binding.actor_id !== "user")
7604
+ throw new Error("batch authorization requires a literal-user actor_id");
7605
+ if (!GIT_COMMIT_ID2.test(binding.base_head))
7606
+ throw new Error("batch authorization base_head must be a committed 40-hex commit id");
7607
+ const expires = Date.parse(binding.expires_at);
7608
+ if (Number.isNaN(expires) || expires <= Date.parse(issuedAt))
7609
+ throw new Error("batch authorization must have a future expiry");
7610
+ validateBudget(binding.budget, issuedAt);
7611
+ },
7612
+ validateAndProject(state, expected, now) {
7613
+ if (!Number.isFinite(now))
7614
+ throw new Error("batch authorization requires a valid clock");
7615
+ const expires = Date.parse(state.expires_at);
7616
+ if (Number.isNaN(expires) || expires <= now)
7617
+ throw new BatchAuthorizationExpiryError("batch authorization has expired");
7618
+ if (Date.parse(state.budget.deadline_at) <= now)
7619
+ throw new BatchAuthorizationExpiryError("batch authorization deadline has expired");
7620
+ for (const key of Object.keys(expected)) {
7621
+ if (key === "budget") {
7622
+ const a = state.budget ?? {};
7623
+ const b = expected.budget ?? {};
7624
+ if (a.max_children !== b.max_children || a.deadline_at !== b.deadline_at || a.qa_failure_limit !== b.qa_failure_limit)
7625
+ throw new Error("batch authorization budget mismatch");
7626
+ continue;
7627
+ }
7628
+ if (state[key] !== expected[key])
7629
+ throw new Error(`batch authorization ${key} mismatch`);
7630
+ }
7631
+ return {
7632
+ batch_id: state.batch_id,
7633
+ initiative_slug: state.initiative_slug,
7634
+ plan_digest: state.plan_digest,
7635
+ branch: state.branch,
7636
+ base_head: state.base_head,
7637
+ budget: { ...state.budget },
7638
+ actor_id: state.actor_id,
7639
+ confirmation_ref: state.confirmation_ref,
7640
+ issued_at: state.issued_at,
7641
+ expires_at: state.expires_at,
7642
+ nonce: state.nonce
7643
+ };
7644
+ }
7645
+ }, "batch authorization");
7646
+ const plans = new WeakMap;
7647
+ const consumed = new WeakMap;
7648
+ function planOf(capability) {
7649
+ const plan = plans.get(capability);
7650
+ if (!plan)
7651
+ throw new Error("batch authorization capability is not recognized by this registry");
7652
+ return plan;
7653
+ }
7654
+ function slotsOf(capability) {
7655
+ const slots = consumed.get(capability);
7656
+ if (!slots)
7657
+ throw new Error("batch authorization capability is not recognized by this registry");
7658
+ return slots;
7659
+ }
7660
+ return {
7661
+ brand: inner.brand,
7662
+ issue(binding, children, issuedAt = new Date().toISOString()) {
7663
+ requireNonEmpty(binding);
7664
+ validateChildren(children, binding.plan_digest);
7665
+ const capability = inner.issue(binding, issuedAt);
7666
+ plans.set(capability, children.map((child) => ({ ...child, blocked_by: [...child.blocked_by] })));
7667
+ consumed.set(capability, new Set);
7668
+ return capability;
7669
+ },
7670
+ inspect(capability, expected, now = Date.now()) {
7671
+ planOf(capability);
7672
+ return inner.inspect(capability, expected, now);
7673
+ },
7674
+ children(capability) {
7675
+ return planOf(capability).map((child) => ({ ...child, blocked_by: [...child.blocked_by] }));
7676
+ },
7677
+ consumedChildren(capability) {
7678
+ return [...slotsOf(capability)];
7679
+ },
7680
+ isChildConsumed(capability, taskId) {
7681
+ return slotsOf(capability).has(taskId);
7682
+ },
7683
+ consumeChild(capability, expected, taskId, now = Date.now()) {
7684
+ const validated = this.inspect(capability, expected, now);
7685
+ const plan = planOf(capability);
7686
+ if (!plan.some((child) => child.task_id === taskId))
7687
+ throw new Error(`batch_child_not_in_plan: ${taskId}`);
7688
+ const slots = slotsOf(capability);
7689
+ if (slots.has(taskId))
7690
+ throw new Error(`batch_child_slot_consumed: ${taskId}`);
7691
+ slots.add(taskId);
7692
+ return validated;
7693
+ },
7694
+ releaseChild(capability, taskId) {
7695
+ slotsOf(capability).delete(taskId);
7696
+ },
7697
+ isExhausted(capability) {
7698
+ return slotsOf(capability).size >= planOf(capability).length;
7699
+ }
7700
+ };
7701
+ }
7702
+ function deriveChildEnrollment(root, registry, input) {
7703
+ const validated = registry.inspect(input.capability, input.binding, Date.parse(input.now));
7704
+ const child = registry.children(input.capability).find((entry) => entry.task_id === input.task_id);
7705
+ if (!child)
7706
+ throw new Error(`batch_child_not_in_plan: ${input.task_id}`);
7707
+ if (registry.isChildConsumed(input.capability, input.task_id))
7708
+ throw new Error(`batch_child_slot_consumed: ${input.task_id}`);
7709
+ if (!GIT_COMMIT_ID2.test(input.expected_head))
7710
+ throw new Error("batch_head_lineage_broken: expected_head is not a commit id");
7711
+ const preparation = preparePiCanary(root, { task_id: input.task_id, now: input.now });
7712
+ if (!preparation.git_base_head)
7713
+ throw new Error(preparation.git_error ?? "enrollment requires a committed Git HEAD");
7714
+ if (preparation.git_base_head !== input.expected_head)
7715
+ throw new Error(`batch_head_lineage_broken: expected ${input.expected_head}, found ${preparation.git_base_head}`);
7716
+ if (!preparation.intent)
7717
+ throw new Error(`batch_child_intent_changed: ${input.task_id} intent sidecar is unreadable`);
7718
+ if (preparation.intent.path !== child.intent_path || preparation.intent.revision !== child.intent_revision || preparation.intent.content_hash !== child.intent_content_hash)
7719
+ throw new Error(`batch_child_intent_changed: ${input.task_id}`);
7720
+ if (registry.consumedChildren(input.capability).length === 0 && input.expected_head !== validated.base_head)
7721
+ throw new Error(`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${validated.base_head}, not ${input.expected_head}`);
7722
+ return {
7723
+ child,
7724
+ preparation,
7725
+ binding: {
7726
+ task_id: child.task_id,
7727
+ intent_path: child.intent_path,
7728
+ intent_revision: child.intent_revision,
7729
+ intent_content_hash: child.intent_content_hash,
7730
+ preparation_digest: preparation.digest,
7731
+ actor_id: validated.actor_id,
7732
+ confirmation_ref: validated.confirmation_ref,
7733
+ expires_at: validated.expires_at,
7734
+ nonce: `${validated.nonce}:${child.task_id}`
7735
+ }
7736
+ };
7737
+ }
7738
+
7739
+ // plugins/immune-brain/runtime/unattended/batch_git.ts
7740
+ import { spawnSync as spawnSync4 } from "node:child_process";
7741
+ import { randomUUID as randomUUID5 } from "node:crypto";
7742
+ import {
7743
+ constants as constants3,
7744
+ closeSync as closeSync4,
7745
+ existsSync as existsSync3,
7746
+ lstatSync as lstatSync6,
7747
+ mkdirSync as mkdirSync3,
7748
+ openSync as openSync4,
7749
+ realpathSync as realpathSync7,
7750
+ renameSync as renameSync2,
7751
+ rmSync as rmSync4,
7752
+ writeFileSync as writeFileSync3
7753
+ } from "node:fs";
7754
+ import { dirname as dirname4, join as join7 } from "node:path";
7755
+ var DEFAULT_GIT_ENV = {
7756
+ ...process.env,
7757
+ GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME || "Immune-Brain Batch",
7758
+ GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL || "immune-brain@local",
7759
+ GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME || "Immune-Brain Batch",
7760
+ GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL || "immune-brain@local"
7761
+ };
7762
+ function runBatchGitPreflight(input) {
7763
+ const { root, initiative_slug: initiativeSlug, base_head: baseHead } = input;
7764
+ const branch = `imm/${initiativeSlug}`;
7765
+ const toplevelResult = spawnSync4("git", ["-C", root, "rev-parse", "--show-toplevel"], {
7766
+ encoding: "utf8",
7767
+ stdio: ["ignore", "pipe", "pipe"]
7768
+ });
7769
+ if (toplevelResult.status !== 0 || !toplevelResult.stdout.trim()) {
7770
+ return {
7771
+ ok: false,
7772
+ reason: "not_a_git_repository",
7773
+ message: "root must be a Git repository with a committed HEAD"
7774
+ };
7775
+ }
7776
+ let realToplevel;
7777
+ let realRoot;
7778
+ try {
7779
+ realToplevel = realpathSync7(toplevelResult.stdout.trim());
7780
+ realRoot = realpathSync7(root);
7781
+ } catch {
7782
+ return {
7783
+ ok: false,
7784
+ reason: "not_repository_root",
7785
+ message: "failed to resolve repository root"
7786
+ };
7787
+ }
7788
+ if (realToplevel !== realRoot) {
7789
+ return {
7790
+ ok: false,
7791
+ reason: "not_repository_root",
7792
+ message: "batch root must be the top-level repository root, not a subdirectory"
7793
+ };
7794
+ }
7795
+ const flaggedPreflight = getUnsupportedIndexFlags(root);
7796
+ if (flaggedPreflight.length > 0) {
7797
+ return {
7798
+ ok: false,
7799
+ reason: "dirty_working_tree",
7800
+ message: `unsupported index flags (assume-unchanged/skip-worktree) detected: ${flaggedPreflight.join(", ")}`
7801
+ };
7802
+ }
7803
+ const statusResult = spawnSync4("git", ["-C", root, "status", "--porcelain", "--untracked-files=all", "--ignore-submodules=none"], {
7804
+ encoding: "utf8",
7805
+ stdio: ["ignore", "pipe", "pipe"]
7806
+ });
7807
+ if (statusResult.status !== 0) {
7808
+ return {
7809
+ ok: false,
7810
+ reason: "dirty_working_tree",
7811
+ message: statusResult.stderr?.trim() || "failed to inspect working tree status"
7812
+ };
7813
+ }
7814
+ if (statusResult.stdout.trim().length > 0) {
7815
+ return {
7816
+ ok: false,
7817
+ reason: "dirty_working_tree",
7818
+ message: "working tree is dirty before batch preflight"
7819
+ };
7820
+ }
7821
+ const headResult = spawnSync4("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
7822
+ encoding: "utf8",
7823
+ stdio: ["ignore", "pipe", "pipe"]
7824
+ });
7825
+ if (headResult.status !== 0 || !headResult.stdout.trim()) {
7826
+ return {
7827
+ ok: false,
7828
+ reason: "uncommitted_head",
7829
+ message: "HEAD is uncommitted or not a valid commit"
7830
+ };
7831
+ }
7832
+ const branchCheck = spawnSync4("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { stdio: ["ignore", "ignore", "ignore"] });
7833
+ if (branchCheck.status === 0) {
7834
+ return {
7835
+ ok: false,
7836
+ reason: "batch_branch_exists",
7837
+ message: `branch refs/heads/${branch} already exists`
7838
+ };
7839
+ }
7840
+ const originalBranchResult = spawnSync4("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
7841
+ encoding: "utf8",
7842
+ stdio: ["ignore", "pipe", "pipe"]
7843
+ });
7844
+ const originalBranch = originalBranchResult.stdout.trim();
7845
+ const checkoutResult = spawnSync4("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", "-b", branch, baseHead], {
7846
+ encoding: "utf8",
7847
+ stdio: ["ignore", "pipe", "pipe"],
7848
+ env: DEFAULT_GIT_ENV
7849
+ });
7850
+ if (checkoutResult.status !== 0) {
7851
+ const stderr = checkoutResult.stderr?.trim() || "";
7852
+ const branchExists = stderr.includes("already exists") || spawnSync4("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
7853
+ const currentBranchCheck = spawnSync4("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
7854
+ encoding: "utf8",
7855
+ stdio: ["ignore", "pipe", "pipe"]
7856
+ }).stdout.trim();
7857
+ if (currentBranchCheck === branch && originalBranch && originalBranch !== branch) {
7858
+ spawnSync4("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", originalBranch], {
7859
+ stdio: ["ignore", "ignore", "ignore"]
7860
+ });
7861
+ }
7862
+ if (branchExists) {
7863
+ return {
7864
+ ok: false,
7865
+ reason: "batch_branch_exists",
7866
+ message: `branch refs/heads/${branch} already exists`
7867
+ };
7868
+ }
7869
+ return {
7870
+ ok: false,
7871
+ reason: "branch_creation_failed",
7872
+ message: stderr || `failed to checkout -b ${branch} ${baseHead}`
7873
+ };
7874
+ }
7875
+ return { ok: true, branch };
7876
+ }
7877
+ function hasBoundaryWhitespace(path) {
7878
+ return path.split("/").some((segment) => segment.trim() !== segment || segment.length === 0);
7879
+ }
7880
+ function getUnsupportedIndexFlags(root) {
7881
+ const result2 = spawnSync4("git", ["-C", root, "ls-files", "-v", "-z", "--"], {
7882
+ encoding: "utf8",
7883
+ stdio: ["ignore", "pipe", "pipe"]
7884
+ });
7885
+ if (result2.status !== 0) {
7886
+ throw new Error("failed to inspect index flags via git ls-files -v");
7887
+ }
7888
+ const entries = result2.stdout.split("\x00").filter((e) => e.length > 0);
7889
+ const flagged = [];
7890
+ for (const entry of entries) {
7891
+ const tag = entry[0];
7892
+ if (tag === "h" || tag === "S" || tag === "s") {
7893
+ flagged.push(entry.slice(2));
7894
+ }
7895
+ }
7896
+ return flagged;
7897
+ }
7898
+ function isPathAllowedForChild(path, taskId, scopeHint) {
7899
+ if (hasBoundaryWhitespace(path))
7900
+ return false;
7901
+ if (path.includes("\\"))
7902
+ return false;
7903
+ const normalized = path.replace(/^\.\//, "");
7904
+ if (normalized === `.imm/audit/${taskId}` || normalized.startsWith(`.imm/audit/${taskId}/`)) {
7905
+ return true;
7906
+ }
7907
+ return scopeHint.some((scopePath) => {
7908
+ if (scopePath.includes("*") || scopePath.includes("?")) {
7909
+ return pathMatchesScope(normalized, scopePath);
7910
+ }
7911
+ return normalized === scopePath || normalized.startsWith(`${scopePath}/`);
7912
+ });
7913
+ }
7914
+ function getChangedProjectPaths(root) {
7915
+ const tracked = spawnSync4("git", ["-C", root, "diff-index", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
7916
+ encoding: "utf8",
7917
+ stdio: ["ignore", "pipe", "pipe"]
7918
+ });
7919
+ if (tracked.status !== 0)
7920
+ throw new Error("failed to inspect tracked diff vs HEAD");
7921
+ const untracked = spawnSync4("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
7922
+ encoding: "utf8",
7923
+ stdio: ["ignore", "pipe", "pipe"]
7924
+ });
7925
+ if (untracked.status !== 0)
7926
+ throw new Error("failed to inspect untracked files");
7927
+ const splitZ = (s) => s.split("\x00").filter((p) => p.length > 0);
7928
+ return [...new Set([...splitZ(tracked.stdout), ...splitZ(untracked.stdout)])];
7929
+ }
7930
+ function getStagedProjectPaths(root) {
7931
+ const staged = spawnSync4("git", ["-C", root, "diff-index", "--cached", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
7932
+ encoding: "utf8",
7933
+ stdio: ["ignore", "pipe", "pipe"]
7934
+ });
7935
+ if (staged.status !== 0)
7936
+ throw new Error("failed to inspect staged diff vs HEAD");
7937
+ return staged.stdout.split("\x00").filter((p) => p.length > 0);
7938
+ }
7939
+ function getUnstagedProjectPaths(root) {
7940
+ const diffFiles = spawnSync4("git", ["-C", root, "diff-files", "--name-only", "-z", "--ignore-submodules=none", "--"], {
7941
+ encoding: "utf8",
7942
+ stdio: ["ignore", "pipe", "pipe"]
7943
+ });
7944
+ if (diffFiles.status !== 0)
7945
+ throw new Error("failed to inspect unstaged tracked changes");
7946
+ const untracked = spawnSync4("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
7947
+ encoding: "utf8",
7948
+ stdio: ["ignore", "pipe", "pipe"]
7949
+ });
7950
+ if (untracked.status !== 0)
7951
+ throw new Error("failed to inspect untracked files");
7952
+ const splitZ = (s) => s.split("\x00").filter((p) => p.length > 0);
7953
+ return [...new Set([...splitZ(diffFiles.stdout), ...splitZ(untracked.stdout)])];
7954
+ }
7955
+ function getCommittedDeltaPaths(root) {
7956
+ const delta = spawnSync4("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", "HEAD~1", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
7957
+ if (delta.status !== 0)
7958
+ throw new Error("failed to inspect committed tree delta");
7959
+ return delta.stdout.split("\x00").filter((p) => p.length > 0);
7960
+ }
7961
+ function commitEvidencePath(batchId, taskId) {
7962
+ return join7(".imm", "state", "batches", "commits", `${batchId}-${taskId}.json`);
7963
+ }
7964
+ function ensureSecureDirectory2(root, relativePath) {
7965
+ const segments = relativePath.split("/").filter(Boolean);
7966
+ let current = root;
7967
+ for (const segment of segments) {
7968
+ current = join7(current, segment);
7969
+ if (existsSync3(current)) {
7970
+ const stats = lstatSync6(current);
7971
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
7972
+ throw new Error(`${segment} exists but is not a real directory`);
7973
+ }
7974
+ } else {
7975
+ mkdirSync3(current);
7976
+ }
7977
+ }
7978
+ return current;
7979
+ }
7980
+ function writeFileAtomically(root, relativePath, bytes) {
7981
+ const target = join7(root, relativePath);
7982
+ const targetDir = dirname4(target);
7983
+ ensureSecureDirectory2(root, dirname4(relativePath));
7984
+ const stats = lstatSync6(targetDir);
7985
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
7986
+ throw new Error(`${dirname4(relativePath)} is not a real directory`);
7987
+ }
7988
+ if (existsSync3(target)) {
7989
+ const targetStats = lstatSync6(target);
7990
+ if (targetStats.isSymbolicLink()) {
7991
+ throw new Error(`${relativePath} is a symlink`);
7992
+ }
7993
+ }
7994
+ const tempPath = `${target}.${randomUUID5()}.tmp`;
7995
+ let fd = null;
7996
+ try {
7997
+ fd = openSync4(tempPath, constants3.O_WRONLY | constants3.O_CREAT | constants3.O_EXCL, 384);
7998
+ writeFileSync3(fd, bytes, "utf8");
7999
+ closeSync4(fd);
8000
+ fd = null;
8001
+ renameSync2(tempPath, target);
8002
+ } finally {
8003
+ if (fd !== null)
8004
+ closeSync4(fd);
8005
+ if (existsSync3(tempPath)) {
8006
+ try {
8007
+ rmSync4(tempPath);
8008
+ } catch {}
8009
+ }
8010
+ }
8011
+ }
8012
+ function writeBatchCommitEvidence(root, evidence) {
8013
+ const path = commitEvidencePath(evidence.batchId, evidence.taskId);
8014
+ const payload = {
8015
+ contract: "assurance_kernel/batch_commit_evidence/v1",
8016
+ batch_id: evidence.batchId,
8017
+ task_id: evidence.taskId,
8018
+ commit: evidence.commit,
8019
+ parent_head: evidence.parentHead,
8020
+ created_at: new Date().toISOString()
8021
+ };
8022
+ writeFileAtomically(root, path, `${JSON.stringify(payload, null, 2)}
8023
+ `);
8024
+ }
8025
+ function readBatchCommitEvidence(root, batchId, taskId) {
8026
+ const path = commitEvidencePath(batchId, taskId);
8027
+ const fullPath = join7(root, path);
8028
+ if (!existsSync3(fullPath))
8029
+ return null;
8030
+ try {
8031
+ const content = readSecureProjectFile(root, path);
8032
+ const parsed = JSON.parse(content);
8033
+ if (parsed.contract === "assurance_kernel/batch_commit_evidence/v1" && parsed.batch_id === batchId && parsed.task_id === taskId && typeof parsed.commit === "string" && typeof parsed.parent_head === "string") {
8034
+ return { commit: parsed.commit, parent_head: parsed.parent_head };
8035
+ }
8036
+ } catch {
8037
+ return null;
8038
+ }
8039
+ return null;
8040
+ }
8041
+ async function commitBatchChild(input) {
8042
+ const { root, taskId, batchId, expectedHead, branch: expectedBranch } = input;
8043
+ const auditPair = readAuditTaskPair(root, taskId);
8044
+ if (!auditPair) {
8045
+ throw new Error(`cannot commit child ${taskId}: task is not settled done (audit pair missing)`);
8046
+ }
8047
+ const lifecycle = "lifecycle" in auditPair.record ? auditPair.record.lifecycle : auditPair.record.phase;
8048
+ if (lifecycle !== "done") {
8049
+ throw new Error(`cannot commit child ${taskId}: task is not settled done (lifecycle is ${lifecycle})`);
8050
+ }
8051
+ if (auditPair.proof.terminal_lifecycle !== "done") {
8052
+ throw new Error(`cannot commit child ${taskId}: terminal proof lifecycle is not done`);
8053
+ }
8054
+ if (auditPair.record.task_id !== taskId || auditPair.proof.task_id !== taskId) {
8055
+ throw new Error(`cannot commit child ${taskId}: audit task id mismatch`);
8056
+ }
8057
+ const intentSnapshot = auditPair.record.intent_snapshot;
8058
+ const goal = typeof intentSnapshot.goal === "string" ? intentSnapshot.goal : "";
8059
+ const scopeHint = Array.isArray(intentSnapshot.scope_hint) ? intentSnapshot.scope_hint.filter((s) => typeof s === "string") : [];
8060
+ if (expectedBranch !== undefined) {
8061
+ const currentBranchResult = spawnSync4("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
8062
+ encoding: "utf8",
8063
+ stdio: ["ignore", "pipe", "pipe"]
8064
+ });
8065
+ const currentBranch = currentBranchResult.stdout.trim();
8066
+ if (currentBranchResult.status !== 0 || currentBranch !== expectedBranch) {
8067
+ throw new Error(`batch_head_lineage_broken: current branch ${currentBranch} does not match expected branch ${expectedBranch}`);
8068
+ }
8069
+ }
8070
+ const currentHeadResult = spawnSync4("git", ["-C", root, "rev-parse", "HEAD"], {
8071
+ encoding: "utf8",
8072
+ stdio: ["ignore", "pipe", "pipe"]
8073
+ });
8074
+ const currentHead = currentHeadResult.stdout.trim();
8075
+ if (currentHeadResult.status !== 0 || !currentHead || currentHead !== expectedHead) {
8076
+ throw new Error(`batch_head_lineage_broken: current HEAD ${currentHead} does not match expected_head ${expectedHead}`);
8077
+ }
8078
+ const flaggedCommit = getUnsupportedIndexFlags(root);
8079
+ if (flaggedCommit.length > 0) {
8080
+ throw new Error("dirty_outside_scope");
8081
+ }
8082
+ const changedPaths = getChangedProjectPaths(root);
8083
+ const outsideScope = changedPaths.filter((p) => !isPathAllowedForChild(p, taskId, scopeHint));
8084
+ if (outsideScope.length > 0) {
8085
+ throw new Error("dirty_outside_scope");
8086
+ }
8087
+ const pathsToStage = getUnstagedProjectPaths(root);
8088
+ if (pathsToStage.length > 0) {
8089
+ const addResult = spawnSync4("git", ["-C", root, "--literal-pathspecs", "add", "-A", "--", ...pathsToStage], {
8090
+ encoding: "utf8",
8091
+ stdio: ["ignore", "pipe", "pipe"]
8092
+ });
8093
+ if (addResult.status !== 0) {
8094
+ throw new Error(`failed to stage changed paths: ${addResult.stderr?.trim() || "git add failed"}`);
8095
+ }
8096
+ }
8097
+ const residualUnstaged = getUnstagedProjectPaths(root);
8098
+ if (residualUnstaged.length > 0) {
8099
+ throw new Error(`residual unstaged changes cannot be captured by the parent repository: ${residualUnstaged.join(", ")}`);
8100
+ }
8101
+ const staged = getStagedProjectPaths(root);
8102
+ const stagedOutside = staged.filter((p) => !isPathAllowedForChild(p, taskId, scopeHint));
8103
+ if (stagedOutside.length > 0) {
8104
+ spawnSync4("git", ["-C", root, "reset", "--quiet"], { stdio: ["ignore", "ignore", "ignore"] });
8105
+ throw new Error("dirty_outside_scope");
8106
+ }
8107
+ const goalFirstLine = goal.trim().split(/\r?\n/)[0]?.trim() || taskId;
8108
+ const commitMessage = `imm(${taskId}): ${goalFirstLine}
8109
+
8110
+ Immune-Brain-Batch: ${batchId}
8111
+ `;
8112
+ const commitResult = spawnSync4("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "commit", "--no-verify", "-F", "-"], {
8113
+ input: commitMessage,
8114
+ encoding: "utf8",
8115
+ stdio: ["pipe", "pipe", "pipe"],
8116
+ env: DEFAULT_GIT_ENV
8117
+ });
8118
+ if (commitResult.status !== 0) {
8119
+ throw new Error(`commit failed for child ${taskId}: ${commitResult.stderr?.trim() || "git commit failed"}`);
8120
+ }
8121
+ const newHeadResult = spawnSync4("git", ["-C", root, "rev-parse", "HEAD"], {
8122
+ encoding: "utf8",
8123
+ stdio: ["ignore", "pipe", "pipe"]
8124
+ });
8125
+ const newHead = newHeadResult.stdout.trim();
8126
+ if (newHeadResult.status !== 0 || !newHead || newHead === expectedHead) {
8127
+ throw new Error("commit_failed");
8128
+ }
8129
+ const parentsResult = spawnSync4("git", ["-C", root, "rev-parse", "HEAD^@"], {
8130
+ encoding: "utf8",
8131
+ stdio: ["ignore", "pipe", "pipe"]
8132
+ });
8133
+ const parents = parentsResult.stdout.trim().split(/\s+/).filter(Boolean);
8134
+ if (parents.length !== 1 || parents[0] !== expectedHead) {
8135
+ throw new Error(`batch_head_lineage_broken: commit parent ${parents.join(",")} does not match expected_head ${expectedHead}`);
8136
+ }
8137
+ const committedDelta = getCommittedDeltaPaths(root);
8138
+ const deltaOutside = committedDelta.filter((p) => !isPathAllowedForChild(p, taskId, scopeHint));
8139
+ if (deltaOutside.length > 0) {
8140
+ throw new Error("dirty_outside_scope");
8141
+ }
8142
+ writeBatchCommitEvidence(root, { batchId, taskId, commit: newHead, parentHead: expectedHead });
8143
+ return { commit: newHead };
8144
+ }
8145
+ async function lookupBatchCommit(input) {
8146
+ const { root, taskId, batchId, expectedHead, branch: expectedBranch } = input;
8147
+ const FORMAT = "%H%x00%(trailers:key=Immune-Brain-Batch,valueonly)%x00%an%x00%ae%x00%s";
8148
+ const subjectPrefix = `imm(${taskId}):`;
8149
+ const parseEntry = (entry) => {
8150
+ const parts = entry.split("\x00");
8151
+ return {
8152
+ commit: parts[0]?.trim() ?? "",
8153
+ trailer: parts[1]?.trim() ?? "",
8154
+ authorName: parts[2]?.trim() ?? "",
8155
+ subject: parts[4]?.trim() ?? ""
8156
+ };
8157
+ };
8158
+ const evidence = readBatchCommitEvidence(root, batchId, taskId);
8159
+ let candidates = [];
8160
+ let evidenceBacked = false;
8161
+ if (evidence && evidence.commit) {
8162
+ const direct = spawnSync4("git", ["-C", root, "log", "-n", "1", `--format=${FORMAT}`, evidence.commit, "--"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
8163
+ if (direct.status !== 0) {
8164
+ throw new Error(`failed to inspect batch commit for ${taskId}: ${direct.stderr?.trim() || "git log failed"}`);
8165
+ }
8166
+ if (direct.stdout?.trim()) {
8167
+ const parsed = parseEntry(direct.stdout);
8168
+ if (parsed.commit === evidence.commit) {
8169
+ candidates = [parsed];
8170
+ evidenceBacked = true;
8171
+ }
8172
+ }
8173
+ }
8174
+ if (!candidates.length) {
8175
+ const result2 = spawnSync4("git", [
8176
+ "-C",
8177
+ root,
8178
+ "log",
8179
+ "--fixed-strings",
8180
+ `--grep=${subjectPrefix}`,
8181
+ "-n",
8182
+ "20",
8183
+ `--format=%x1e${FORMAT}`
8184
+ ], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
8185
+ if (result2.status !== 0) {
8186
+ throw new Error(`failed to lookup batch commit for ${taskId}: ${result2.stderr?.trim() || "git log failed"}`);
8187
+ }
8188
+ candidates = (result2.stdout ?? "").split("\x1E").filter((entry) => entry.trim()).map(parseEntry);
8189
+ }
8190
+ let match = null;
8191
+ for (const candidate of candidates) {
8192
+ if (!candidate.commit || candidate.trailer !== batchId)
8193
+ continue;
8194
+ if (!candidate.subject.startsWith(subjectPrefix))
8195
+ continue;
8196
+ match = candidate;
8197
+ break;
8198
+ }
8199
+ if (!match)
8200
+ return null;
8201
+ const commit = match.commit;
8202
+ if (!evidenceBacked) {
8203
+ const evidenceForMatch = readBatchCommitEvidence(root, batchId, taskId);
8204
+ if (!evidenceForMatch || evidenceForMatch.commit !== commit) {
8205
+ throw new Error(`batch_head_lineage_broken: commit ${commit} lacks durable batch runner production evidence`);
8206
+ }
8207
+ }
8208
+ if (match.authorName && match.authorName !== DEFAULT_GIT_ENV.GIT_AUTHOR_NAME) {
8209
+ throw new Error(`batch_head_lineage_broken: adopted commit author ${match.authorName} does not match batch authority ${DEFAULT_GIT_ENV.GIT_AUTHOR_NAME}`);
8210
+ }
8211
+ if (expectedBranch !== undefined) {
8212
+ const currentBranchResult = spawnSync4("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
8213
+ encoding: "utf8",
8214
+ stdio: ["ignore", "pipe", "pipe"]
8215
+ });
8216
+ const currentBranch = currentBranchResult.stdout.trim();
8217
+ if (currentBranchResult.status !== 0 || currentBranch !== expectedBranch) {
8218
+ throw new Error(`batch_head_lineage_broken: current branch ${currentBranch} does not match expected branch ${expectedBranch}`);
8219
+ }
8220
+ }
8221
+ if (expectedHead !== undefined) {
8222
+ const currentHeadResult = spawnSync4("git", ["-C", root, "rev-parse", "HEAD"], {
8223
+ encoding: "utf8",
8224
+ stdio: ["ignore", "pipe", "pipe"]
8225
+ });
8226
+ const currentHead = currentHeadResult.stdout.trim();
8227
+ if (currentHead !== commit) {
8228
+ throw new Error(`batch_head_lineage_broken: current HEAD ${currentHead} diverged from adopted commit ${commit}`);
8229
+ }
8230
+ const parentsResult = spawnSync4("git", ["-C", root, "rev-parse", `${commit}^@`], {
8231
+ encoding: "utf8",
8232
+ stdio: ["ignore", "pipe", "pipe"]
8233
+ });
8234
+ const parents = parentsResult.stdout.trim().split(/\s+/).filter(Boolean);
8235
+ if (parents.length !== 1 || parents[0] !== expectedHead) {
8236
+ throw new Error(`batch_head_lineage_broken: adopted commit parent ${parents.join(",")} does not match expected_head ${expectedHead}`);
8237
+ }
8238
+ const auditPair = readAuditTaskPair(root, taskId);
8239
+ if (!auditPair) {
8240
+ throw new Error(`batch_head_lineage_broken: adopted commit lacks terminal audit pair for ${taskId}`);
8241
+ }
8242
+ const lifecycle = "lifecycle" in auditPair.record ? auditPair.record.lifecycle : auditPair.record.phase;
8243
+ if (lifecycle !== "done") {
8244
+ throw new Error(`batch_head_lineage_broken: adopted commit task lifecycle is not done: ${lifecycle}`);
8245
+ }
8246
+ const scopeHint = Array.isArray(auditPair.record.intent_snapshot.scope_hint) ? auditPair.record.intent_snapshot.scope_hint.filter((s) => typeof s === "string") : [];
8247
+ const deltaResult = spawnSync4("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", expectedHead, commit], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
8248
+ if (deltaResult.status !== 0) {
8249
+ throw new Error("batch_head_lineage_broken: failed to inspect adopted commit delta");
8250
+ }
8251
+ const changedInCommit = deltaResult.stdout.split("\x00").filter((p) => p.length > 0);
8252
+ const outside = changedInCommit.filter((p) => !isPathAllowedForChild(p, taskId, scopeHint));
8253
+ if (outside.length > 0) {
8254
+ throw new Error(`batch_head_lineage_broken: adopted commit contains out-of-scope changes: ${outside.join(", ")}`);
8255
+ }
8256
+ }
8257
+ return { commit };
8258
+ }
8259
+
8260
+ // plugins/immune-brain/runtime/unattended/batch_state.ts
8261
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, openSync as openSync5, closeSync as closeSync5, writeFileSync as writeFileSync4, renameSync as renameSync3, lstatSync as lstatSync7, constants as constants4, rmSync as rmSync5 } from "node:fs";
8262
+ import { randomUUID as randomUUID6 } from "node:crypto";
8263
+ import { dirname as dirname5, join as join8 } from "node:path";
8264
+ var BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
8265
+ var CHILD_RUN_STATES = new Set([
8266
+ "pending",
8267
+ "enrolled",
8268
+ "settled",
8269
+ "committed",
8270
+ "needs_human",
8271
+ "skipped_blocked"
8272
+ ]);
8273
+ var BATCH_RUN_STATES = new Set([
8274
+ "prepared",
8275
+ "running",
8276
+ "needs_human",
8277
+ "completed",
8278
+ "budget_stopped",
8279
+ "failed",
8280
+ "rejected"
8281
+ ]);
8282
+ var ISO_TIMESTAMP_PATTERN2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
8283
+ function isCanonicalTimestamp(value) {
8284
+ if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN2.test(value))
8285
+ return false;
8286
+ const milliseconds = Date.parse(value);
8287
+ return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === value;
8288
+ }
8289
+ function validateBatchId(batchId) {
8290
+ if (typeof batchId !== "string" || !BATCH_ID_PATTERN.test(batchId))
8291
+ throw new Error("batch id is not a safe file identity");
8292
+ }
8293
+ function statePath(batchId) {
8294
+ validateBatchId(batchId);
8295
+ return join8(".imm", "state", "batches", `${batchId}.json`);
8296
+ }
8297
+ function canonicalBytes(record) {
8298
+ return `${JSON.stringify(record, null, 2)}
8299
+ `;
8300
+ }
8301
+ function validateRecordShape(value, batchId) {
8302
+ if (typeof value !== "object" || value === null)
8303
+ throw new Error(`batch run state ${batchId} is not an object`);
8304
+ const record = value;
8305
+ if (record.contract !== "assurance_kernel/batch_run_state/v1")
8306
+ throw new Error(`batch run state ${batchId} has an unknown contract`);
8307
+ if (record.batch_id !== batchId)
8308
+ throw new Error(`batch run state ${batchId} carries batch_id ${String(record.batch_id)}`);
8309
+ if (typeof record.plan_digest !== "string" || !record.plan_digest)
8310
+ throw new Error(`batch run state ${batchId} has an invalid plan_digest`);
8311
+ if (typeof record.base_head !== "string" || !record.base_head)
8312
+ throw new Error(`batch run state ${batchId} has an invalid base_head`);
8313
+ if (record.branch !== undefined && (typeof record.branch !== "string" || !record.branch))
8314
+ throw new Error(`batch run state ${batchId} has an invalid branch`);
8315
+ if (!isCanonicalTimestamp(record.confirmation_time))
8316
+ throw new Error(`batch run state ${batchId} has an invalid confirmation_time`);
8317
+ if (!isCanonicalTimestamp(record.authorization_expires_at))
8318
+ throw new Error(`batch run state ${batchId} has an invalid authorization_expires_at`);
8319
+ if (!isCanonicalTimestamp(record.created_at) || !isCanonicalTimestamp(record.updated_at))
8320
+ throw new Error(`batch run state ${batchId} has invalid state timestamps`);
8321
+ if (!Array.isArray(record.children) || record.children.length === 0)
8322
+ throw new Error(`batch run state ${batchId} has no children`);
8323
+ if (!BATCH_RUN_STATES.has(String(record.batch_state)))
8324
+ throw new Error(`batch run state ${batchId} has an invalid batch_state`);
8325
+ if (typeof record.consecutive_qa_failures !== "number" || !Number.isInteger(record.consecutive_qa_failures) || record.consecutive_qa_failures < 0)
8326
+ throw new Error(`batch run state ${batchId} has an invalid consecutive_qa_failures`);
8327
+ const budget2 = record.budget;
8328
+ if (typeof record.budget !== "object" || record.budget === null || typeof budget2.max_children !== "number" || !Number.isInteger(budget2.max_children) || budget2.max_children <= 0 || !isCanonicalTimestamp(budget2.deadline_at) || typeof budget2.qa_failure_limit !== "number" || !Number.isInteger(budget2.qa_failure_limit) || budget2.qa_failure_limit <= 0)
8329
+ throw new Error(`batch run state ${batchId} has an invalid budget`);
8330
+ if (!Array.isArray(record.commits) || record.commits.some((c) => typeof c !== "string"))
8331
+ throw new Error(`batch run state ${batchId} has an invalid commits list`);
8332
+ const seenTaskIds = new Set;
8333
+ for (const child of record.children) {
8334
+ if (typeof child !== "object" || child === null || typeof child.task_id !== "string" || !child.task_id || typeof child.slice_id !== "string" || !child.slice_id)
8335
+ throw new Error(`batch run state ${batchId} has an invalid child entry`);
8336
+ if (!CHILD_RUN_STATES.has(String(child.state)))
8337
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid state`);
8338
+ if (!Array.isArray(child.blocked_by) || child.blocked_by.some((b) => typeof b !== "string"))
8339
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid blocked_by`);
8340
+ if (child.reason !== null && typeof child.reason !== "string" || child.commit !== null && typeof child.commit !== "string")
8341
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has invalid terminal fields`);
8342
+ if (child.state === "committed" && child.commit === null)
8343
+ throw new Error(`batch run state ${batchId} child ${child.task_id} is committed without a commit`);
8344
+ if (child.state !== "committed" && child.commit !== null)
8345
+ throw new Error(`batch run state ${batchId} child ${child.task_id} has a commit but is not committed`);
8346
+ if ((child.state === "needs_human" || child.state === "skipped_blocked") && child.reason === null)
8347
+ throw new Error(`batch run state ${batchId} child ${child.task_id} needs a terminal reason`);
8348
+ if (seenTaskIds.has(child.task_id))
8349
+ throw new Error(`batch run state ${batchId} has a duplicate child ${child.task_id}`);
8350
+ seenTaskIds.add(child.task_id);
8351
+ }
8352
+ for (const child of record.children) {
8353
+ if (child.state === "committed" && child.commit !== null && !record.commits.includes(child.commit))
8354
+ throw new Error(`batch run state ${batchId} child ${child.task_id} commit is missing from commits`);
8355
+ }
8356
+ if ((record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected") && record.children.some((child) => child.state === "enrolled" || child.state === "settled"))
8357
+ throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
8358
+ if (record.batch_state === "completed" && record.children.some((child) => child.state !== "committed"))
8359
+ throw new Error(`batch run state ${batchId} is completed but a child is not committed`);
8360
+ }
8361
+ function prepareBatchRunState(input) {
8362
+ validateBatchId(input.batch_id);
8363
+ const prepared = {
8364
+ contract: "assurance_kernel/batch_run_state/v1",
8365
+ batch_id: input.batch_id,
8366
+ initiative_slug: input.initiative_slug,
8367
+ plan_digest: input.plan_digest,
8368
+ base_head: input.base_head,
8369
+ branch: input.branch ?? `imm/${input.initiative_slug}`,
8370
+ confirmation_time: input.confirmation_time,
8371
+ authorization_expires_at: input.authorization_expires_at,
8372
+ budget: input.budget,
8373
+ batch_state: "prepared",
8374
+ children: input.children.map((child) => ({
8375
+ task_id: child.task_id,
8376
+ slice_id: child.slice_id,
8377
+ blocked_by: [...child.blocked_by],
8378
+ state: "pending",
8379
+ reason: null,
8380
+ commit: null
8381
+ })),
8382
+ consecutive_qa_failures: 0,
8383
+ commits: [],
8384
+ created_at: input.now,
8385
+ updated_at: input.now
8386
+ };
8387
+ return prepared;
8388
+ }
8389
+ function readBatchRunState(root, batchId) {
8390
+ const path = statePath(batchId);
8391
+ if (!existsSync4(join8(root, path)))
8392
+ return null;
8393
+ const parsed = JSON.parse(readSecureProjectFile(root, path));
8394
+ validateRecordShape(parsed, batchId);
8395
+ return parsed;
8396
+ }
8397
+ function ensureSecureDirectory3(root, relative4) {
8398
+ const target = join8(root, relative4);
8399
+ const parent = dirname5(target);
8400
+ if (!existsSync4(parent))
8401
+ mkdirSync4(parent, { recursive: true });
8402
+ if (existsSync4(target)) {
8403
+ const stats = lstatSync7(target);
8404
+ if (!stats.isDirectory())
8405
+ throw new Error(`${relative4} exists but is not a directory`);
8406
+ } else {
8407
+ mkdirSync4(target);
8408
+ }
8409
+ return target;
8410
+ }
8411
+ function writeFileAtomically2(root, relative4, bytes) {
8412
+ const target = join8(root, relative4);
8413
+ const targetDir = dirname5(target);
8414
+ const stats = lstatSync7(targetDir);
8415
+ if (!stats.isDirectory())
8416
+ throw new Error(`${dirname5(relative4)} is not a directory`);
8417
+ const tempPath = `${target}.${randomUUID6()}.tmp`;
8418
+ let fd = null;
8419
+ try {
8420
+ fd = openSync5(tempPath, constants4.O_WRONLY | constants4.O_CREAT | constants4.O_EXCL, 384);
8421
+ writeFileSync4(fd, bytes, "utf8");
8422
+ closeSync5(fd);
8423
+ fd = null;
8424
+ renameSync3(tempPath, target);
8425
+ } finally {
8426
+ if (fd !== null)
8427
+ closeSync5(fd);
8428
+ if (existsSync4(tempPath)) {
8429
+ try {
8430
+ rmSync5(tempPath);
8431
+ } catch {}
8432
+ }
8433
+ }
8434
+ }
8435
+ function writeBatchRunState(root, record) {
8436
+ const path = statePath(record.batch_id);
8437
+ validateRecordShape(record, record.batch_id);
8438
+ return withKernelStoreLock(root, () => {
8439
+ const existing = existsSync4(join8(root, path)) ? readSecureProjectFile(root, path) : null;
8440
+ if (existing !== null && existing === canonicalBytes(record))
8441
+ return record;
8442
+ const stored = {
8443
+ ...record,
8444
+ updated_at: new Date().toISOString()
8445
+ };
8446
+ ensureSecureDirectory3(root, join8(".imm", "state", "batches"));
8447
+ writeFileAtomically2(root, path, canonicalBytes(stored));
8448
+ return stored;
8449
+ });
8450
+ }
8451
+ function reportPath(batchId) {
8452
+ validateBatchId(batchId);
8453
+ return join8(".imm", "state", "batches", `${batchId}.report.json`);
8454
+ }
8455
+ function writeBatchRunReport(root, report) {
8456
+ const relative4 = reportPath(report.batch_id);
8457
+ return withKernelStoreLock(root, () => {
8458
+ const path = join8(root, relative4);
8459
+ if (existsSync4(path)) {
8460
+ const original = JSON.parse(readSecureProjectFile(root, relative4));
8461
+ if (typeof original !== "object" || original === null || original.contract !== "assurance_kernel/batch_run_report/v1")
8462
+ throw new Error(`batch run report ${report.batch_id} has an unknown contract`);
8463
+ const prior = original;
8464
+ if (canonicalReportBytes(prior) === canonicalReportBytes(report))
8465
+ return prior;
8466
+ if (prior.batch_state !== "needs_human")
8467
+ return prior;
8468
+ }
8469
+ ensureSecureDirectory3(root, join8(".imm", "state", "batches"));
8470
+ writeFileAtomically2(root, relative4, canonicalReportBytes(report));
8471
+ return report;
8472
+ });
8473
+ }
8474
+ function canonicalReportBytes(report) {
8475
+ return `${JSON.stringify(report, null, 2)}
8476
+ `;
8477
+ }
8478
+ function isTerminalBatchState(state) {
8479
+ return state === "completed" || state === "budget_stopped" || state === "failed" || state === "rejected";
8480
+ }
8481
+
8482
+ // plugins/immune-brain/runtime/unattended/batch_runner.ts
8483
+ function dependentsOf(record, taskId) {
8484
+ return record.children.filter((child) => child.blocked_by.includes(taskId));
8485
+ }
8486
+ function skipDependents(record, taskId, reason) {
8487
+ const skip = new Set([taskId]);
8488
+ const queue = [taskId];
8489
+ while (queue.length > 0) {
8490
+ const current = queue.shift();
8491
+ for (const dependent of dependentsOf(record, current)) {
8492
+ if (skip.has(dependent.task_id))
8493
+ continue;
8494
+ skip.add(dependent.task_id);
8495
+ queue.push(dependent.task_id);
8496
+ }
8497
+ }
8498
+ record.children = record.children.map((child) => skip.has(child.task_id) && child.state === "pending" ? { ...child, state: "skipped_blocked", reason } : child);
8499
+ }
8500
+ function budgetStopReason(record, now) {
8501
+ const enrolledCount = record.children.filter((child) => child.state !== "pending" && child.state !== "skipped_blocked").length;
8502
+ if (enrolledCount >= record.budget.max_children)
8503
+ return `max_children budget exhausted (${record.budget.max_children})`;
8504
+ const deadline = Date.parse(record.budget.deadline_at);
8505
+ if (!Number.isNaN(deadline) && now >= deadline)
8506
+ return `deadline_at reached (${record.budget.deadline_at})`;
8507
+ return null;
8508
+ }
8509
+ function isAuthorizationExpiryError(error) {
8510
+ return error instanceof BatchAuthorizationExpiryError;
8511
+ }
8512
+ function requireFreshProjection(result2, taskId) {
8513
+ if (result2.error !== null)
8514
+ throw new Error(`cannot reconcile Kernel projection for ${taskId}: ${result2.error}`);
8515
+ return result2;
8516
+ }
8517
+ function nextEnrollableChild(record) {
8518
+ return record.children.find((child) => child.state === "pending" && record.children.every((other) => !child.blocked_by.includes(other.task_id) || other.state === "committed")) ?? null;
8519
+ }
8520
+ var TERMINAL_NEXT_ACTIONS = {
8521
+ completed: "The batch settled every enrollable child; review the commits and the tracker.",
8522
+ budget_stopped: "Budget, deadline, or authorization expiry stopped new enrollments; re-confirm to continue under a new authorization.",
8523
+ failed: "A commit or lineage failure stopped the batch; inspect the failing child and the branch state.",
8524
+ rejected: "The batch was rejected before any enrollment; correct the stated reason and re-confirm.",
8525
+ needs_human: "A parked child needs a human decision; resolve it, then re-confirm to continue.",
8526
+ running: "The batch is still running; no terminal report is due yet.",
8527
+ prepared: "The batch is prepared but not started."
8528
+ };
8529
+ function reportFor(record, reason, nextAction) {
8530
+ return {
8531
+ contract: "assurance_kernel/batch_run_report/v1",
8532
+ batch_id: record.batch_id,
8533
+ initiative_slug: record.initiative_slug,
8534
+ batch_state: record.batch_state,
8535
+ children: record.children,
8536
+ commits: record.commits,
8537
+ reason,
8538
+ next_action: nextAction || (TERMINAL_NEXT_ACTIONS[record.batch_state] ?? "Inspect the batch run state."),
8539
+ created_at: record.updated_at
8540
+ };
8541
+ }
8542
+ function finalize(root, record, reason, nextAction) {
8543
+ const report = reportFor(record, reason, nextAction);
8544
+ if (isTerminalBatchState(record.batch_state) || record.batch_state === "needs_human") {
8545
+ writeBatchRunReport(root, report);
8546
+ }
8547
+ return report;
8548
+ }
8549
+ function isLineageBreakError(message) {
8550
+ return message.includes("batch_head_lineage_broken");
8551
+ }
8552
+ function failPersistedLineage(root, existing, message) {
8553
+ const children = existing.children.map((c) => c.state === "enrolled" || c.state === "settled" ? { ...c, state: "needs_human", reason: message } : c);
8554
+ const record = { ...existing, batch_state: "failed", children };
8555
+ for (const child of record.children) {
8556
+ if (child.state === "needs_human") {
8557
+ skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
8558
+ }
8559
+ }
8560
+ return writeBatchRunState(root, record);
8561
+ }
8562
+ function externalHeadDriftMessage(root, record) {
8563
+ if (!existsSync5(join9(root, ".git")))
8564
+ return null;
8565
+ const head = record.commits.length ? record.commits[record.commits.length - 1] : record.base_head;
8566
+ const headCheck = spawnSync5("git", ["-C", root, "rev-parse", "HEAD"], {
8567
+ encoding: "utf8",
8568
+ stdio: ["ignore", "pipe", "pipe"]
8569
+ });
8570
+ if (headCheck.status === 0 && headCheck.stdout.trim() && headCheck.stdout.trim() !== head) {
8571
+ return `batch_head_lineage_broken: current HEAD ${headCheck.stdout.trim()} does not match expected batch head ${head}`;
8572
+ }
8573
+ const branchCheck = spawnSync5("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
8574
+ encoding: "utf8",
8575
+ stdio: ["ignore", "pipe", "pipe"]
8576
+ });
8577
+ const currentBranch = branchCheck.stdout.trim();
8578
+ if (branchCheck.status !== 0 || currentBranch !== record.branch) {
8579
+ return `batch_head_lineage_broken: current branch ${currentBranch} does not match expected batch branch ${record.branch}`;
8580
+ }
8581
+ return null;
8582
+ }
8583
+ async function validatePersistedRun(input, record) {
8584
+ const plan = input.registry.children(input.capability);
8585
+ if (record.plan_digest !== computeBatchPlanDigest(plan) || record.children.length !== plan.length || record.children.some((child, index) => {
8586
+ const expected = plan[index];
8587
+ return child.task_id !== expected.task_id || JSON.stringify(child.blocked_by) !== JSON.stringify(expected.blocked_by);
8588
+ }))
8589
+ throw new Error("plan_digest mismatch: persisted children do not match the authorized plan");
8590
+ const commits = [];
8591
+ for (const child of record.children) {
8592
+ if (child.state !== "committed") {
8593
+ if (child.commit !== null)
8594
+ throw new Error("uncommitted batch child has a commit");
8595
+ continue;
8596
+ }
8597
+ const evidence = input.git?.lookupBatchCommit ? await input.git.lookupBatchCommit(input.root, child.task_id, record.batch_id, undefined, record.branch) : input.kernel.lookupBatchCommit ? await input.kernel.lookupBatchCommit(input.root, child.task_id, record.batch_id, undefined, record.branch) : await lookupBatchCommit({
8598
+ root: input.root,
8599
+ taskId: child.task_id,
8600
+ batchId: record.batch_id,
8601
+ branch: record.branch
8602
+ });
8603
+ if (!evidence || evidence.commit !== child.commit) {
8604
+ if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync5(join9(input.root, ".git"))) {
8605
+ const reach = spawnSync5("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
8606
+ if (reach.status !== 0) {
8607
+ throw new Error(`batch_head_lineage_broken: recorded commit ${child.commit} for ${child.task_id} is no longer reachable from HEAD`);
8608
+ }
8609
+ }
8610
+ throw new Error(`persisted batch commit lacks evidence for ${child.task_id}`);
8611
+ }
8612
+ commits.push(evidence.commit);
8613
+ }
8614
+ if (JSON.stringify([...record.commits].sort()) !== JSON.stringify(commits.sort()))
8615
+ throw new Error("persisted batch commit list does not match child evidence");
8616
+ }
8617
+ function replayTerminal(root, existing) {
8618
+ return finalize(root, existing, `terminal state already reached: ${existing.batch_state}`, "");
8619
+ }
8620
+ function validateRunAuthorization(input, existing) {
8621
+ const authorized = input.kernel.validateBatchAuthorization({
8622
+ registry: input.registry,
8623
+ capability: input.capability,
8624
+ binding: {
8625
+ batch_id: input.batch_id,
8626
+ plan_digest: existing?.plan_digest ?? input.plan_digest,
8627
+ base_head: existing?.base_head ?? input.base_head,
8628
+ initiative_slug: existing?.initiative_slug ?? input.initiative_slug,
8629
+ budget: input.budget,
8630
+ expires_at: input.authorization_expires_at
8631
+ }
8632
+ });
8633
+ if (authorized.issued_at !== input.confirmation_time || existing && Date.parse(authorized.issued_at) <= Date.parse(existing.confirmation_time))
8634
+ throw new Error("the parked batch requires a fresh literal-user confirmation");
8635
+ const children = input.children.map((child) => {
8636
+ const { intent_path, intent_revision, intent_content_hash } = child;
8637
+ if (intent_path === null || intent_revision === null || intent_content_hash === null)
8638
+ throw new Error(`batch child ${child.task_id} has no complete intent identity`);
8639
+ return { ...child, intent_path, intent_revision, intent_content_hash };
8640
+ });
8641
+ if (computeBatchPlanDigest(children) !== authorized.plan_digest || input.plan_digest !== authorized.plan_digest || input.base_head !== authorized.base_head)
8642
+ throw new Error("batch run input does not match the authorized plan or base_head");
8643
+ }
8644
+ async function startBatch(input) {
8645
+ if (!input.children.length) {
8646
+ const rejected = prepareBatchRunState({ ...input, children: [], now: input.now });
8647
+ return reportFor({ ...rejected, batch_state: "rejected" }, "batch plan is empty", "Provide a non-empty enrollable child plan.");
8648
+ }
8649
+ let existing = readBatchRunState(input.root, input.batch_id);
8650
+ if (existing) {
8651
+ try {
8652
+ await validatePersistedRun(input, existing);
8653
+ } catch (error) {
8654
+ const message = error instanceof Error ? error.message : String(error);
8655
+ if (isLineageBreakError(message)) {
8656
+ if (isTerminalBatchState(existing.batch_state))
8657
+ return replayTerminal(input.root, existing);
8658
+ const failed = failPersistedLineage(input.root, existing, message);
8659
+ return finalize(input.root, failed, message, "");
8660
+ }
8661
+ throw error;
8662
+ }
8663
+ }
8664
+ if (existing && isTerminalBatchState(existing.batch_state)) {
8665
+ return replayTerminal(input.root, existing);
8666
+ }
8667
+ if (existing?.batch_state === "needs_human") {
8668
+ const priorConfirmation = Date.parse(existing.confirmation_time);
8669
+ const nextConfirmation = Date.parse(input.confirmation_time);
8670
+ const nextExpiry = Date.parse(input.authorization_expires_at);
8671
+ const freshAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation && Number.isFinite(nextExpiry) && nextExpiry > Date.now();
8672
+ if (!freshAuthorization) {
8673
+ 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.");
8674
+ }
8675
+ validateRunAuthorization(input, existing);
8676
+ prepareBatchRunState({ ...input, now: input.now });
8677
+ const remapped = await Promise.all(existing.children.map(async (child) => {
8678
+ if (child.state !== "needs_human")
8679
+ return child;
8680
+ const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
8681
+ if (fresh.projection.lifecycle === "done" && fresh.projection.completion_ready)
8682
+ return { ...child, state: "settled", reason: null };
8683
+ if (fresh.error === null && fresh.claim?.task_id === child.task_id) {
8684
+ return input.kernel.ownsTaskClaim(child.task_id) ? { ...child, state: "enrolled", reason: null } : child;
8685
+ }
8686
+ return { ...child, state: "pending", reason: null };
8687
+ }));
8688
+ if (remapped.some((child) => child.state === "needs_human")) {
8689
+ const reparked = { ...existing, children: remapped };
8690
+ for (const parked of remapped) {
8691
+ if (parked.state === "needs_human")
8692
+ skipDependents(reparked, parked.task_id, `dependency ${parked.task_id} parked`);
8693
+ }
8694
+ existing = writeBatchRunState(input.root, {
8695
+ ...reparked,
8696
+ confirmation_time: input.confirmation_time,
8697
+ authorization_expires_at: input.authorization_expires_at,
8698
+ budget: input.budget,
8699
+ batch_state: "needs_human"
8700
+ });
8701
+ return finalize(input.root, existing, "a parked child's Kernel claim is held by a foreign batch", "Resolve the foreign claim, then re-confirm the batch to continue.");
8702
+ }
8703
+ const resumedChildren = remapped.map((child) => child.state === "skipped_blocked" ? { ...child, state: "pending", reason: null } : child);
8704
+ existing = writeBatchRunState(input.root, {
8705
+ ...existing,
8706
+ confirmation_time: input.confirmation_time,
8707
+ authorization_expires_at: input.authorization_expires_at,
8708
+ budget: input.budget,
8709
+ batch_state: "running",
8710
+ consecutive_qa_failures: 0,
8711
+ children: resumedChildren
8712
+ });
8713
+ }
8714
+ if (existing?.batch_state === "running") {
8715
+ const nextConfirmation = Date.parse(input.confirmation_time);
8716
+ const nextExpiry = Date.parse(input.authorization_expires_at);
8717
+ const priorConfirmation = Date.parse(existing.confirmation_time);
8718
+ const persistedExpiry = Date.parse(existing.authorization_expires_at);
8719
+ const renewedAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation && Number.isFinite(nextExpiry) && nextExpiry > Date.now() && nextExpiry > persistedExpiry;
8720
+ if (renewedAuthorization) {
8721
+ validateRunAuthorization(input, existing);
8722
+ existing = writeBatchRunState(input.root, {
8723
+ ...existing,
8724
+ confirmation_time: input.confirmation_time,
8725
+ authorization_expires_at: input.authorization_expires_at,
8726
+ budget: input.budget
8727
+ });
8728
+ }
8729
+ }
8730
+ if (existing) {
8731
+ const interruptedChild = existing.children.find((child) => child.state === "enrolled" || child.state === "settled");
8732
+ if (interruptedChild) {
8733
+ return await resumeBatch(input, (root, taskId) => input.kernel.projectTask(root, taskId));
8734
+ }
8735
+ }
8736
+ if (!existing) {
8737
+ try {
8738
+ validateRunAuthorization(input, null);
8739
+ } catch (error) {
8740
+ return reportFor({ ...prepareBatchRunState(input), batch_state: "rejected" }, error instanceof Error ? error.message : String(error), "Correct the authorization or plan, then re-confirm the batch.");
8741
+ }
8742
+ const preflightResult = input.git?.preflight ? await input.git.preflight({
8743
+ root: input.root,
8744
+ initiative_slug: input.initiative_slug,
8745
+ base_head: input.base_head
8746
+ }) : input.kernel.gitPreflight ? await input.kernel.gitPreflight({
8747
+ root: input.root,
8748
+ initiative_slug: input.initiative_slug,
8749
+ base_head: input.base_head
8750
+ }) : runBatchGitPreflight({
8751
+ root: input.root,
8752
+ initiative_slug: input.initiative_slug,
8753
+ base_head: input.base_head
8754
+ });
8755
+ if (!preflightResult.ok) {
8756
+ const rejected = prepareBatchRunState({ ...input, now: input.now });
8757
+ return reportFor({ ...rejected, batch_state: "rejected" }, preflightResult.reason, preflightResult.message || "Correct the preflight condition and re-confirm.");
8758
+ }
8759
+ }
8760
+ let record = existing ?? prepareBatchRunState({
8761
+ batch_id: input.batch_id,
8762
+ initiative_slug: input.initiative_slug,
8763
+ children: input.children,
8764
+ plan_digest: input.plan_digest,
8765
+ base_head: input.base_head,
8766
+ confirmation_time: input.confirmation_time,
8767
+ authorization_expires_at: input.authorization_expires_at,
8768
+ budget: input.budget,
8769
+ now: input.now
8770
+ });
8771
+ const persist = () => {
8772
+ record = writeBatchRunState(input.root, record);
8773
+ };
8774
+ if (record.batch_state === "prepared") {
8775
+ record.batch_state = "running";
8776
+ persist();
8777
+ }
8778
+ let head = record.commits.length ? record.commits[record.commits.length - 1] : record.base_head;
8779
+ const driftMessage = externalHeadDriftMessage(input.root, record);
8780
+ if (driftMessage) {
8781
+ record.batch_state = "failed";
8782
+ persist();
8783
+ return finalize(input.root, record, driftMessage, "");
8784
+ }
8785
+ while (record.batch_state === "running") {
8786
+ const child = nextEnrollableChild(record);
8787
+ if (!child) {
8788
+ record.batch_state = record.children.some((c) => c.state === "needs_human" || c.state === "skipped_blocked") ? "needs_human" : "completed";
8789
+ persist();
8790
+ break;
8791
+ }
8792
+ let adoptedClaim = false;
8793
+ let claimState = "none";
8794
+ const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
8795
+ if (fresh.claim !== null && fresh.claim.task_id === child.task_id) {
8796
+ claimState = input.kernel.ownsTaskClaim(child.task_id) ? "ours" : "foreign";
8797
+ }
8798
+ if (claimState === "ours") {
8799
+ adoptedClaim = true;
8800
+ } else if (claimState === "foreign") {
8801
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: "claim held by another batch" } : c);
8802
+ skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
8803
+ record.batch_state = "needs_human";
8804
+ persist();
8805
+ return finalize(input.root, record, "claim held by another batch", "needs-human-attention");
8806
+ }
8807
+ if (adoptedClaim) {
8808
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "enrolled", reason: "adopted existing batch claim after interruption" } : c);
8809
+ persist();
8810
+ } else {
8811
+ const now = Date.now();
8812
+ if (now >= Date.parse(record.authorization_expires_at) || budgetStopReason(record, now)) {
8813
+ record.batch_state = record.children.some((c) => c.state === "needs_human" || c.state === "skipped_blocked") ? "needs_human" : "budget_stopped";
8814
+ persist();
8815
+ break;
8816
+ }
8817
+ try {
8818
+ await validatePersistedRun(input, record);
8819
+ const { issued_at: _issuedAt, ...binding } = input.kernel.validateBatchAuthorization({
8820
+ registry: input.registry,
8821
+ capability: input.capability,
8822
+ binding: {
8823
+ batch_id: record.batch_id,
8824
+ plan_digest: record.plan_digest,
8825
+ base_head: record.base_head,
8826
+ initiative_slug: record.initiative_slug,
8827
+ budget: record.budget,
8828
+ expires_at: record.authorization_expires_at
8829
+ }
8830
+ });
8831
+ for (const committed of record.children) {
8832
+ if (committed.state === "committed" && !input.registry.isChildConsumed(input.capability, committed.task_id))
8833
+ input.registry.consumeChild(input.capability, binding, committed.task_id);
8834
+ }
8835
+ await input.kernel.enrollTask({
8836
+ root: input.root,
8837
+ task_id: child.task_id,
8838
+ batch: {
8839
+ registry: input.registry,
8840
+ capability: input.capability,
8841
+ binding: { batch_id: input.batch_id, expected_head: head }
8842
+ }
8843
+ });
8844
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "enrolled" } : c);
8845
+ persist();
8846
+ } catch (error) {
8847
+ const message = error instanceof Error ? error.message : String(error);
8848
+ if (isAuthorizationExpiryError(error)) {
8849
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "pending", reason: message } : c);
8850
+ record.batch_state = "budget_stopped";
8851
+ persist();
8852
+ break;
8853
+ }
8854
+ if (isLineageBreakError(message)) {
8855
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
8856
+ skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
8857
+ record.batch_state = "failed";
8858
+ persist();
8859
+ break;
8860
+ }
8861
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
8862
+ skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
8863
+ record.batch_state = "needs_human";
8864
+ persist();
8865
+ break;
8866
+ }
8867
+ }
8868
+ let childTerminal = false;
8869
+ while (!childTerminal) {
8870
+ const terminal = await input.kernel.advanceTask(input.root, child.task_id);
8871
+ if (terminal.state === "completed") {
8872
+ childTerminal = true;
8873
+ record.consecutive_qa_failures = 0;
8874
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "settled", reason: null } : c);
8875
+ persist();
8876
+ try {
8877
+ const planChild = input.children.find((c) => c.task_id === child.task_id);
8878
+ const intentPath = planChild?.intent_path ?? undefined;
8879
+ const { commit } = input.git?.commitChild ? await input.git.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath) : input.kernel.commitChild ? await input.kernel.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath) : await commitBatchChild({
8880
+ root: input.root,
8881
+ taskId: child.task_id,
8882
+ batchId: input.batch_id,
8883
+ expectedHead: head,
8884
+ branch: record.branch,
8885
+ intentPath
8886
+ });
8887
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "committed", commit } : c);
8888
+ record.commits.push(commit);
8889
+ head = commit;
8890
+ persist();
8891
+ } catch (error) {
8892
+ const message = error instanceof Error ? error.message : String(error);
8893
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
8894
+ skipDependents(record, child.task_id, `dependency ${child.task_id} failed to commit`);
8895
+ record.batch_state = "failed";
8896
+ persist();
8897
+ break;
8898
+ }
8899
+ } else if (terminal.state === "stopped") {
8900
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: "Kernel reported the child stopped" } : c);
8901
+ childTerminal = true;
8902
+ skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
8903
+ record.batch_state = "needs_human";
8904
+ persist();
8905
+ } else if (terminal.state === "rework" && terminal.operation === "qa") {
8906
+ record.consecutive_qa_failures += 1;
8907
+ if (record.consecutive_qa_failures >= record.budget.qa_failure_limit) {
8908
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: `QA failure limit reached: ${terminal.summary}` } : c);
8909
+ record.batch_state = "needs_human";
8910
+ childTerminal = true;
8911
+ skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
8912
+ persist();
8913
+ } else {
8914
+ persist();
8915
+ }
8916
+ } else if (terminal.state === "failed" || terminal.state === "blocked" || terminal.state === "rework") {
8917
+ const reason = terminal.state === "rework" ? terminal.summary : terminal.reason;
8918
+ childTerminal = true;
8919
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
8920
+ skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
8921
+ record.batch_state = "needs_human";
8922
+ persist();
8923
+ } else if (terminal.state === "review_ready") {
8924
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "enrolled", reason: `review reservation ${terminal.operation_id} open` } : c);
8925
+ persist();
8926
+ return reportFor(record, `child ${child.task_id} holds an open Review reservation`, "Submit the reserved foreground Review verdict, then call startBatch again to continue.");
8927
+ }
8928
+ }
8929
+ }
8930
+ return finalize(input.root, record, stopReasonFor(record), "");
8931
+ }
8932
+ function stopReasonFor(record) {
8933
+ switch (record.batch_state) {
8934
+ case "budget_stopped":
8935
+ return budgetStopReason(record, Date.now()) ?? "budget, deadline, or authorization expiry stopped new enrollments";
8936
+ case "failed": {
8937
+ const failedChild = record.children.find((c) => c.state === "needs_human" && c.reason);
8938
+ return failedChild?.reason ?? "a commit or lineage failure stopped the batch";
8939
+ }
8940
+ case "needs_human":
8941
+ return "a parked child needs a human decision";
8942
+ case "completed":
8943
+ return "all enrollable children committed";
8944
+ default:
8945
+ return null;
8946
+ }
8947
+ }
8948
+ async function resumeBatch(input, projection) {
8949
+ let existing = readBatchRunState(input.root, input.batch_id);
8950
+ if (!existing)
8951
+ return startBatch(input);
8952
+ try {
8953
+ await validatePersistedRun(input, existing);
8954
+ } catch (error) {
8955
+ const message = error instanceof Error ? error.message : String(error);
8956
+ if (isLineageBreakError(message)) {
8957
+ if (isTerminalBatchState(existing.batch_state))
8958
+ return replayTerminal(input.root, existing);
8959
+ const failed = failPersistedLineage(input.root, existing, message);
8960
+ return finalize(input.root, failed, message, "");
8961
+ }
8962
+ throw error;
8963
+ }
8964
+ if (isTerminalBatchState(existing.batch_state))
8965
+ return replayTerminal(input.root, existing);
8966
+ if (existing.batch_state === "needs_human")
8967
+ return startBatch(input);
8968
+ const driven = existing.children.find((child) => child.state === "enrolled" || child.state === "settled");
8969
+ if (driven) {
8970
+ if (driven.state === "enrolled") {
8971
+ const drivenDrift = externalHeadDriftMessage(input.root, existing);
8972
+ if (drivenDrift) {
8973
+ const failed = failPersistedLineage(input.root, existing, drivenDrift);
8974
+ return finalize(input.root, failed, drivenDrift, "");
8975
+ }
8976
+ const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, driven.task_id), driven.task_id);
8977
+ const holdsClaim = fresh.claim !== null && fresh.claim.task_id === driven.task_id && input.kernel.ownsTaskClaim(driven.task_id);
8978
+ if (holdsClaim === false && fresh.claim !== null && fresh.claim.task_id === driven.task_id && !input.kernel.ownsTaskClaim(driven.task_id)) {
8979
+ skipDependents(existing, driven.task_id, `dependency ${driven.task_id} parked`);
8980
+ existing = writeBatchRunState(input.root, {
8981
+ ...existing,
8982
+ batch_state: "needs_human",
8983
+ children: existing.children.map((c) => c.task_id === driven.task_id ? { ...c, state: "needs_human", reason: "claim held by another batch" } : c)
8984
+ });
8985
+ return finalize(input.root, existing, "claim held by another batch", "needs-human-attention");
8986
+ }
8987
+ if (!holdsClaim) {
8988
+ const settledRemotely = fresh.projection.lifecycle === "done" && fresh.projection.completion_ready === true;
8989
+ if (settledRemotely) {
8990
+ existing = writeBatchRunState(input.root, {
8991
+ ...existing,
8992
+ children: existing.children.map((c) => c.task_id === driven.task_id ? { ...c, state: "settled", reason: "crash after settlement; resuming at commit" } : c)
8993
+ });
8994
+ } else {
8995
+ existing = writeBatchRunState(input.root, {
8996
+ ...existing,
8997
+ children: existing.children.map((c) => c.task_id === driven.task_id ? { ...c, state: "pending", reason: "enrollment did not complete; re-enrolling" } : c)
8998
+ });
8999
+ }
9000
+ }
9001
+ }
9002
+ const current = existing.children.find((c) => c.task_id === driven.task_id) ?? driven;
9003
+ if (current.state === "settled" || current.state === "enrolled") {
9004
+ try {
9005
+ const driven2 = await driveInterruptedChild(input, current);
9006
+ if (driven2)
9007
+ return driven2;
9008
+ } catch (error) {
9009
+ if (error instanceof BatchCommitAbortError) {
9010
+ const failed = readBatchRunState(input.root, input.batch_id);
9011
+ return finalize(input.root, failed, error.message, "");
9012
+ }
9013
+ throw error;
9014
+ }
9015
+ }
9016
+ existing = readBatchRunState(input.root, input.batch_id);
9017
+ if (isTerminalBatchState(existing.batch_state))
9018
+ return replayTerminal(input.root, existing);
9019
+ }
9020
+ return startBatch(input);
9021
+ }
9022
+
9023
+ class BatchCommitAbortError extends Error {
9024
+ }
9025
+ async function driveInterruptedChild(input, child) {
9026
+ let record = readBatchRunState(input.root, input.batch_id);
9027
+ const persist = () => {
9028
+ record = writeBatchRunState(input.root, record);
9029
+ };
9030
+ if (record.batch_state === "prepared") {
9031
+ record.batch_state = "running";
9032
+ persist();
9033
+ }
9034
+ let head = record.commits.length ? record.commits[record.commits.length - 1] : record.base_head;
9035
+ while (child.state === "enrolled" || child.state === "settled") {
9036
+ if (child.state === "settled") {
9037
+ let existing = null;
9038
+ try {
9039
+ existing = input.git?.lookupBatchCommit ? await input.git.lookupBatchCommit(input.root, child.task_id, input.batch_id, head, record.branch) : input.kernel.lookupBatchCommit ? await input.kernel.lookupBatchCommit(input.root, child.task_id, input.batch_id, head, record.branch) : await lookupBatchCommit({
9040
+ root: input.root,
9041
+ taskId: child.task_id,
9042
+ batchId: input.batch_id,
9043
+ expectedHead: head,
9044
+ branch: record.branch
9045
+ });
9046
+ } catch (error) {
9047
+ const message = error instanceof Error ? error.message : String(error);
9048
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: `commit lookup failed: ${message}` } : c);
9049
+ skipDependents(record, child.task_id, `dependency ${child.task_id} failed to commit`);
9050
+ const isLineageError = message.includes("batch_head_lineage_broken") || message.includes("lineage");
9051
+ record.batch_state = isLineageError ? "failed" : "needs_human";
9052
+ persist();
9053
+ throw new BatchCommitAbortError(`commit lookup failed: ${message}`);
9054
+ }
9055
+ const planChild = input.children.find((c) => c.task_id === child.task_id);
9056
+ const intentPath = planChild?.intent_path ?? undefined;
9057
+ const doCommit = async () => {
9058
+ if (input.git?.commitChild) {
9059
+ return input.git.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath);
9060
+ }
9061
+ if (input.kernel.commitChild) {
9062
+ return input.kernel.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath);
9063
+ }
9064
+ return commitBatchChild({
9065
+ root: input.root,
9066
+ taskId: child.task_id,
9067
+ batchId: input.batch_id,
9068
+ expectedHead: head,
9069
+ branch: record.branch,
9070
+ intentPath
9071
+ });
9072
+ };
9073
+ const adopted = existing ?? await doCommit().catch((error) => {
9074
+ const message = error instanceof Error ? error.message : String(error);
9075
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
9076
+ skipDependents(record, child.task_id, `dependency ${child.task_id} failed to commit`);
9077
+ record.batch_state = "failed";
9078
+ persist();
9079
+ throw new BatchCommitAbortError(message);
9080
+ });
9081
+ const { commit } = adopted;
9082
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "committed", commit } : c);
9083
+ record.commits.push(commit);
9084
+ head = commit;
9085
+ persist();
9086
+ child = { ...child, state: "committed", commit };
9087
+ continue;
9088
+ }
9089
+ const terminal = await input.kernel.advanceTask(input.root, child.task_id);
9090
+ if (terminal.state === "completed") {
9091
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "settled", reason: null } : c);
9092
+ record.consecutive_qa_failures = 0;
9093
+ persist();
9094
+ try {
9095
+ const planChild = input.children.find((c) => c.task_id === child.task_id);
9096
+ const intentPath = planChild?.intent_path ?? undefined;
9097
+ const { commit } = input.git?.commitChild ? await input.git.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath) : input.kernel.commitChild ? await input.kernel.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath) : await commitBatchChild({
9098
+ root: input.root,
9099
+ taskId: child.task_id,
9100
+ batchId: input.batch_id,
9101
+ expectedHead: head,
9102
+ branch: record.branch,
9103
+ intentPath
9104
+ });
9105
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "committed", commit } : c);
9106
+ record.commits.push(commit);
9107
+ head = commit;
9108
+ persist();
9109
+ child = { ...child, state: "committed", commit };
9110
+ } catch (error) {
9111
+ const message = error instanceof Error ? error.message : String(error);
9112
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
9113
+ skipDependents(record, child.task_id, `dependency ${child.task_id} failed to commit`);
9114
+ record.batch_state = "failed";
9115
+ persist();
9116
+ return finalize(input.root, record, message, "");
9117
+ }
9118
+ } else if (terminal.state === "review_ready") {
9119
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, reason: `review reservation ${terminal.operation_id} open` } : c);
9120
+ persist();
9121
+ return reportFor(record, `child ${child.task_id} holds an open Review reservation`, "Submit the reserved foreground Review verdict, then call startBatch again to continue.");
9122
+ } else if (terminal.state === "rework" && terminal.operation === "qa") {
9123
+ record.consecutive_qa_failures += 1;
9124
+ if (record.consecutive_qa_failures >= record.budget.qa_failure_limit) {
9125
+ const reason = `QA failure limit reached: ${terminal.summary}`;
9126
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
9127
+ record.batch_state = "needs_human";
9128
+ skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
9129
+ persist();
9130
+ return finalize(input.root, record, reason, "");
9131
+ }
9132
+ persist();
9133
+ } else {
9134
+ const reason = terminal.state === "stopped" ? "Kernel reported the child stopped" : terminal.state === "rework" ? terminal.summary : terminal.reason;
9135
+ record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
9136
+ record.batch_state = "needs_human";
9137
+ skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
9138
+ persist();
9139
+ return finalize(input.root, record, reason, "");
9140
+ }
9141
+ }
9142
+ return null;
9143
+ }
9144
+
9145
+ // plugins/immune-brain/runtime/claude/kernel_ports.ts
9146
+ function diffSnapshotOf(root, record) {
9147
+ if (record.contract === "assurance_kernel/task_record/v4") {
9148
+ if (!record.git_base_head)
9149
+ throw new Error("TaskRecord v4 is missing git_base_head");
9150
+ return taskRevisionIdentity(root, record.intent_snapshot.scope_hint, record.git_base_head);
9151
+ }
9152
+ return taskDiffIdentity(root, record.intent_snapshot.scope_hint);
9153
+ }
9154
+ function diffHashOf(root, record) {
9155
+ return diffSnapshotOf(root, record).diff_hash;
9156
+ }
9157
+ function readTaskIntentForRecord(root, taskId) {
9158
+ const currentPath = readTaskRecordRaw(root, taskId).record?.intent_ref?.path;
9159
+ return readTaskIntent(root, taskId, currentPath);
9160
+ }
9161
+ function extractVerdictJson(input) {
9162
+ if (typeof input === "string") {
9163
+ const cleaned = input.split(`
9164
+ `).map((line) => line.trim()).filter((line) => line.startsWith("{") && line.endsWith("}")).join("");
9165
+ if (!cleaned)
9166
+ return null;
9167
+ try {
9168
+ return JSON.parse(cleaned);
9169
+ } catch {
9170
+ return null;
9171
+ }
9172
+ }
9173
+ if (typeof input === "object" && input !== null && !Array.isArray(input))
9174
+ return input;
9175
+ return null;
9176
+ }
9177
+ function verdictFingerprint(raw) {
9178
+ return JSON.stringify({
9179
+ contract: raw.contract ?? null,
9180
+ role: raw.role ?? null,
9181
+ task_id: raw.task_id ?? null,
9182
+ snapshot_digest: raw.snapshot_digest ?? null,
9183
+ decision: raw.decision ?? null,
9184
+ approval: raw.approval ?? null,
9185
+ findings: raw.findings ?? null
9186
+ });
9187
+ }
9188
+ async function submitClaudeReview(host, coordinator, ctx, taskId, verdictInput) {
9189
+ if (verdictInput === undefined)
9190
+ throw new Error("verdict is required");
9191
+ const observed = host.inspectReviewForTask(taskId);
9192
+ if (!observed.ok) {
9193
+ if (observed.release)
9194
+ return coordinator.abandonReview(taskId, observed.reason);
9195
+ return { state: "blocked", reason: observed.reason };
9196
+ }
9197
+ const parentValid = coordinator.isReviewVerdictValid(taskId, verdictInput);
9198
+ if (!parentValid)
9199
+ return coordinator.submitReview(taskId, ctx, verdictInput);
9200
+ const receiptValid = coordinator.isReviewVerdictValid(taskId, observed.receipt.result);
9201
+ if (!receiptValid) {
9202
+ return coordinator.abandonReview(taskId, "reviewer receipt is not a valid verdict");
9203
+ }
9204
+ const parentJson = extractVerdictJson(verdictInput);
9205
+ const receiptJson = extractVerdictJson(observed.receipt.result);
9206
+ if (verdictFingerprint(parentJson) !== verdictFingerprint(receiptJson)) {
9207
+ return { state: "blocked", reason: "parent verdict does not match reviewer receipt" };
9208
+ }
9209
+ return coordinator.submitReview(taskId, ctx, verdictInput);
9210
+ }
9211
+ function stopReason(value) {
9212
+ return typeof value === "string" && value.length > 0 ? value : "user stop";
9213
+ }
9214
+ function assertProjectionBinding(before, after, allowDiffChange = false) {
9215
+ const fields = allowDiffChange ? ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash"] : ["record_revision", "workspace_revision", "intent_revision", "intent_content_hash", "diff_hash"];
9216
+ if (before.error || !before.claim || after.error || !after.claim || before.claim.task_id !== after.claim.task_id || fields.some((field) => before.projection[field] !== after.projection[field])) {
9217
+ throw new Error("Task changed after native confirmation; authority aborted before capability issuance");
9218
+ }
9219
+ }
9220
+ function qaOutcomes(record) {
9221
+ return Object.fromEntries(record.attestations.filter((item) => item.kind === "qa").flatMap((item) => item.acceptance_results).map((result2) => [result2.acceptance_id, { status: result2.status, summary: result2.summary }]));
9222
+ }
9223
+ async function ensureClaudeReviewRevision(root, taskId, projection) {
9224
+ const current = await readTaskRecord(root, taskId);
9225
+ const record = current.record;
9226
+ if (!record)
9227
+ throw new Error(`task ${taskId} has no TaskRecord`);
9228
+ if (current.revision !== projection.projection.record_revision)
9229
+ throw new Error("TaskRecord changed before Review revision preparation");
9230
+ if (record.contract !== "assurance_kernel/task_record/v4")
9231
+ return null;
9232
+ if (!record.git_base_head)
9233
+ throw new Error("Review revision requires a TaskRecord v4 git_base_head");
9234
+ const manifest = captureReviewManifest(root, {
9235
+ taskId,
9236
+ baseHead: record.git_base_head,
9237
+ scopeHint: record.intent_snapshot.scope_hint,
9238
+ expectedDiffHash: projection.projection.diff_hash,
9239
+ intentRevision: projection.projection.intent_revision,
9240
+ intentContentHash: projection.projection.intent_content_hash,
9241
+ recordRevision: projection.projection.record_revision,
9242
+ workspaceRevision: projection.projection.workspace_revision,
9243
+ lifecycle: projection.projection.lifecycle,
9244
+ artifactState: projection.projection.artifact_state,
9245
+ risk: record.intent_snapshot.risk,
9246
+ outcomes: qaOutcomes(record)
9247
+ });
9248
+ return {
9249
+ contract: "assurance_kernel/review_revision/v1",
9250
+ base_head: manifest.base_head,
9251
+ review_tree: manifest.review_tree,
9252
+ review_commit: manifest.review_commit,
9253
+ review_ref: manifest.review_ref,
9254
+ diff_hash: manifest.diff_hash,
9255
+ manifest_digest: manifest.manifest_digest
9256
+ };
9257
+ }
9258
+ async function buildAssuranceSnapshot(root, taskId, role, projection, runner) {
9259
+ const read = await readTaskRecord(root, taskId);
6801
9260
  const record = read.record;
6802
9261
  if (!record || read.revision !== projection.projection.record_revision)
6803
9262
  throw new Error("TaskRecord changed before assurance snapshot capture");
@@ -6824,7 +9283,7 @@ async function buildAssuranceSnapshot(root, taskId, role, projection, runner) {
6824
9283
  outcomes: qaOutcomes(record)
6825
9284
  }) : null;
6826
9285
  const dirtyFiles = reviewManifest ? Object.keys(reviewManifest.changed_paths) : reviewBundle ? Object.keys(reviewBundle.dirty_files) : [];
6827
- const snapshot = {
9286
+ const snapshot2 = {
6828
9287
  contract: "assurance_kernel/assurance_snapshot/v2",
6829
9288
  task_id: taskId,
6830
9289
  role,
@@ -6853,7 +9312,7 @@ async function buildAssuranceSnapshot(root, taskId, role, projection, runner) {
6853
9312
  }
6854
9313
  } : {}
6855
9314
  };
6856
- return { snapshot, descriptors, reviewBundle, reviewManifest };
9315
+ return { snapshot: snapshot2, descriptors, reviewBundle, reviewManifest };
6857
9316
  }
6858
9317
  function stagePlanningArtifactTransition(root, record) {
6859
9318
  const intentActive = record.intent_ref.path.replace("docs/plans/archive/", "docs/plans/");
@@ -6864,7 +9323,7 @@ function stagePlanningArtifactTransition(root, record) {
6864
9323
  intentArchive,
6865
9324
  ...specActive ? [specActive, specActive.replace("docs/specs/", "docs/specs/archive/")] : []
6866
9325
  ];
6867
- const paths = candidates.filter((path) => existsSync3(join7(root, path)) || execFileSync4("git", ["ls-files", "--cached", "--", path], { cwd: root, encoding: "utf8" }).trim().length > 0);
9326
+ const paths = candidates.filter((path) => existsSync6(join10(root, path)) || execFileSync4("git", ["ls-files", "--cached", "--", path], { cwd: root, encoding: "utf8" }).trim().length > 0);
6868
9327
  if (paths.length === 0)
6869
9328
  return;
6870
9329
  execFileSync4("git", ["add", "--", ...paths], { cwd: root, stdio: ["ignore", "pipe", "pipe"] });
@@ -6894,7 +9353,7 @@ async function mintCapability(registry, input) {
6894
9353
  actor_id: input.actor_id,
6895
9354
  confirmation_ref: input.confirmation_ref,
6896
9355
  expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
6897
- findings_digest: input.action_kind === "request_rework" ? await findingsDigest(input.findings) : null
9356
+ findings_digest: input.action_kind === "request_rework" ? findingsDigestV2(input.findings) : null
6898
9357
  };
6899
9358
  return registry.issue(binding);
6900
9359
  }
@@ -6902,6 +9361,79 @@ function throwIfCancelled(signal) {
6902
9361
  if (signal?.aborted)
6903
9362
  throw new NativeAuthorityError("user_cancelled", "Tool call was cancelled");
6904
9363
  }
9364
+ function syncIsOwnBatchClaim(cwd, existingBatch, taskId, batchBranch) {
9365
+ let claim = null;
9366
+ let workspace = null;
9367
+ try {
9368
+ claim = JSON.parse(readFileSync8(join10(cwd, ".imm", "state", "active-claim.json"), "utf8"));
9369
+ workspace = JSON.parse(readFileSync8(join10(cwd, ".imm", "state", "workspace.json"), "utf8"));
9370
+ } catch {
9371
+ return false;
9372
+ }
9373
+ const currentTaskId = workspace?.state?.current_working || (claim?.lifecycle_status === "active" ? claim?.task_id : null);
9374
+ if (currentTaskId !== taskId || !claim)
9375
+ return false;
9376
+ const branch = spawnSync6("git", ["-C", cwd, "branch", "--show-current"], { encoding: "utf8" }).stdout.trim();
9377
+ if (branch !== batchBranch)
9378
+ return false;
9379
+ const childInBatch = existingBatch.children.find((c) => c.task_id === taskId);
9380
+ if (!childInBatch || !(childInBatch.state === "enrolled" || childInBatch.state === "needs_human")) {
9381
+ return false;
9382
+ }
9383
+ let rec = null;
9384
+ try {
9385
+ rec = JSON.parse(readFileSync8(join10(cwd, ".imm", "state", "tasks", `${taskId}.json`), "utf8"));
9386
+ } catch {
9387
+ return false;
9388
+ }
9389
+ const lineageHeads = [existingBatch.base_head].concat(Array.isArray(existingBatch.commits) ? existingBatch.commits : []);
9390
+ if (!lineageHeads.includes(rec.git_base_head))
9391
+ return false;
9392
+ if (claim.enrollment_event_id !== `enroll-${taskId}-${claim.created_at}`)
9393
+ return false;
9394
+ const createdAt = Date.parse(claim.created_at);
9395
+ if (!Number.isFinite(createdAt) || createdAt > Date.parse(existingBatch.updated_at))
9396
+ return false;
9397
+ if (claim.task_id !== taskId || claim.lifecycle_status !== "active")
9398
+ return false;
9399
+ if (claim.intent_revision !== rec.intent_snapshot?.revision)
9400
+ return false;
9401
+ if (claim.intent_content_hash !== rec.intent_ref?.content_hash)
9402
+ return false;
9403
+ return true;
9404
+ }
9405
+ function findExistingActiveBatch(root, initiativeSlug) {
9406
+ const batchesDir = join10(root, ".imm", "state", "batches");
9407
+ if (!existsSync6(batchesDir))
9408
+ return null;
9409
+ const files = readdirSync2(batchesDir);
9410
+ for (const file of files) {
9411
+ if (!file.endsWith(".json"))
9412
+ continue;
9413
+ let record;
9414
+ try {
9415
+ record = JSON.parse(readFileSync8(join10(batchesDir, file), "utf8"));
9416
+ } catch {
9417
+ return { corrupt: true, path: file };
9418
+ }
9419
+ if (record.contract === "assurance_kernel/batch_run_state/v1" && record.initiative_slug === initiativeSlug) {
9420
+ const validStates = new Set([
9421
+ "prepared",
9422
+ "running",
9423
+ "needs_human",
9424
+ "completed",
9425
+ "budget_stopped",
9426
+ "failed",
9427
+ "rejected"
9428
+ ]);
9429
+ if (typeof record.batch_id !== "string" || typeof record.base_head !== "string" || !Array.isArray(record.children) || !validStates.has(record.batch_state)) {
9430
+ return { corrupt: true, path: file };
9431
+ }
9432
+ return record;
9433
+ }
9434
+ }
9435
+ return null;
9436
+ }
6905
9437
 
6906
9438
  class ClaudeRuntime {
6907
9439
  host;
@@ -6913,12 +9445,19 @@ class ClaudeRuntime {
6913
9445
  hostVersion;
6914
9446
  mutationRegistry = null;
6915
9447
  enrollmentRegistry = createEnrollmentAuthorityRegistry();
9448
+ batchRegistry = createBatchAuthorityRegistry();
6916
9449
  app = null;
9450
+ batchKernel;
9451
+ batchGit;
9452
+ readInitiative;
6917
9453
  constructor(options) {
6918
9454
  this.cwd = options.cwd;
6919
9455
  this.env = options.env ?? process.env;
6920
9456
  this.interactive = options.interactive ?? true;
6921
9457
  this.requestConfirmation = options.requestConfirmation;
9458
+ this.batchKernel = options.batchKernel;
9459
+ this.batchGit = options.batchGit;
9460
+ this.readInitiative = options.readInitiative;
6922
9461
  this.host = options.host ?? new ClaudeReviewHost(new FileHookEventLog);
6923
9462
  this.coordinator = new AssuranceCoordinator({
6924
9463
  ...this.createKernelPorts(),
@@ -6950,7 +9489,7 @@ class ClaudeRuntime {
6950
9489
  frozenRunner: async () => resolveBunRunner(),
6951
9490
  buildAssurance: (root, taskId, role, projection, runner) => buildAssuranceSnapshot(root, taskId, role, projection, runner),
6952
9491
  ensureReviewRevision: (root, taskId, projection) => ensureClaudeReviewRevision(root, taskId, projection),
6953
- runQa: (snapshot, descriptors, runner, options) => runDeterministicQa(snapshot, descriptors, runner, options),
9492
+ runQa: (snapshot2, descriptors, runner, options) => runDeterministicQa(snapshot2, descriptors, runner, options),
6954
9493
  writeReviewEvidence: (input) => writeNativeReviewEvidence(input.evidence),
6955
9494
  applyVerdict: (ctx, input) => this.applyVerdict(ctx, input),
6956
9495
  applyOrdinaryOperation: (ctx, input) => this.executeOrdinary(ctx, input)
@@ -6973,16 +9512,16 @@ class ClaudeRuntime {
6973
9512
  throw new Error(`unsupported native operation ${operation}`);
6974
9513
  if (!this.requestConfirmation)
6975
9514
  throw new NativeAuthorityError("interaction_not_opened", "native confirmation port is unavailable");
6976
- const result = await this.requestConfirmation({ operation, taskId: meta.taskId, toolCallId: meta.toolCallId, signal: meta.signal, ...binding });
9515
+ const result2 = await this.requestConfirmation({ operation, taskId: meta.taskId, toolCallId: meta.toolCallId, signal: meta.signal, ...binding });
6977
9516
  throwIfCancelled(meta.signal);
6978
- const gate = evaluateNativeGate({ operation, interactive, decision: result.decision });
9517
+ const gate = evaluateNativeGate({ operation, interactive, decision: result2.decision });
6979
9518
  if (!gate.ok)
6980
9519
  throw gate.error;
6981
9520
  return {
6982
9521
  confirmation_ref: confirmationRef({
6983
9522
  connectionId: meta.sessionId,
6984
9523
  toolCallId: meta.toolCallId,
6985
- requestId: result.requestId,
9524
+ requestId: result2.requestId,
6986
9525
  operation,
6987
9526
  taskId: meta.taskId,
6988
9527
  ...binding
@@ -7048,6 +9587,17 @@ class ClaudeRuntime {
7048
9587
  operation: { op: "resolve_finding", finding_id: findingId, actor_id: "executor" }
7049
9588
  });
7050
9589
  }
9590
+ async refuteFinding(taskId, findingId, attestationId) {
9591
+ return this.executeOrdinary({ cwd: this.cwd }, {
9592
+ taskId,
9593
+ operation: {
9594
+ op: "refute_finding",
9595
+ finding_id: findingId,
9596
+ attestation_id: attestationId,
9597
+ actor_id: "executor"
9598
+ }
9599
+ });
9600
+ }
7051
9601
  async authorize(taskId, operation, meta, extra = {}) {
7052
9602
  if (operation === "repair_authority_state") {
7053
9603
  const authority = reconcileKernelAuthority(this.cwd, taskId);
@@ -7086,8 +9636,8 @@ class ClaudeRuntime {
7086
9636
  throw new Error("approve_breaking_intent_revision requires next_intent");
7087
9637
  const nextIntentHash = nextIntent ? canonicalIntentHash(nextIntent) : undefined;
7088
9638
  const nextIntentRef = nextIntent ? { path: `docs/plans/${nextIntent.task_id}.intent.json`, content_hash: nextIntentHash } : undefined;
7089
- const sidecar = nextIntent ? join7(this.cwd, priorIntent.intent_ref.path) : undefined;
7090
- const priorBytes = sidecar ? readFileSync7(sidecar) : undefined;
9639
+ const sidecar = nextIntent ? join10(this.cwd, priorIntent.intent_ref.path) : undefined;
9640
+ const priorBytes = sidecar ? readFileSync8(sidecar) : undefined;
7091
9641
  const priorIndexState = sidecar ? execFileSync4("git", ["ls-files", "--stage", "-z", "--", priorIntent.intent_ref.path], {
7092
9642
  cwd: this.cwd,
7093
9643
  stdio: ["ignore", "pipe", "pipe"]
@@ -7095,7 +9645,7 @@ class ClaudeRuntime {
7095
9645
  const restoreStagedIntent = () => {
7096
9646
  if (!sidecar || !priorBytes || !priorIndexState)
7097
9647
  return;
7098
- writeFileSync3(sidecar, priorBytes);
9648
+ writeFileSync5(sidecar, priorBytes);
7099
9649
  execFileSync4("git", ["update-index", "--force-remove", "--", priorIntent.intent_ref.path], {
7100
9650
  cwd: this.cwd,
7101
9651
  stdio: ["ignore", "pipe", "pipe"]
@@ -7112,7 +9662,7 @@ class ClaudeRuntime {
7112
9662
  let gate;
7113
9663
  try {
7114
9664
  if (sidecar && nextIntent) {
7115
- writeFileSync3(sidecar, `${JSON.stringify(nextIntent, null, 2)}
9665
+ writeFileSync5(sidecar, `${JSON.stringify(nextIntent, null, 2)}
7116
9666
  `);
7117
9667
  execFileSync4("git", ["add", "--", priorIntent.intent_ref.path], { cwd: this.cwd, stdio: ["ignore", "pipe", "pipe"] });
7118
9668
  const preparedRecord = await readTaskRecord(this.cwd, taskId);
@@ -7175,7 +9725,7 @@ class ClaudeRuntime {
7175
9725
  ...op === "stop" ? { reason: stopReason(extra.reason) } : {}
7176
9726
  });
7177
9727
  throwIfCancelled(meta.signal);
7178
- const result = app.execute({
9728
+ const result2 = app.execute({
7179
9729
  root: this.cwd,
7180
9730
  task_id: taskId,
7181
9731
  operation: {
@@ -7191,8 +9741,8 @@ class ClaudeRuntime {
7191
9741
  now
7192
9742
  });
7193
9743
  if (op === "stop" || op === "authorize_rework" || op === "approve_breaking_intent_revision")
7194
- stagePlanningArtifactTransition(this.cwd, result.record);
7195
- return result;
9744
+ stagePlanningArtifactTransition(this.cwd, result2.record);
9745
+ return result2;
7196
9746
  } catch (error) {
7197
9747
  if (sidecar && priorBytes && priorIndexState) {
7198
9748
  const current = await readTaskRecord(this.cwd, taskId);
@@ -7211,20 +9761,12 @@ class ClaudeRuntime {
7211
9761
  this.coordinator.commitInvocation(input.invocation);
7212
9762
  const settlement = apply();
7213
9763
  input.hooks?.onCommit?.();
7214
- const result = await settlement;
9764
+ const result2 = await settlement;
7215
9765
  await input.hooks?.afterCommit?.();
7216
- return result;
9766
+ return result2;
7217
9767
  };
7218
9768
  if (input.verdict.decision === "rework") {
7219
- const findings = input.verdict.findings.map((finding) => ({
7220
- id: finding.id,
7221
- kind: finding.kind,
7222
- status: "open",
7223
- acceptance_id: finding.acceptance_id,
7224
- source: "review",
7225
- review_round: null,
7226
- summary: finding.summary
7227
- }));
9769
+ const findings = reviewReworkFindings(input.verdict);
7228
9770
  const capability2 = await mintCapability(registry, {
7229
9771
  authority_kind: input.snapshot.role,
7230
9772
  task_id: input.taskId,
@@ -7239,7 +9781,7 @@ class ClaudeRuntime {
7239
9781
  confirmation_ref: `claude:${input.actorId}`
7240
9782
  });
7241
9783
  await input.hooks?.beforeCommit?.();
7242
- const result = await commitAndApply(async () => app.execute({
9784
+ const result2 = await commitAndApply(async () => app.execute({
7243
9785
  root: ctx.cwd,
7244
9786
  task_id: input.taskId,
7245
9787
  operation: { op: "request_rework", capability: capability2, findings, actor_id: input.actorId },
@@ -7247,11 +9789,11 @@ class ClaudeRuntime {
7247
9789
  diffProvider: diffSnapshotOf,
7248
9790
  now
7249
9791
  }));
7250
- stagePlanningArtifactTransition(ctx.cwd, result.record);
9792
+ stagePlanningArtifactTransition(ctx.cwd, result2.record);
7251
9793
  return;
7252
9794
  }
7253
9795
  const approval = {
7254
- id: `approval-${input.snapshot.role}-${randomUUID5().slice(0, 8)}`,
9796
+ id: `approval-${input.snapshot.role}-${randomUUID7().slice(0, 8)}`,
7255
9797
  kind: input.snapshot.role === "qa" ? "qa" : "review",
7256
9798
  authority_role: input.snapshot.role === "qa" ? "qa" : "reviewer",
7257
9799
  task_revision: input.snapshot.intent_revision,
@@ -7288,13 +9830,13 @@ class ClaudeRuntime {
7288
9830
  const { app } = await this.authority();
7289
9831
  const operation = input.operation.op === "revise_intent" ? { ...input.operation, next_intent: await parseTaskIntentV1(input.operation.next_intent) } : input.operation;
7290
9832
  const priorIntent = await readTaskIntentForRecord(ctx.cwd, input.taskId);
7291
- const sidecar = join7(ctx.cwd, priorIntent.intent_ref.path);
7292
- const priorBytes = operation.op === "revise_intent" ? readFileSync7(sidecar) : null;
9833
+ const sidecar = join10(ctx.cwd, priorIntent.intent_ref.path);
9834
+ const priorBytes = operation.op === "revise_intent" ? readFileSync8(sidecar) : null;
7293
9835
  try {
7294
9836
  if (priorBytes)
7295
- writeFileSync3(sidecar, `${JSON.stringify(operation.next_intent, null, 2)}
9837
+ writeFileSync5(sidecar, `${JSON.stringify(operation.next_intent, null, 2)}
7296
9838
  `);
7297
- const result = await app.execute({
9839
+ const result2 = await app.execute({
7298
9840
  root: ctx.cwd,
7299
9841
  task_id: input.taskId,
7300
9842
  operation,
@@ -7303,17 +9845,588 @@ class ClaudeRuntime {
7303
9845
  now: new Date().toISOString()
7304
9846
  });
7305
9847
  if (operation.op === "freeze_artifacts" || operation.op === "stop")
7306
- stagePlanningArtifactTransition(ctx.cwd, result.record);
7307
- return result;
9848
+ stagePlanningArtifactTransition(ctx.cwd, result2.record);
9849
+ return result2;
7308
9850
  } catch (error) {
7309
9851
  if (priorBytes) {
7310
9852
  const current = await readTaskRecord(ctx.cwd, input.taskId);
7311
9853
  if (current.record?.intent_snapshot.revision === priorIntent.intent.revision)
7312
- writeFileSync3(sidecar, priorBytes);
9854
+ writeFileSync5(sidecar, priorBytes);
7313
9855
  }
7314
9856
  throw error;
7315
9857
  }
7316
9858
  }
9859
+ async startUnattendedBatch(initiativeSlug, meta) {
9860
+ throwIfCancelled(meta.signal);
9861
+ const probe = probeHost(this.env, process.platform, this.hostVersion);
9862
+ if (!probe.ok)
9863
+ throw new NativeAuthorityError("unsupported_host", probe.reason);
9864
+ const interactive = meta.interactive ?? this.interactive;
9865
+ if (!interactive)
9866
+ throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
9867
+ if (!this.requestConfirmation)
9868
+ throw new NativeAuthorityError("interaction_not_opened", "native confirmation port is unavailable");
9869
+ if (!/^[a-z0-9][a-z0-9._-]{0,127}$/i.test(initiativeSlug)) {
9870
+ return {
9871
+ state: "rejected",
9872
+ reason: `invalid initiative slug: ${initiativeSlug}`,
9873
+ recovery_action: "specify a valid initiative slug and retry in the current Host"
9874
+ };
9875
+ }
9876
+ const existingBatch = findExistingActiveBatch(this.cwd, initiativeSlug);
9877
+ const isResuming = existingBatch !== null;
9878
+ if (existingBatch?.corrupt) {
9879
+ return {
9880
+ state: "blocked",
9881
+ reason: `batch run state is unreadable or invalid: ${existingBatch.path}`,
9882
+ recovery_action: "resolve or remove the invalid batch state file, then retry in the current Host"
9883
+ };
9884
+ }
9885
+ const batchBranch = `imm/${initiativeSlug}`;
9886
+ const workspaceState = readWorkspaceStateRaw(this.cwd);
9887
+ const claim = readBackendClaim(this.cwd);
9888
+ const activeTaskId = workspaceState.state.current_working || (claim?.lifecycle_status === "active" ? claim.task_id : null);
9889
+ const isOwnClaim = isResuming && activeTaskId !== null && syncIsOwnBatchClaim(this.cwd, existingBatch, activeTaskId, batchBranch);
9890
+ if (activeTaskId && !isOwnClaim) {
9891
+ return {
9892
+ state: "blocked",
9893
+ reason: `an active workspace claim already exists for task: ${activeTaskId}`,
9894
+ recovery_action: "resolve or stop the active task before starting a batch in the current Host"
9895
+ };
9896
+ }
9897
+ let baseHead;
9898
+ try {
9899
+ baseHead = readGitHead(this.cwd);
9900
+ } catch (err) {
9901
+ return {
9902
+ state: "rejected",
9903
+ reason: err instanceof Error ? err.message : String(err),
9904
+ recovery_action: "commit working changes and ensure a committed Git HEAD exists in the current Host"
9905
+ };
9906
+ }
9907
+ const branchExists = spawnSync6("git", ["-C", this.cwd, "show-ref", "--verify", "--quiet", `refs/heads/${batchBranch}`]);
9908
+ if (branchExists.status === 0 && !isResuming) {
9909
+ return {
9910
+ state: "rejected",
9911
+ reason: `branch preflight failed: branch refs/heads/${batchBranch} already exists`,
9912
+ recovery_action: "delete or rename the conflicting branch, or commit working changes in the current Host"
9913
+ };
9914
+ }
9915
+ const statusProc = spawnSync6("git", ["-C", this.cwd, "status", "--porcelain=v1", "-z", "--no-renames", "--untracked-files=all"], {
9916
+ encoding: "utf8"
9917
+ });
9918
+ if (statusProc.status !== 0) {
9919
+ return {
9920
+ state: "rejected",
9921
+ reason: "branch preflight failed: git status is unreadable",
9922
+ recovery_action: "check the repository integrity and retry in the current Host"
9923
+ };
9924
+ }
9925
+ const statusEntries = [];
9926
+ for (const entry of statusProc.stdout.split("\x00")) {
9927
+ if (entry.length === 0)
9928
+ continue;
9929
+ statusEntries.push({ code: entry.slice(0, 2), path: entry.slice(3) });
9930
+ }
9931
+ if (statusEntries.length > 0) {
9932
+ if (!isResuming) {
9933
+ return {
9934
+ state: "rejected",
9935
+ reason: "branch preflight failed: working tree is dirty",
9936
+ recovery_action: "delete or rename the conflicting branch, or commit working changes in the current Host"
9937
+ };
9938
+ }
9939
+ const inFlightChild = existingBatch.children.find((c) => c.state === "enrolled" || c.state === "needs_human" || c.state === "settled");
9940
+ let authorizedScope = [];
9941
+ if (inFlightChild) {
9942
+ try {
9943
+ const recordRead = readTaskRecordRaw(this.cwd, inFlightChild.task_id);
9944
+ authorizedScope = recordRead.record?.intent_snapshot?.scope_hint ?? [];
9945
+ } catch {}
9946
+ if (authorizedScope.length === 0 && inFlightChild.state === "settled") {
9947
+ try {
9948
+ const settled = readAuditTaskPair(this.cwd, inFlightChild.task_id)?.record;
9949
+ authorizedScope = settled?.intent_snapshot?.scope_hint ?? [];
9950
+ } catch {}
9951
+ }
9952
+ if (authorizedScope.length === 0) {
9953
+ try {
9954
+ const read = readTaskIntentForRecord(this.cwd, inFlightChild.task_id);
9955
+ authorizedScope = read.intent.scope_hint ?? [];
9956
+ } catch {}
9957
+ }
9958
+ if (authorizedScope.length === 0) {
9959
+ return {
9960
+ state: "rejected",
9961
+ reason: "branch preflight failed: cannot derive the in-flight child's authorized scope",
9962
+ recovery_action: "resolve the child's intent record, then retry in the current Host"
9963
+ };
9964
+ }
9965
+ }
9966
+ const dirtyBytes = statusEntries.some(({ code }) => code === "??" || code[1] !== " ");
9967
+ if (dirtyBytes) {
9968
+ return {
9969
+ state: "rejected",
9970
+ reason: "branch preflight failed: working tree has unstaged or untracked changes",
9971
+ recovery_action: "stage the in-flight changes with git add, then retry in the current Host"
9972
+ };
9973
+ }
9974
+ const outsideScope = statusEntries.some(({ path }) => {
9975
+ if (path.startsWith(".imm/") || path.startsWith("docs/plans/") || path.startsWith("docs/specs/"))
9976
+ return false;
9977
+ return !authorizedScope.some((scopePath) => pathMatchesScope(path, scopePath));
9978
+ });
9979
+ if (outsideScope) {
9980
+ return {
9981
+ state: "rejected",
9982
+ reason: "branch preflight failed: working tree has changes outside the authorized child scope",
9983
+ recovery_action: "commit or unstage changes outside the active task scope, then retry in the current Host"
9984
+ };
9985
+ }
9986
+ }
9987
+ const now = new Date().toISOString();
9988
+ let recoveryChildren = [];
9989
+ let planDigest;
9990
+ let confirmChildrenDetails = [];
9991
+ let confirmExcludedDetails = [];
9992
+ const recoveryRiskByTask = new Map;
9993
+ let budget2 = existingBatch ? existingBatch.budget : { max_children: 10, deadline_at: new Date(Date.now() + 8 * 3600 * 1000).toISOString(), qa_failure_limit: 2 };
9994
+ if (isResuming) {
9995
+ try {
9996
+ recoveryChildren = await Promise.all(existingBatch.children.map(async (c) => {
9997
+ const intentPath = `docs/plans/${c.task_id}.intent.json`;
9998
+ let read = { intent: { revision: 1, risk: "material" }, content_hash: "" };
9999
+ try {
10000
+ const taskRecordRead = readTaskRecordRaw(this.cwd, c.task_id);
10001
+ if (taskRecordRead.record) {
10002
+ read = {
10003
+ intent: taskRecordRead.record.intent_snapshot,
10004
+ content_hash: taskRecordRead.record.intent_ref.content_hash
10005
+ };
10006
+ } else {
10007
+ read = await readTaskIntent(this.cwd, c.task_id, intentPath);
10008
+ }
10009
+ } catch {
10010
+ const archivePath2 = `docs/plans/archive/${c.task_id}.intent.json`;
10011
+ try {
10012
+ read = await readTaskIntent(this.cwd, c.task_id, archivePath2);
10013
+ } catch {
10014
+ read = await readTaskIntent(this.cwd, c.task_id, intentPath);
10015
+ }
10016
+ }
10017
+ recoveryRiskByTask.set(c.task_id, read.intent?.risk ?? "material");
10018
+ const isDone = c.state === "committed" || c.state === "settled";
10019
+ return {
10020
+ task_id: c.task_id,
10021
+ slice_id: c.slice_id,
10022
+ status: isDone ? "already_settled" : "enrollable",
10023
+ blocked_by: [...c.blocked_by],
10024
+ reason: c.reason ?? null,
10025
+ intent_path: intentPath,
10026
+ intent_revision: read.intent.revision,
10027
+ intent_content_hash: read.content_hash
10028
+ };
10029
+ }));
10030
+ } catch (err) {
10031
+ return {
10032
+ state: "rejected",
10033
+ reason: `failed to project batch plan: ${err instanceof Error ? err.message : String(err)}`,
10034
+ recovery_action: "review initiative issues and planning sidecars in the current Host"
10035
+ };
10036
+ }
10037
+ planDigest = computeBatchPlanDigest(recoveryChildren);
10038
+ for (const c of recoveryChildren) {
10039
+ confirmChildrenDetails.push({
10040
+ task_id: c.task_id,
10041
+ slice_id: c.slice_id,
10042
+ risk: recoveryRiskByTask.get(c.task_id) ?? "material"
10043
+ });
10044
+ }
10045
+ } else {
10046
+ let plan;
10047
+ try {
10048
+ plan = await projectBatchPlan(this.cwd, initiativeSlug, { confirmation_time: now }, this.readInitiative ?? observeGithubInitiative);
10049
+ } catch (err) {
10050
+ const msg = err instanceof Error ? err.message : String(err);
10051
+ if (msg.includes("has no enrollable children")) {
10052
+ return {
10053
+ state: "rejected",
10054
+ reason: "empty enrollable child set: no enrollable child tasks found in the initiative plan",
10055
+ recovery_action: "ensure the initiative has uncompleted, non-critical child tasks in the current Host"
10056
+ };
10057
+ }
10058
+ return {
10059
+ state: "rejected",
10060
+ reason: `failed to project batch plan: ${msg}`,
10061
+ recovery_action: "review initiative issues and planning sidecars in the current Host"
10062
+ };
10063
+ }
10064
+ if (plan.enrollable.length === 0) {
10065
+ return {
10066
+ state: "rejected",
10067
+ reason: "empty enrollable child set: no enrollable child tasks found in the initiative plan",
10068
+ recovery_action: "ensure the initiative has uncompleted, non-critical child tasks in the current Host"
10069
+ };
10070
+ }
10071
+ budget2 = plan.budget;
10072
+ const enrollableChildById = new Map(plan.enrollable.map((c) => [c.task_id, c]));
10073
+ recoveryChildren = plan.children.filter((c) => c.status === "enrollable").map((c) => {
10074
+ const digestChild = enrollableChildById.get(c.task_id);
10075
+ return {
10076
+ ...c,
10077
+ blocked_by: digestChild ? [...digestChild.blocked_by] : c.blocked_by
10078
+ };
10079
+ });
10080
+ planDigest = computeBatchPlanDigest(plan.enrollable);
10081
+ for (const c of plan.children.filter((item) => item.status === "enrollable")) {
10082
+ let childRisk = "material";
10083
+ try {
10084
+ const intentRead = await readTaskIntent(this.cwd, c.task_id, c.intent_path ?? undefined);
10085
+ childRisk = intentRead.intent.risk;
10086
+ } catch {}
10087
+ confirmChildrenDetails.push({
10088
+ task_id: c.task_id,
10089
+ slice_id: c.slice_id,
10090
+ risk: childRisk
10091
+ });
10092
+ }
10093
+ confirmExcludedDetails = plan.children.filter((c) => c.status !== "enrollable").map((c) => ({
10094
+ task_id: c.task_id,
10095
+ slice_id: c.slice_id,
10096
+ reason: c.status === "needs_human" && c.reason === "critical" ? "critical" : c.reason ?? c.status
10097
+ }));
10098
+ }
10099
+ const isExistingExpired = isResuming && Date.parse(existingBatch.authorization_expires_at) <= Date.now();
10100
+ const expiresAt = isResuming && !isExistingExpired && existingBatch.batch_state === "running" ? existingBatch.authorization_expires_at : new Date(Date.now() + 10 * 60 * 1000).toISOString();
10101
+ const batchDetails = {
10102
+ initiative_slug: initiativeSlug,
10103
+ batch_branch: batchBranch,
10104
+ children: confirmChildrenDetails,
10105
+ excluded: confirmExcludedDetails,
10106
+ budget: budget2,
10107
+ expires_at: expiresAt
10108
+ };
10109
+ const configuredTimeout = Number(this.env.IMMUNE_BRAIN_BATCH_TIMEOUT_MS);
10110
+ const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : 60000;
10111
+ const timeoutController = new AbortController;
10112
+ const timeoutTimer = setTimeout(() => {
10113
+ timeoutController.abort(new NativeAuthorityError("user_cancelled", "native confirmation timed out"));
10114
+ }, timeoutMs);
10115
+ const elicitationSignal = meta.signal ? AbortSignal.any([meta.signal, timeoutController.signal]) : timeoutController.signal;
10116
+ let confirmationResult;
10117
+ try {
10118
+ confirmationResult = await this.requestConfirmation({
10119
+ operation: "start_unattended_batch",
10120
+ initiativeSlug,
10121
+ toolCallId: meta.toolCallId,
10122
+ planDigest,
10123
+ batchDetails,
10124
+ signal: elicitationSignal
10125
+ });
10126
+ } catch (err) {
10127
+ if (timeoutController.signal.aborted && !meta.signal?.aborted) {
10128
+ return {
10129
+ state: "rejected",
10130
+ reason: "native confirmation timed out waiting for user interaction",
10131
+ recovery_action: "retry through a fresh native gate in the current Host"
10132
+ };
10133
+ }
10134
+ if (meta.signal?.aborted) {
10135
+ return {
10136
+ state: "cancelled",
10137
+ reason: "user cancelled before batch execution",
10138
+ recovery_action: "wait for a fresh literal-user request"
10139
+ };
10140
+ }
10141
+ if (err instanceof NativeAuthorityError) {
10142
+ if (err.reasonCode === "unsupported_host")
10143
+ throw err;
10144
+ if (err.reasonCode === "user_cancelled") {
10145
+ return { state: "cancelled", reason: err.message, recovery_action: err.recoveryAction };
10146
+ }
10147
+ if (err.reasonCode === "user_denied") {
10148
+ return { state: "rejected", reason: err.message, recovery_action: err.recoveryAction };
10149
+ }
10150
+ return { state: "rejected", reason: err.message, recovery_action: err.recoveryAction };
10151
+ }
10152
+ return {
10153
+ state: "rejected",
10154
+ reason: err instanceof Error ? err.message : String(err),
10155
+ recovery_action: "retry through a fresh native gate in the current Host"
10156
+ };
10157
+ } finally {
10158
+ clearTimeout(timeoutTimer);
10159
+ }
10160
+ if (meta.signal?.aborted) {
10161
+ return {
10162
+ state: "cancelled",
10163
+ reason: "user cancelled before batch execution",
10164
+ recovery_action: "wait for a fresh literal-user request"
10165
+ };
10166
+ }
10167
+ if (confirmationResult.decision === "decline") {
10168
+ return {
10169
+ state: "rejected",
10170
+ reason: "native interaction declined",
10171
+ recovery_action: "wait for a fresh literal-user request"
10172
+ };
10173
+ }
10174
+ if (confirmationResult.decision === "cancel") {
10175
+ return {
10176
+ state: "cancelled",
10177
+ reason: "native interaction cancelled",
10178
+ recovery_action: "wait for a fresh literal-user request"
10179
+ };
10180
+ }
10181
+ if (confirmationResult.decision !== "accept") {
10182
+ return {
10183
+ state: "rejected",
10184
+ reason: "native interaction returned no decision",
10185
+ recovery_action: "retry through a fresh native gate in the current Host"
10186
+ };
10187
+ }
10188
+ const postWorkspaceState = readWorkspaceStateRaw(this.cwd);
10189
+ const postClaim = readBackendClaim(this.cwd);
10190
+ const postActiveTaskId = postWorkspaceState.state.current_working || (postClaim?.lifecycle_status === "active" ? postClaim.task_id : null);
10191
+ const isPostOwnClaim = isResuming && postActiveTaskId !== null && syncIsOwnBatchClaim(this.cwd, existingBatch, postActiveTaskId, batchBranch);
10192
+ if (postActiveTaskId && !isPostOwnClaim) {
10193
+ return {
10194
+ state: "blocked",
10195
+ reason: `an active workspace claim appeared during confirmation for task: ${postActiveTaskId}`,
10196
+ recovery_action: "resolve or stop the active task before starting a batch in the current Host"
10197
+ };
10198
+ }
10199
+ if (isResuming) {
10200
+ let recheckedDigest;
10201
+ try {
10202
+ const recheckedChildren = await Promise.all(existingBatch.children.map(async (c) => {
10203
+ const intentPath = `docs/plans/${c.task_id}.intent.json`;
10204
+ let read = { intent: { revision: 1, risk: "material" }, content_hash: "" };
10205
+ try {
10206
+ const taskRecordRead = readTaskRecordRaw(this.cwd, c.task_id);
10207
+ if (taskRecordRead.record) {
10208
+ read = {
10209
+ intent: taskRecordRead.record.intent_snapshot,
10210
+ content_hash: taskRecordRead.record.intent_ref.content_hash
10211
+ };
10212
+ } else {
10213
+ read = await readTaskIntent(this.cwd, c.task_id, intentPath);
10214
+ }
10215
+ } catch {
10216
+ const archivePath2 = `docs/plans/archive/${c.task_id}.intent.json`;
10217
+ try {
10218
+ read = await readTaskIntent(this.cwd, c.task_id, archivePath2);
10219
+ } catch {
10220
+ read = await readTaskIntent(this.cwd, c.task_id, intentPath);
10221
+ }
10222
+ }
10223
+ const isDone = c.state === "committed" || c.state === "settled";
10224
+ return {
10225
+ task_id: c.task_id,
10226
+ slice_id: c.slice_id,
10227
+ status: isDone ? "already_settled" : "enrollable",
10228
+ blocked_by: [...c.blocked_by],
10229
+ reason: c.reason ?? null,
10230
+ intent_path: intentPath,
10231
+ intent_revision: read.intent.revision,
10232
+ intent_content_hash: read.content_hash
10233
+ };
10234
+ }));
10235
+ recheckedDigest = computeBatchPlanDigest(recheckedChildren);
10236
+ } catch (err) {
10237
+ return {
10238
+ state: "rejected",
10239
+ reason: "batch plan became unreadable after native confirmation",
10240
+ recovery_action: "review the current workspace and retry through a fresh native gate in the current Host"
10241
+ };
10242
+ }
10243
+ if (recheckedDigest !== planDigest) {
10244
+ return {
10245
+ state: "rejected",
10246
+ reason: "batch plan changed after native confirmation",
10247
+ recovery_action: "review the current workspace and retry through a fresh native gate in the current Host"
10248
+ };
10249
+ }
10250
+ } else {
10251
+ let revalidatedPlan;
10252
+ try {
10253
+ revalidatedPlan = await projectBatchPlan(this.cwd, initiativeSlug, { confirmation_time: now }, this.readInitiative ?? observeGithubInitiative);
10254
+ } catch (err) {
10255
+ return {
10256
+ state: "rejected",
10257
+ reason: "batch plan became unreadable after native confirmation",
10258
+ recovery_action: "review the current workspace and retry through a fresh native gate in the current Host"
10259
+ };
10260
+ }
10261
+ const revalidatedDigest = computeBatchPlanDigest(revalidatedPlan.enrollable);
10262
+ if (revalidatedDigest !== planDigest) {
10263
+ return {
10264
+ state: "rejected",
10265
+ reason: "batch plan changed after native confirmation",
10266
+ recovery_action: "review the current workspace and retry through a fresh native gate in the current Host"
10267
+ };
10268
+ }
10269
+ }
10270
+ let postHead;
10271
+ try {
10272
+ postHead = readGitHead(this.cwd);
10273
+ } catch (err) {
10274
+ return {
10275
+ state: "rejected",
10276
+ reason: "Git repository became unreadable after native confirmation",
10277
+ recovery_action: "review the current workspace and retry through a fresh native gate in the current Host"
10278
+ };
10279
+ }
10280
+ if (postHead !== baseHead) {
10281
+ return {
10282
+ state: "rejected",
10283
+ reason: "Git HEAD moved after native confirmation",
10284
+ recovery_action: "review the current workspace and retry through a fresh native gate in the current Host"
10285
+ };
10286
+ }
10287
+ const finalWorkspaceState = readWorkspaceStateRaw(this.cwd);
10288
+ const finalClaim = readBackendClaim(this.cwd);
10289
+ const finalActiveTaskId = finalWorkspaceState.state.current_working || (finalClaim?.lifecycle_status === "active" ? finalClaim.task_id : null);
10290
+ const finalIsOwnClaim = isResuming && finalActiveTaskId !== null && syncIsOwnBatchClaim(this.cwd, existingBatch, finalActiveTaskId, batchBranch);
10291
+ if (finalActiveTaskId && !finalIsOwnClaim) {
10292
+ return {
10293
+ state: "blocked",
10294
+ reason: `an active workspace claim appeared during confirmation for task: ${finalActiveTaskId}`,
10295
+ recovery_action: "resolve or stop the active task before starting a batch in the current Host"
10296
+ };
10297
+ }
10298
+ if (meta.signal?.aborted) {
10299
+ return {
10300
+ state: "cancelled",
10301
+ reason: "user cancelled before batch execution",
10302
+ recovery_action: "wait for a fresh literal-user request"
10303
+ };
10304
+ }
10305
+ const batchId = existingBatch ? existingBatch.batch_id : `batch-${initiativeSlug}-${Date.now()}`;
10306
+ const confirmation = confirmationRef({
10307
+ connectionId: meta.sessionId,
10308
+ toolCallId: meta.toolCallId,
10309
+ requestId: confirmationResult.requestId,
10310
+ operation: "start_unattended_batch",
10311
+ initiativeSlug,
10312
+ planDigest
10313
+ });
10314
+ const binding = {
10315
+ batch_id: batchId,
10316
+ initiative_slug: initiativeSlug,
10317
+ plan_digest: planDigest,
10318
+ branch: batchBranch,
10319
+ base_head: isResuming ? existingBatch.base_head : baseHead,
10320
+ budget: budget2,
10321
+ actor_id: "user",
10322
+ confirmation_ref: confirmation,
10323
+ expires_at: expiresAt,
10324
+ nonce: enrollmentNonce()
10325
+ };
10326
+ const capability = this.batchRegistry.issue(binding, recoveryChildren, now);
10327
+ const basePort = this.createBatchKernelPort(this.batchRegistry, capability, binding);
10328
+ const kernelPort = {
10329
+ ...basePort,
10330
+ ownsTaskClaim: (taskId) => {
10331
+ if (isResuming && taskId === activeTaskId) {
10332
+ return syncIsOwnBatchClaim(this.cwd, existingBatch, taskId, batchBranch);
10333
+ }
10334
+ return basePort.ownsTaskClaim(taskId);
10335
+ }
10336
+ };
10337
+ const report = await startBatch({
10338
+ root: this.cwd,
10339
+ batch_id: batchId,
10340
+ initiative_slug: initiativeSlug,
10341
+ registry: this.batchRegistry,
10342
+ capability,
10343
+ children: recoveryChildren,
10344
+ plan_digest: planDigest,
10345
+ base_head: isResuming ? existingBatch.base_head : baseHead,
10346
+ confirmation_time: now,
10347
+ authorization_expires_at: expiresAt,
10348
+ budget: budget2,
10349
+ now,
10350
+ kernel: kernelPort,
10351
+ git: this.batchGit
10352
+ });
10353
+ if (report.batch_state === "rejected") {
10354
+ return {
10355
+ state: "rejected",
10356
+ reason: report.reason ?? "batch run rejected",
10357
+ recovery_action: "delete or rename the conflicting branch, or commit working changes and retry in the current Host"
10358
+ };
10359
+ }
10360
+ return {
10361
+ state: "started",
10362
+ batch_id: batchId,
10363
+ report
10364
+ };
10365
+ }
10366
+ createBatchKernelPort(registry, capability, binding) {
10367
+ const realPort = {
10368
+ enrollTask: async ({ root, task_id, batch }) => {
10369
+ const now = new Date().toISOString();
10370
+ const derived = deriveChildEnrollment(root, batch.registry, {
10371
+ capability: batch.capability,
10372
+ binding,
10373
+ task_id,
10374
+ expected_head: batch.binding.expected_head,
10375
+ now
10376
+ });
10377
+ const enrollmentCapability = this.enrollmentRegistry.issue(derived.binding);
10378
+ const input = {
10379
+ task_id,
10380
+ intent_path: derived.binding.intent_path,
10381
+ intent_revision: derived.binding.intent_revision,
10382
+ preparation_digest: derived.binding.preparation_digest,
10383
+ capability: enrollmentCapability,
10384
+ capability_binding: derived.binding,
10385
+ batch: {
10386
+ registry: batch.registry,
10387
+ capability: batch.capability,
10388
+ binding,
10389
+ expected_head: batch.binding.expected_head
10390
+ },
10391
+ now
10392
+ };
10393
+ const rehearsal = runEnrollmentRehearsal(root, input, enrollmentCapability, this.enrollmentRegistry);
10394
+ if (!rehearsal.rehearsed || rehearsal.evidence.outcome !== "ready") {
10395
+ throw new Error(`Kernel enrollment rehearsal failed: ${rehearsal.evidence.blockers.join("; ")}`);
10396
+ }
10397
+ const enrolled = enrollCanaryTask(root, input, this.enrollmentRegistry);
10398
+ const recordRaw = readTaskRecordRaw(root, task_id);
10399
+ return { record_revision: recordRaw.revision };
10400
+ },
10401
+ advanceTask: async (root, taskId) => {
10402
+ const result2 = await this.coordinator.advance(taskId, { cwd: root });
10403
+ if (result2.state === "completed")
10404
+ return { state: "completed" };
10405
+ if (result2.state === "stopped")
10406
+ return { state: "stopped" };
10407
+ if (result2.state === "rework")
10408
+ return { state: "rework", operation: result2.operation, summary: result2.summary };
10409
+ if (result2.state === "review_ready")
10410
+ return { state: "review_ready", operation_id: result2.operation_id };
10411
+ if (result2.state === "blocked")
10412
+ return { state: "blocked", reason: result2.reason };
10413
+ return { state: "failed", reason: result2.reason ?? "advance failed" };
10414
+ },
10415
+ projectTask: async (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
10416
+ ownsTaskClaim: (taskId) => {
10417
+ return registry.isChildConsumed(capability, taskId);
10418
+ },
10419
+ validateBatchAuthorization: (input) => {
10420
+ return input.registry.inspect(input.capability, input.binding);
10421
+ }
10422
+ };
10423
+ if (!this.batchKernel)
10424
+ return realPort;
10425
+ return {
10426
+ ...realPort,
10427
+ ...this.batchKernel
10428
+ };
10429
+ }
7317
10430
  }
7318
10431
 
7319
10432
  // plugins/immune-brain/runtime/claude/mcp_server.ts
@@ -7326,8 +10439,10 @@ var TOOLS = [
7326
10439
  { name: "request_authorization", description: "Apply exact literal-user authorization.", privileged: true },
7327
10440
  { name: "approve_breaking_intent_revision", description: "Approve a breaking TaskIntent revision.", privileged: true },
7328
10441
  { name: "stop", description: "Stop the active task with literal-user authority.", privileged: true },
10442
+ { name: "start_unattended_batch", description: "Start an unattended serial batch run for an Initiative after native confirmation.", privileged: true },
7329
10443
  { name: "repair_authority_state", description: "Repair a proven recoverable stale backend claim.", privileged: false },
7330
- { name: "resolve_finding", description: "Resolve one open blocking or advisory finding whose cause is fixed and verified.", privileged: false }
10444
+ { name: "resolve_finding", description: "Resolve one open blocking or advisory finding whose cause is fixed and verified.", privileged: false },
10445
+ { name: "refute_finding", description: "Refute one open finding by binding the fresh passing QA attestation that contradicts it.", privileged: false }
7331
10446
  ];
7332
10447
  function listMcpTools() {
7333
10448
  return TOOLS.map((tool) => ({
@@ -7336,13 +10451,16 @@ function listMcpTools() {
7336
10451
  inputSchema: {
7337
10452
  type: "object",
7338
10453
  properties: {
7339
- task_id: { type: "string" },
7340
- ...tool.name === "approve_breaking_intent_revision" ? { next_intent: { type: "object" } } : {},
7341
- ...tool.name === "stop" ? { reason: { type: "string" } } : {},
7342
- ...tool.name === "submit_review" ? { verdict: { type: "object" } } : {},
7343
- ...tool.name === "resolve_finding" ? { finding_id: { type: "string" } } : {}
10454
+ ...tool.name === "start_unattended_batch" ? { initiative_slug: { type: "string" } } : {
10455
+ task_id: { type: "string" },
10456
+ ...tool.name === "approve_breaking_intent_revision" ? { next_intent: { type: "object" } } : {},
10457
+ ...tool.name === "stop" ? { reason: { type: "string" } } : {},
10458
+ ...tool.name === "submit_review" ? { verdict: { type: "object" } } : {},
10459
+ ...tool.name === "resolve_finding" ? { finding_id: { type: "string" } } : {},
10460
+ ...tool.name === "refute_finding" ? { finding_id: { type: "string" }, attestation_id: { type: "string" } } : {}
10461
+ }
7344
10462
  },
7345
- required: tool.name === "submit_review" ? ["task_id", "verdict"] : tool.name === "resolve_finding" ? ["task_id", "finding_id"] : ["task_id"]
10463
+ required: tool.name === "start_unattended_batch" ? ["initiative_slug"] : tool.name === "submit_review" ? ["task_id", "verdict"] : tool.name === "resolve_finding" ? ["task_id", "finding_id"] : tool.name === "refute_finding" ? ["task_id", "finding_id", "attestation_id"] : ["task_id"]
7346
10464
  },
7347
10465
  annotations: tool.privileged ? privilegedAnnotations() : { readOnlyHint: tool.name === "status" }
7348
10466
  }));
@@ -7358,11 +10476,14 @@ function createMcpRuntime(options = {}) {
7358
10476
  host,
7359
10477
  ports: options.ports,
7360
10478
  interactive: options.interactive,
7361
- requestConfirmation: options.requestConfirmation
10479
+ requestConfirmation: options.requestConfirmation,
10480
+ batchKernel: options.batchKernel,
10481
+ batchGit: options.batchGit,
10482
+ readInitiative: options.readInitiative
7362
10483
  });
7363
10484
  let negotiatedVersion;
7364
10485
  let negotiatedInteractive = false;
7365
- const connectionId = randomUUID6();
10486
+ const connectionId = randomUUID8();
7366
10487
  return {
7367
10488
  runtime,
7368
10489
  host,
@@ -7378,6 +10499,30 @@ function createMcpRuntime(options = {}) {
7378
10499
  },
7379
10500
  sessionInteractive: () => negotiatedInteractive,
7380
10501
  async callTool(name, args, meta = {}) {
10502
+ if (name === "start_unattended_batch") {
10503
+ const initiativeSlug = String(args.initiative_slug ?? "");
10504
+ if (!initiativeSlug)
10505
+ throw new Error("initiative_slug is required");
10506
+ if ("native_decision" in args)
10507
+ throw new Error("native_decision cannot be supplied in tool arguments");
10508
+ if (!negotiatedVersion)
10509
+ throw new NativeAuthorityError("unsupported_host", "Claude Code version is unavailable");
10510
+ if (!negotiatedInteractive) {
10511
+ throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
10512
+ }
10513
+ const probe2 = probeHost(options.env ?? process.env, process.platform, negotiatedVersion);
10514
+ if (!probe2.ok)
10515
+ throw new NativeAuthorityError("unsupported_host", probe2.reason);
10516
+ const toolMeta2 = {
10517
+ sessionId: meta.sessionId ?? connectionId,
10518
+ toolCallId: meta.toolCallId ?? `call-${name}`,
10519
+ taskId: initiativeSlug,
10520
+ initiativeSlug,
10521
+ interactive: meta.interactive ?? options.interactive ?? negotiatedInteractive,
10522
+ signal: meta.signal
10523
+ };
10524
+ return runtime.startUnattendedBatch(initiativeSlug, toolMeta2);
10525
+ }
7381
10526
  const taskId = String(args.task_id ?? "");
7382
10527
  if (!taskId)
7383
10528
  throw new Error("task_id is required");
@@ -7414,6 +10559,13 @@ function createMcpRuntime(options = {}) {
7414
10559
  throw new Error("finding_id is required");
7415
10560
  return runtime.resolveFinding(taskId, args.finding_id);
7416
10561
  }
10562
+ if (name === "refute_finding") {
10563
+ if (typeof args.finding_id !== "string" || !args.finding_id)
10564
+ throw new Error("finding_id is required");
10565
+ if (typeof args.attestation_id !== "string" || !args.attestation_id)
10566
+ throw new Error("attestation_id is required");
10567
+ return runtime.refuteFinding(taskId, args.finding_id, args.attestation_id);
10568
+ }
7417
10569
  if (name === "request_authorization" || name === "approve_breaking_intent_revision" || name === "stop" || name === "repair_authority_state") {
7418
10570
  return runtime.authorize(taskId, name, toolMeta, args);
7419
10571
  }
@@ -7497,7 +10649,7 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
7497
10649
  }
7498
10650
  const sessionId = mcp.connectionId;
7499
10651
  const toolCallId = identity?.toolCallId ?? String(message.id ?? "stdio");
7500
- const result = await mcp.callTool(name, params.arguments ?? {}, {
10652
+ const result2 = await mcp.callTool(name, params.arguments ?? {}, {
7501
10653
  sessionId,
7502
10654
  toolCallId,
7503
10655
  interactive,
@@ -7507,7 +10659,7 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
7507
10659
  return {
7508
10660
  jsonrpc: "2.0",
7509
10661
  id: message.id ?? null,
7510
- result: { content: [{ type: "text", text: JSON.stringify(result) }] }
10662
+ result: { content: [{ type: "text", text: JSON.stringify(result2) }] }
7511
10663
  };
7512
10664
  } catch (error) {
7513
10665
  return {
@@ -7525,7 +10677,7 @@ async function handleJsonRpc(message, mcp = createMcpRuntime()) {
7525
10677
  }
7526
10678
  async function writeReply(output, reply) {
7527
10679
  const payload = encodeMessage(reply);
7528
- await new Promise((resolve7, reject) => {
10680
+ await new Promise((resolve8, reject) => {
7529
10681
  if (!output.writable) {
7530
10682
  reject(new Error("output stream is not writable"));
7531
10683
  return;
@@ -7541,7 +10693,7 @@ async function writeReply(output, reply) {
7541
10693
  return;
7542
10694
  settled = true;
7543
10695
  cleanup();
7544
- resolve7();
10696
+ resolve8();
7545
10697
  };
7546
10698
  const onError = (error) => {
7547
10699
  if (settled)
@@ -7571,13 +10723,38 @@ async function writeReply(output, reply) {
7571
10723
  if (ok) {
7572
10724
  settled = true;
7573
10725
  cleanup();
7574
- resolve7();
10726
+ resolve8();
7575
10727
  return;
7576
10728
  }
7577
10729
  output.once("drain", onDrain);
7578
10730
  });
7579
10731
  }
7580
10732
  function elicitationParams(input) {
10733
+ if (input.operation === "start_unattended_batch") {
10734
+ const b = input.batchDetails;
10735
+ const details2 = [
10736
+ `Operation: start_unattended_batch`,
10737
+ `Initiative: ${input.initiativeSlug ?? b?.initiative_slug}`,
10738
+ b?.batch_branch ? `Batch branch: ${b.batch_branch}` : null,
10739
+ input.planDigest ? `Plan digest: ${input.planDigest}` : null,
10740
+ b?.children ? `Ordered children (${b.children.length}):
10741
+ ${b.children.map((c) => ` - ${c.task_id} (${c.slice_id}) [risk: ${c.risk ?? "unknown"}]`).join(`
10742
+ `)}` : null,
10743
+ b?.excluded && b.excluded.length > 0 ? `Excluded children (${b.excluded.length}):
10744
+ ${b.excluded.map((e) => ` - ${e.task_id} (${e.slice_id}): ${e.reason}`).join(`
10745
+ `)}` : null,
10746
+ b?.budget ? `Budget: max_children=${b.budget.max_children}, deadline_at=${b.budget.deadline_at}, qa_failure_limit=${b.budget.qa_failure_limit}` : null,
10747
+ b?.expires_at ? `Expires at: ${b.expires_at}` : null
10748
+ ].filter(Boolean);
10749
+ return {
10750
+ mode: "form",
10751
+ message: `Authorize this unattended batch run for ${input.initiativeSlug}?
10752
+
10753
+ ${details2.join(`
10754
+ `)}`,
10755
+ requestedSchema: { type: "object", properties: {} }
10756
+ };
10757
+ }
7581
10758
  const details = [
7582
10759
  `Operation: ${input.operation}`,
7583
10760
  `Task: ${input.taskId}`,
@@ -7620,8 +10797,8 @@ async function serveStdio(options = {}) {
7620
10797
  throw new NativeAuthorityError("user_cancelled", "Tool call was cancelled");
7621
10798
  const requestId = `immune-brain:elicitation:${mcp.connectionId}:${++requestSequence}`;
7622
10799
  let abortListener;
7623
- const response = new Promise((resolve7, reject) => {
7624
- pending.set(requestId, { resolve: resolve7, reject });
10800
+ const response = new Promise((resolve8, reject) => {
10801
+ pending.set(requestId, { resolve: resolve8, reject });
7625
10802
  abortListener = () => {
7626
10803
  pending.delete(requestId);
7627
10804
  reject(new NativeAuthorityError("user_cancelled", "Tool call was cancelled"));
@@ -7642,13 +10819,13 @@ async function serveStdio(options = {}) {
7642
10819
  }
7643
10820
  throw new NativeAuthorityError("correlation_missing", `MCP elicitation failed: ${reply.error.message}`);
7644
10821
  }
7645
- const result = reply.result;
7646
- const action = typeof result === "object" && result !== null && !Array.isArray(result) ? result.action : undefined;
10822
+ const result2 = reply.result;
10823
+ const action = typeof result2 === "object" && result2 !== null && !Array.isArray(result2) ? result2.action : undefined;
7647
10824
  if (action !== "accept" && action !== "decline" && action !== "cancel") {
7648
10825
  throw new NativeAuthorityError("correlation_missing", "MCP elicitation returned an invalid action");
7649
10826
  }
7650
10827
  if (action === "accept") {
7651
- const content = result.content;
10828
+ const content = result2.content;
7652
10829
  if (typeof content !== "object" || content === null || Array.isArray(content) || Object.keys(content).length !== 0) {
7653
10830
  throw new NativeAuthorityError("correlation_missing", "MCP elicitation accept content did not match the requested schema");
7654
10831
  }
@@ -7766,7 +10943,7 @@ async function serveStdio(options = {}) {
7766
10943
  rejectPending(new NativeAuthorityError("user_cancelled", "MCP connection closed during native interaction"));
7767
10944
  await Promise.race([
7768
10945
  Promise.allSettled([...inFlight]),
7769
- new Promise((resolve7) => setTimeout(resolve7, 200))
10946
+ new Promise((resolve8) => setTimeout(resolve8, 200))
7770
10947
  ]);
7771
10948
  await mcp.shutdown();
7772
10949
  if (output.writable && typeof output.end === "function") {
@@ -7778,8 +10955,8 @@ async function serveStdio(options = {}) {
7778
10955
  })();
7779
10956
  return shutdownPromise;
7780
10957
  };
7781
- await new Promise((resolve7) => {
7782
- resolveStdio = resolve7;
10958
+ await new Promise((resolve8) => {
10959
+ resolveStdio = resolve8;
7783
10960
  output.on("error", () => {
7784
10961
  executeShutdown(1);
7785
10962
  });