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