immune-brain 3.6.6 → 3.6.7
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.
- package/README.md +24 -3
- package/README.zh-CN.md +23 -3
- package/package.json +2 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +29 -19
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +932 -0
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +200 -2
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +3404 -227
- package/plugins/immune-brain/dist/imm-loop.md +22 -0
- package/plugins/immune-brain/dist/role-prompts/code-review.md +9 -1
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +101 -3
- package/plugins/immune-brain/runtime/claude/interaction.ts +16 -3
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +840 -17
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +72 -10
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +9 -0
- package/plugins/immune-brain/runtime/kernel/completion.ts +19 -1
- package/plugins/immune-brain/runtime/kernel/reducer.ts +162 -11
- package/plugins/immune-brain/runtime/kernel/refutation.ts +82 -0
- package/plugins/immune-brain/runtime/kernel/types.ts +34 -1
- package/plugins/immune-brain/runtime/kernel/validation.ts +202 -9
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +9 -1
- package/plugins/immune-brain/runtime/unattended/batch_git.ts +775 -0
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +203 -0
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +1224 -0
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +360 -0
- package/plugins/immune-brain/runtime/unattended/types.ts +60 -0
- 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
|
|
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.
|
|
45
|
+
var PLUGIN_VERSION = "3.6.7";
|
|
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
|
|
682
|
-
import { existsSync as
|
|
683
|
-
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
684
|
-
import { join as
|
|
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
|
|
688
|
+
import { createHash as createHash6, randomUUID as randomUUID2 } from "node:crypto";
|
|
688
689
|
|
|
689
|
-
// plugins/immune-brain/runtime/
|
|
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:${
|
|
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:${
|
|
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
|
|
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:${
|
|
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:${
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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:${
|
|
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:${
|
|
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:${
|
|
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:${
|
|
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
|
|
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
|
|
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
|
|
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, [
|
|
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
|
|
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
|
|
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:${
|
|
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
|
|
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
|
|
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:${
|
|
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
|
|
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:${
|
|
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
|
|
5558
|
-
|
|
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
|
|
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
|
-
|
|
5572
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
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
|
|
6694
|
-
}
|
|
6695
|
-
function diffHashOf(root, record) {
|
|
6696
|
-
return diffSnapshotOf(root, record).diff_hash;
|
|
6989
|
+
return count;
|
|
6697
6990
|
}
|
|
6698
|
-
function
|
|
6699
|
-
|
|
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
|
|
6719
|
-
return
|
|
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
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
|
|
6733
|
-
|
|
6734
|
-
|
|
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
|
-
|
|
6753
|
-
|
|
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
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
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
|
|
6762
|
-
return
|
|
7015
|
+
function redactGithubDiagnostic(value) {
|
|
7016
|
+
return redactSecrets(value).replace(/\s+/g, " ").trim().slice(0, MAX_DIAGNOSTIC);
|
|
6763
7017
|
}
|
|
6764
|
-
|
|
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:
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
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
|
-
|
|
6800
|
-
const
|
|
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
|
|
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) =>
|
|
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" ?
|
|
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: (
|
|
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
|
|
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:
|
|
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:
|
|
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 ?
|
|
7090
|
-
const priorBytes = sidecar ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
7195
|
-
return
|
|
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
|
|
9764
|
+
const result2 = await settlement;
|
|
7215
9765
|
await input.hooks?.afterCommit?.();
|
|
7216
|
-
return
|
|
9766
|
+
return result2;
|
|
7217
9767
|
};
|
|
7218
9768
|
if (input.verdict.decision === "rework") {
|
|
7219
|
-
const findings = input.verdict
|
|
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
|
|
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,
|
|
9792
|
+
stagePlanningArtifactTransition(ctx.cwd, result2.record);
|
|
7251
9793
|
return;
|
|
7252
9794
|
}
|
|
7253
9795
|
const approval = {
|
|
7254
|
-
id: `approval-${input.snapshot.role}-${
|
|
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 =
|
|
7292
|
-
const priorBytes = operation.op === "revise_intent" ?
|
|
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
|
-
|
|
9837
|
+
writeFileSync5(sidecar, `${JSON.stringify(operation.next_intent, null, 2)}
|
|
7296
9838
|
`);
|
|
7297
|
-
const
|
|
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,
|
|
7307
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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((
|
|
7624
|
-
pending.set(requestId, { resolve:
|
|
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
|
|
7646
|
-
const action = typeof
|
|
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 =
|
|
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((
|
|
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((
|
|
7782
|
-
resolveStdio =
|
|
10958
|
+
await new Promise((resolve8) => {
|
|
10959
|
+
resolveStdio = resolve8;
|
|
7783
10960
|
output.on("error", () => {
|
|
7784
10961
|
executeShutdown(1);
|
|
7785
10962
|
});
|