immune-brain 4.4.1 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-enroll.ts +0 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +47 -2
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +23 -18
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +0 -3
- package/plugins/immune-brain/dist/BASELINE.md +18 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +1401 -1101
- 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 +52 -6
- package/plugins/immune-brain/dist/imm-planner.md +102 -13
- 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/assurance/coordinator.ts +55 -2
- package/plugins/immune-brain/runtime/assurance/review_evidence.ts +4 -0
- package/plugins/immune-brain/runtime/claude/interaction.ts +1 -2
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +73 -27
- package/plugins/immune-brain/runtime/claude/mcp_server.ts +3 -4
- package/plugins/immune-brain/runtime/claude/review_host.ts +15 -0
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +956 -634
- package/plugins/immune-brain/runtime/kernel/application.ts +2 -5
- package/plugins/immune-brain/runtime/kernel/authority_port.ts +5 -14
- package/plugins/immune-brain/runtime/kernel/batch_authority.ts +30 -49
- package/plugins/immune-brain/runtime/kernel/canary_application.ts +1 -2
- package/plugins/immune-brain/runtime/kernel/canary_eligibility.ts +0 -1
- package/plugins/immune-brain/runtime/kernel/capability_registry.ts +7 -9
- package/plugins/immune-brain/runtime/kernel/enrollment.ts +21 -12
- package/plugins/immune-brain/runtime/kernel/enrollment_authority.ts +5 -11
- package/plugins/immune-brain/runtime/kernel/storage.ts +8 -3
- package/plugins/immune-brain/runtime/kernel/types.ts +2 -1
- package/plugins/immune-brain/runtime/kernel/validation.ts +3 -1
- 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_git.ts +9 -1
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +3 -9
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +152 -46
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +193 -0
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +112 -122
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +57 -16
- package/plugins/immune-brain/runtime/unattended/types.ts +0 -2
- package/plugins/immune-brain/runtime/v4_runtime.ts +54 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +75 -12
- 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
- package/plugins/immune-brain/runtime/unattended/confirmation_deadline.ts +0 -33
|
@@ -39,7 +39,7 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// plugins/immune-brain/runtime/plugin_version.ts
|
|
42
|
-
var PLUGIN_VERSION = "4.
|
|
42
|
+
var PLUGIN_VERSION = "4.6.0";
|
|
43
43
|
|
|
44
44
|
// plugins/immune-brain/runtime/claude/interaction.ts
|
|
45
45
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -220,6 +220,7 @@ function parseAsyncAgentLaunch(result) {
|
|
|
220
220
|
return null;
|
|
221
221
|
return { agentId, outputFile };
|
|
222
222
|
}
|
|
223
|
+
var HANDBACK_TOOL = "SubagentHandback";
|
|
223
224
|
function readAgentTranscriptResult(transcript, agentId) {
|
|
224
225
|
let last = null;
|
|
225
226
|
for (const line of transcript.split(`
|
|
@@ -247,6 +248,13 @@ function readAgentTranscriptResult(transcript, agentId) {
|
|
|
247
248
|
const part = block;
|
|
248
249
|
if (part.type === "text" && typeof part.text === "string")
|
|
249
250
|
text += part.text;
|
|
251
|
+
if (part.type === "tool_use" && part.name === HANDBACK_TOOL && part.input && typeof part.input === "object") {
|
|
252
|
+
const report = part.input.message;
|
|
253
|
+
if (typeof report === "string" && report.trim()) {
|
|
254
|
+
last = report;
|
|
255
|
+
text = "";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
250
258
|
}
|
|
251
259
|
if (text.trim())
|
|
252
260
|
last = text;
|
|
@@ -1448,6 +1456,7 @@ var QA_MIN_JOB_TIMEOUT_SECONDS = 15 * 60;
|
|
|
1448
1456
|
var QA_MAX_JOB_TIMEOUT_SECONDS = 60 * 60;
|
|
1449
1457
|
var QA_JOB_OVERHEAD_SECONDS = 2 * 60;
|
|
1450
1458
|
var QA_JOB_TIMEOUT_SECONDS = QA_MIN_JOB_TIMEOUT_SECONDS;
|
|
1459
|
+
var QA_MAX_IDENTICAL_FAILURES = 2;
|
|
1451
1460
|
var REVIEW_TIMING_PROFILES = {
|
|
1452
1461
|
quick: { softDeadlineSeconds: 5 * 60, stopThresholdSeconds: 15 * 60 },
|
|
1453
1462
|
standard: { softDeadlineSeconds: 10 * 60, stopThresholdSeconds: 30 * 60 },
|
|
@@ -1682,6 +1691,8 @@ class AssuranceCoordinator {
|
|
|
1682
1691
|
operationControllers = new Map;
|
|
1683
1692
|
reviewReservations = new Map;
|
|
1684
1693
|
rejectedReviewOperations = new Map;
|
|
1694
|
+
qaFailureStreaks = new Map;
|
|
1695
|
+
qaAttemptKeys = new Map;
|
|
1685
1696
|
unknownOperations = new Map;
|
|
1686
1697
|
sessionInvocations = new Set;
|
|
1687
1698
|
sessionActive = true;
|
|
@@ -1710,6 +1721,8 @@ class AssuranceCoordinator {
|
|
|
1710
1721
|
this.removeEvidence(reservation);
|
|
1711
1722
|
}
|
|
1712
1723
|
this.reviewReservations.clear();
|
|
1724
|
+
this.qaFailureStreaks.clear();
|
|
1725
|
+
this.qaAttemptKeys.clear();
|
|
1713
1726
|
for (const invocation of [...this.sessionInvocations])
|
|
1714
1727
|
this.closeSessionInvocation(invocation);
|
|
1715
1728
|
}
|
|
@@ -1756,7 +1769,21 @@ class AssuranceCoordinator {
|
|
|
1756
1769
|
}
|
|
1757
1770
|
async advance(taskId, ctx, signal, onUpdate) {
|
|
1758
1771
|
const checks = new Map;
|
|
1772
|
+
const started = performance.now();
|
|
1773
|
+
const stageMs = {};
|
|
1774
|
+
let stage = "preparing";
|
|
1775
|
+
let stageStarted = started;
|
|
1776
|
+
const closeStage = (now) => {
|
|
1777
|
+
stageMs[stage] = (stageMs[stage] ?? 0) + Math.round(now - stageStarted);
|
|
1778
|
+
};
|
|
1759
1779
|
const result = await this.advanceOnce(taskId, ctx, signal, (update) => {
|
|
1780
|
+
const next = update.details.stage;
|
|
1781
|
+
if (typeof next === "string" && next !== stage) {
|
|
1782
|
+
const now = performance.now();
|
|
1783
|
+
closeStage(now);
|
|
1784
|
+
stage = next;
|
|
1785
|
+
stageStarted = now;
|
|
1786
|
+
}
|
|
1760
1787
|
const diagnostic = update.details.diagnostic;
|
|
1761
1788
|
if (diagnostic) {
|
|
1762
1789
|
checks.set(diagnostic.descriptor_ref, diagnostic);
|
|
@@ -1765,8 +1792,26 @@ class AssuranceCoordinator {
|
|
|
1765
1792
|
}
|
|
1766
1793
|
onUpdate?.(update);
|
|
1767
1794
|
});
|
|
1795
|
+
this.recordQaAttempt(taskId, result);
|
|
1768
1796
|
const diagnostics = result.diagnostics ?? [...checks.values()];
|
|
1769
|
-
|
|
1797
|
+
const enriched = diagnostics.length ? { ...result, diagnostics } : result;
|
|
1798
|
+
if (!(this.ports.reportTimings ?? process.env.IMM_ASSURANCE_TIMINGS === "1"))
|
|
1799
|
+
return this.withRecovery(taskId, ctx, enriched);
|
|
1800
|
+
const finished = performance.now();
|
|
1801
|
+
closeStage(finished);
|
|
1802
|
+
return this.withRecovery(taskId, ctx, { ...enriched, timings: { total_ms: Math.round(finished - started), stage_ms: stageMs } });
|
|
1803
|
+
}
|
|
1804
|
+
recordQaAttempt(taskId, result) {
|
|
1805
|
+
const key = this.qaAttemptKeys.get(taskId);
|
|
1806
|
+
this.qaAttemptKeys.delete(taskId);
|
|
1807
|
+
if (!key || result.state === "cancelled")
|
|
1808
|
+
return;
|
|
1809
|
+
if (result.state !== "failed" || result.operation !== "qa") {
|
|
1810
|
+
this.qaFailureStreaks.delete(taskId);
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
const streak = this.qaFailureStreaks.get(taskId);
|
|
1814
|
+
this.qaFailureStreaks.set(taskId, { key, count: streak?.key === key ? streak.count + 1 : 1 });
|
|
1770
1815
|
}
|
|
1771
1816
|
async withRecovery(taskId, ctx, enriched) {
|
|
1772
1817
|
if (enriched.recovery_error || !["failed", "blocked", "rework", "review_preparation_failed"].includes(enriched.state))
|
|
@@ -1933,6 +1978,12 @@ class AssuranceCoordinator {
|
|
|
1933
1978
|
ensureOperationLive();
|
|
1934
1979
|
const assurance = await this.ports.buildAssurance(ctx.cwd, taskId, "qa", projection);
|
|
1935
1980
|
ensureOperationLive();
|
|
1981
|
+
const qaKey = `${assurance.snapshot.record_revision}|${assurance.snapshot.intent_content_hash}|${assurance.snapshot.diff_hash}`;
|
|
1982
|
+
const streak = this.qaFailureStreaks.get(taskId);
|
|
1983
|
+
const maxFailures = this.ports.qaMaxIdenticalFailures ?? QA_MAX_IDENTICAL_FAILURES;
|
|
1984
|
+
if (streak?.key === qaKey && streak.count >= maxFailures)
|
|
1985
|
+
return { state: "blocked", reason: `deterministic QA already failed ${streak.count} times on this exact snapshot; change the workspace diff or intent, or restart the session after fixing the environment, before retrying` };
|
|
1986
|
+
this.qaAttemptKeys.set(taskId, qaKey);
|
|
1936
1987
|
const declaredQaJobMs = deriveQaJobTimeoutMs(assurance.descriptors.values());
|
|
1937
1988
|
const qaJobTimeoutMs = Math.min(declaredQaJobMs, this.ports.qaJobTimeoutMs ?? declaredQaJobMs);
|
|
1938
1989
|
const qaJobDeadline = setTimeout(() => {
|
|
@@ -2323,11 +2374,13 @@ import {
|
|
|
2323
2374
|
} from "node:fs";
|
|
2324
2375
|
import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
|
|
2325
2376
|
function git(root, args) {
|
|
2326
|
-
const
|
|
2377
|
+
const diff = args[0] === "diff";
|
|
2378
|
+
const command = diff ? ["diff", "--exit-code", ...args.slice(1)] : args;
|
|
2379
|
+
const result = spawnSync("git", ["--no-optional-locks", "-c", "diff.autoRefreshIndex=false", "-C", root, ...command], {
|
|
2327
2380
|
encoding: "utf8",
|
|
2328
2381
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2329
2382
|
});
|
|
2330
|
-
return result.status === 0 ? result.stdout : null;
|
|
2383
|
+
return result.status === 0 || diff && result.status === 1 ? result.stdout : null;
|
|
2331
2384
|
}
|
|
2332
2385
|
function splitNull(value) {
|
|
2333
2386
|
return value.split("\x00").filter((entry) => entry.length > 0);
|
|
@@ -2407,7 +2460,7 @@ function fileFingerprint(root, relativePath) {
|
|
|
2407
2460
|
return `file:${stat.mode}:${createHash7("sha256").update(readFileSync3(absolutePath)).digest("hex")}`;
|
|
2408
2461
|
}
|
|
2409
2462
|
function dirtyPaths(root) {
|
|
2410
|
-
const tracked = git(root, ["diff", "--name-only", "-z", "HEAD", "--"]);
|
|
2463
|
+
const tracked = git(root, ["diff", "--name-only", "--no-renames", "-z", "HEAD", "--"]);
|
|
2411
2464
|
const untracked = git(root, [
|
|
2412
2465
|
"ls-files",
|
|
2413
2466
|
"--others",
|
|
@@ -2446,12 +2499,14 @@ var portablePathCollator = new Intl.Collator("und", {
|
|
|
2446
2499
|
});
|
|
2447
2500
|
var gitTaskSnapshotTestHook;
|
|
2448
2501
|
function gitBytes(root, args) {
|
|
2449
|
-
const
|
|
2502
|
+
const diff = args[0] === "diff";
|
|
2503
|
+
const command = diff ? ["diff", "--exit-code", ...args.slice(1)] : args;
|
|
2504
|
+
const result = spawnSync("git", ["--no-optional-locks", "-c", "diff.autoRefreshIndex=false", "-C", root, ...command], {
|
|
2450
2505
|
encoding: null,
|
|
2451
2506
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2452
2507
|
maxBuffer: 8 * 1024 * 1024
|
|
2453
2508
|
});
|
|
2454
|
-
if (result.status !== 0 || !Buffer.isBuffer(result.stdout)) {
|
|
2509
|
+
if (result.status !== 0 && !(diff && result.status === 1) || !Buffer.isBuffer(result.stdout)) {
|
|
2455
2510
|
throw new Error(`Git task snapshot command failed: git ${args.join(" ")}`);
|
|
2456
2511
|
}
|
|
2457
2512
|
return result.stdout;
|
|
@@ -2580,6 +2635,25 @@ function headEntry(root, head, path) {
|
|
|
2580
2635
|
function taskPathMatchesScope(path, scope) {
|
|
2581
2636
|
return scope.some((scopePath) => pathMatchesScope(path, scopePath));
|
|
2582
2637
|
}
|
|
2638
|
+
function assertNoPreEnrollmentScopeChanges(root, scopedStagedPaths, taskId) {
|
|
2639
|
+
const baseline = readEnrollmentBaseline(root);
|
|
2640
|
+
if (!baseline)
|
|
2641
|
+
return;
|
|
2642
|
+
const current = captureGitWorkspaceSnapshot(root);
|
|
2643
|
+
if (!current)
|
|
2644
|
+
throw new Error("enrollment baseline cannot be compared because Git is unavailable");
|
|
2645
|
+
const withheld = [...new Set(scopedStagedPaths)].filter((path) => !isOwnPlanningSidecar(path, taskId)).filter((path) => baseline.dirty_files[path] !== undefined && baseline.dirty_files[path] === current.dirty_files[path]).sort(comparePaths);
|
|
2646
|
+
if (withheld.length > 0)
|
|
2647
|
+
throw new Error(`task scope contains staged changes that predate Enrollment and cannot become task work: ${withheld.join(", ")}; stop the task and re-enroll from a clean scope`);
|
|
2648
|
+
}
|
|
2649
|
+
function dirtyScopePaths(projectRoot, scopeHint, taskId) {
|
|
2650
|
+
const root = realpathSync3(resolve2(projectRoot));
|
|
2651
|
+
const scope = assertCanonicalTaskScope(scopeHint);
|
|
2652
|
+
const paths = dirtyPaths(root);
|
|
2653
|
+
if (!paths)
|
|
2654
|
+
throw new Error("cannot inspect the task scope outside a committed Git workspace");
|
|
2655
|
+
return paths.filter((path) => !isNonDeliveryPath(path) && !isOwnPlanningSidecar(path, taskId) && taskPathMatchesScope(path, scope));
|
|
2656
|
+
}
|
|
2583
2657
|
function taskSnapshotOnce(root, scope, taskId) {
|
|
2584
2658
|
const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
|
|
2585
2659
|
const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
|
|
@@ -2602,9 +2676,8 @@ function taskSnapshotOnce(root, scope, taskId) {
|
|
|
2602
2676
|
const uncommittedInScope = [...new Set([...unstagedPaths, ...untrackedPaths])].filter((path) => taskPathMatchesScope(path, scope)).sort(comparePaths);
|
|
2603
2677
|
if (uncommittedInScope.length > 0)
|
|
2604
2678
|
throw new Error(`task scope contains unstaged or untracked changes: ${uncommittedInScope.join(", ")}`);
|
|
2605
|
-
const
|
|
2606
|
-
|
|
2607
|
-
const taskPaths = [...new Set(stagedPaths)].filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope)).filter((path) => !(baseline && current && baseline.dirty_files[path] === current.dirty_files[path] && baseline.dirty_files[path] !== undefined)).sort(comparePaths);
|
|
2679
|
+
const taskPaths = [...new Set(stagedPaths)].filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope)).sort(comparePaths);
|
|
2680
|
+
assertNoPreEnrollmentScopeChanges(root, taskPaths, taskId);
|
|
2608
2681
|
const stagedFiles = {};
|
|
2609
2682
|
for (const path of taskPaths) {
|
|
2610
2683
|
const current = indexEntry(root, path);
|
|
@@ -2682,9 +2755,8 @@ function taskRevisionSnapshotOnce(root, scope, baseHead, taskId) {
|
|
|
2682
2755
|
const unstagedPaths = decodeNullPaths(gitBytes(root, ["diff", "--no-renames", "--name-only", "-z", "--"]), "unstaged task revision paths");
|
|
2683
2756
|
const untrackedPaths = decodeNullPaths(gitBytes(root, ["ls-files", "--others", "--exclude-standard", "-z", "--"]), "untracked task revision paths");
|
|
2684
2757
|
assertNoEnvelopeEscape(root, stagedPaths, scope, taskId);
|
|
2685
|
-
const
|
|
2686
|
-
|
|
2687
|
-
const scopedStagedPaths = stagedPaths.filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope)).filter((path) => !(baseline && current && baseline.dirty_files[path] === current.dirty_files[path] && baseline.dirty_files[path] !== undefined));
|
|
2758
|
+
const scopedStagedPaths = stagedPaths.filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope));
|
|
2759
|
+
assertNoPreEnrollmentScopeChanges(root, scopedStagedPaths, taskId);
|
|
2688
2760
|
const scopedUnstagedPaths = unstagedPaths.filter((path) => taskPathMatchesScope(path, scope));
|
|
2689
2761
|
const scopedUntrackedPaths = untrackedPaths.filter((path) => taskPathMatchesScope(path, scope));
|
|
2690
2762
|
assertNoCaseFoldCollisions([...scopedStagedPaths, ...scopedUnstagedPaths, ...scopedUntrackedPaths], "Git task revision paths");
|
|
@@ -2741,6 +2813,38 @@ function taskRevisionIdentity(projectRoot, scopeHint, baseHead, taskId) {
|
|
|
2741
2813
|
changed_paths: Object.keys(snapshot.changed_paths).sort(comparePaths)
|
|
2742
2814
|
};
|
|
2743
2815
|
}
|
|
2816
|
+
function taskCommitRevisionIdentity(projectRoot, scopeHint, baseHead, commit) {
|
|
2817
|
+
const requested = resolve2(projectRoot);
|
|
2818
|
+
if (lstatSync2(requested).isSymbolicLink())
|
|
2819
|
+
throw new Error("task commit root must be a real directory");
|
|
2820
|
+
const root = realpathSync3(requested), scope = assertCanonicalTaskScope(scopeHint);
|
|
2821
|
+
if (!GIT_OBJECT_ID.test(baseHead) || !GIT_OBJECT_ID.test(commit))
|
|
2822
|
+
throw new Error("invalid task commit identity");
|
|
2823
|
+
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)
|
|
2824
|
+
throw new Error("invalid task commit ancestry");
|
|
2825
|
+
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);
|
|
2826
|
+
assertNoCaseFoldCollisions(paths, "task commit paths");
|
|
2827
|
+
const changed_paths = {};
|
|
2828
|
+
for (const path of paths) {
|
|
2829
|
+
const current = headEntry(root, commit, path), base = headEntry(root, baseHead, path);
|
|
2830
|
+
changed_paths[path] = {
|
|
2831
|
+
status: !base ? "added" : !current ? "deleted" : "modified",
|
|
2832
|
+
mode: current?.mode ?? null,
|
|
2833
|
+
oid: current?.oid ?? null,
|
|
2834
|
+
base_mode: base?.mode ?? null,
|
|
2835
|
+
base_oid: base?.oid ?? null
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
const snapshot = {
|
|
2839
|
+
kind: "git-task-revision-v1",
|
|
2840
|
+
repository_root: root,
|
|
2841
|
+
base_head: baseHead,
|
|
2842
|
+
base_tree: gitRequired(root, ["rev-parse", `${baseHead}^{tree}`], "unreadable task base tree"),
|
|
2843
|
+
scope,
|
|
2844
|
+
changed_paths
|
|
2845
|
+
};
|
|
2846
|
+
return { diff_hash: hashTaskSnapshot(snapshot), changed_paths: paths };
|
|
2847
|
+
}
|
|
2744
2848
|
function isGitWorkspaceSnapshot(value) {
|
|
2745
2849
|
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
2850
|
}
|
|
@@ -4160,7 +4264,7 @@ function parseHistoryV3(value, index, violations) {
|
|
|
4160
4264
|
actor_id: stringAt(auth.actor_id, `${path}.authority.actor_id`, violations),
|
|
4161
4265
|
confirmation_ref: stringAt(auth.confirmation_ref, `${path}.authority.confirmation_ref`, violations),
|
|
4162
4266
|
issued_at: stringAt(auth.issued_at, `${path}.authority.issued_at`, violations),
|
|
4163
|
-
expires_at: stringAt(auth.expires_at, `${path}.authority.expires_at`, violations)
|
|
4267
|
+
...auth.expires_at !== undefined ? { expires_at: stringAt(auth.expires_at, `${path}.authority.expires_at`, violations) } : {}
|
|
4164
4268
|
};
|
|
4165
4269
|
}
|
|
4166
4270
|
return {
|
|
@@ -5728,6 +5832,9 @@ function ensureSecureDirectory(root, relativePath) {
|
|
|
5728
5832
|
return target.path;
|
|
5729
5833
|
}
|
|
5730
5834
|
function readSecureProjectFile(root, relativePath) {
|
|
5835
|
+
return readSecureProjectBytes(root, relativePath).toString("utf8");
|
|
5836
|
+
}
|
|
5837
|
+
function readSecureProjectBytes(root, relativePath) {
|
|
5731
5838
|
const candidate = safeCandidate(root, relativePath);
|
|
5732
5839
|
assertNoSymlinkSegments(candidate.root, candidate.path);
|
|
5733
5840
|
const before = pathStatOrNull(candidate.path);
|
|
@@ -5743,7 +5850,7 @@ function readSecureProjectFile(root, relativePath) {
|
|
|
5743
5850
|
const opened = fstatSync4(fd);
|
|
5744
5851
|
if (opened.dev !== before.dev || opened.ino !== before.ino)
|
|
5745
5852
|
throw new KernelStoreSecurityError(`source identity changed: ${relativePath}`);
|
|
5746
|
-
const content = readFileSync8(fd
|
|
5853
|
+
const content = readFileSync8(fd);
|
|
5747
5854
|
const after = lstatSync7(candidate.path);
|
|
5748
5855
|
if (after.dev !== opened.dev || after.ino !== opened.ino)
|
|
5749
5856
|
throw new KernelStoreSecurityError(`source identity changed: ${relativePath}`);
|
|
@@ -6679,6 +6786,8 @@ function publishInput(root, input) {
|
|
|
6679
6786
|
const recomputed = `sha256:${createHash13("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
|
|
6680
6787
|
if (recomputed !== input.expectedDiffHash)
|
|
6681
6788
|
throw new Error("review task revision does not match assurance snapshot");
|
|
6789
|
+
if (Object.keys(snapshot.changed_paths).length === 0)
|
|
6790
|
+
throw new Error("review revision carries no task change; stage the task's in-scope work before Review");
|
|
6682
6791
|
return { snapshot, revision: publishReviewRevision(snapshot.repository_root, snapshot, recomputed, input.taskId) };
|
|
6683
6792
|
}
|
|
6684
6793
|
function captureReviewManifest(root, input) {
|
|
@@ -6968,7 +7077,7 @@ async function projectAssurance(root, taskId, diffProvider) {
|
|
|
6968
7077
|
|
|
6969
7078
|
// plugins/immune-brain/runtime/kernel/application.ts
|
|
6970
7079
|
function applyTaskAction(input) {
|
|
6971
|
-
const { root, task_id, prior_intent_token, registry, capability, diffProvider
|
|
7080
|
+
const { root, task_id, prior_intent_token, registry, capability, diffProvider } = input;
|
|
6972
7081
|
return withKernelStoreLock(root, () => {
|
|
6973
7082
|
const current = readTaskRecordRaw(root, task_id);
|
|
6974
7083
|
if (!current.record)
|
|
@@ -7019,7 +7128,7 @@ function applyTaskAction(input) {
|
|
|
7019
7128
|
diff_hash: trustedDiff.diff_hash,
|
|
7020
7129
|
...action.type === "request_rework" ? { findings_digest: findingsDigestV2(action.findings) } : {}
|
|
7021
7130
|
} : null;
|
|
7022
|
-
const inspectedAudit = expectedAuthority ? registry.inspect(capability, expectedAuthority
|
|
7131
|
+
const inspectedAudit = expectedAuthority ? registry.inspect(capability, expectedAuthority) : null;
|
|
7023
7132
|
const authorityRunId = inspectedAudit?.run_id;
|
|
7024
7133
|
const mutation = reduceTask(current.record, action, inspectedAudit ? inspectedAudit.audit : null, trustedDiff.changed_paths);
|
|
7025
7134
|
if (!isReducedMutation(mutation))
|
|
@@ -7045,7 +7154,7 @@ function applyTaskAction(input) {
|
|
|
7045
7154
|
consumeIntentToken(prior_intent_token);
|
|
7046
7155
|
consumeIntentToken(freshRead.token);
|
|
7047
7156
|
if (expectedAuthority) {
|
|
7048
|
-
registry.consume(capability, expectedAuthority
|
|
7157
|
+
registry.consume(capability, expectedAuthority);
|
|
7049
7158
|
}
|
|
7050
7159
|
const nextWorking = mutation.next_workspace_working;
|
|
7051
7160
|
const nextRecord = input.artifact_transition ? {
|
|
@@ -7371,7 +7480,6 @@ function createCanaryApplication(registry) {
|
|
|
7371
7480
|
registry,
|
|
7372
7481
|
capability,
|
|
7373
7482
|
diffProvider: input.diffProvider,
|
|
7374
|
-
now: Date.parse(now),
|
|
7375
7483
|
...operation.op === "complete" || operation.op === "stop" ? { terminal: { terminalized_at: now } } : {},
|
|
7376
7484
|
...artifactTransition ? { artifact_transition: artifactTransition } : {}
|
|
7377
7485
|
});
|
|
@@ -7419,7 +7527,7 @@ function createCanaryApplication(registry) {
|
|
|
7419
7527
|
intent_revision: current.record.intent_snapshot.revision,
|
|
7420
7528
|
intent_content_hash: current.record.intent_ref.content_hash,
|
|
7421
7529
|
diff_hash: "sha256:" + "0".repeat(64)
|
|
7422
|
-
}
|
|
7530
|
+
});
|
|
7423
7531
|
const nextClaim = {
|
|
7424
7532
|
...claim,
|
|
7425
7533
|
lifecycle_status: "draining",
|
|
@@ -7460,7 +7568,7 @@ function createCapabilityRegistry(capabilityBrand, hooks, domainLabel) {
|
|
|
7460
7568
|
return {
|
|
7461
7569
|
brand,
|
|
7462
7570
|
issue(binding, issuedAt = new Date().toISOString()) {
|
|
7463
|
-
hooks.validateBinding(binding
|
|
7571
|
+
hooks.validateBinding(binding);
|
|
7464
7572
|
const capability = Object.freeze(Object.defineProperties({}, {
|
|
7465
7573
|
[capabilityBrand]: { value: true, enumerable: false, writable: false, configurable: false },
|
|
7466
7574
|
[brand]: { value: true, enumerable: false, writable: false, configurable: false }
|
|
@@ -7468,16 +7576,16 @@ function createCapabilityRegistry(capabilityBrand, hooks, domainLabel) {
|
|
|
7468
7576
|
states.set(capability, { binding: { ...binding }, issued_at: issuedAt, consumed: false });
|
|
7469
7577
|
return capability;
|
|
7470
7578
|
},
|
|
7471
|
-
inspect(capability, expected
|
|
7579
|
+
inspect(capability, expected) {
|
|
7472
7580
|
if (!isCapability(capability))
|
|
7473
7581
|
throw new Error(`${domainLabel} capability is not recognized by this registry`);
|
|
7474
7582
|
const state = stateOf(capability);
|
|
7475
7583
|
if (state.consumed)
|
|
7476
7584
|
throw new Error(`${domainLabel} capability already consumed`);
|
|
7477
|
-
return hooks.validateAndProject({ ...state.binding, issued_at: state.issued_at }, expected
|
|
7585
|
+
return hooks.validateAndProject({ ...state.binding, issued_at: state.issued_at }, expected);
|
|
7478
7586
|
},
|
|
7479
|
-
consume(capability, expected
|
|
7480
|
-
const validated = this.inspect(capability, expected
|
|
7587
|
+
consume(capability, expected) {
|
|
7588
|
+
const validated = this.inspect(capability, expected);
|
|
7481
7589
|
stateOf(capability).consumed = true;
|
|
7482
7590
|
return validated;
|
|
7483
7591
|
},
|
|
@@ -7494,7 +7602,7 @@ function digestOfAction(action) {
|
|
|
7494
7602
|
}
|
|
7495
7603
|
function createMutationAuthorityRegistry() {
|
|
7496
7604
|
const inner = createCapabilityRegistry(MUTATION_AUTHORITY_CAPABILITY_BRAND, {
|
|
7497
|
-
validateBinding(binding
|
|
7605
|
+
validateBinding(binding) {
|
|
7498
7606
|
const missing = [];
|
|
7499
7607
|
if (binding.run_id !== undefined && binding.run_id.length === 0)
|
|
7500
7608
|
throw new Error("authority capability run_id must not be empty");
|
|
@@ -7508,12 +7616,8 @@ function createMutationAuthorityRegistry() {
|
|
|
7508
7616
|
throw new Error(`authority capability binding is incomplete: ${missing.join(", ")}`);
|
|
7509
7617
|
if (binding.findings_digest !== null && !/^sha256:[a-f0-9]{64}$/.test(binding.findings_digest))
|
|
7510
7618
|
throw new Error("authority capability findings_digest must be a canonical sha256 hash");
|
|
7511
|
-
if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
|
|
7512
|
-
throw new Error("authority capability must have a future expiry");
|
|
7513
7619
|
},
|
|
7514
|
-
validateAndProject(state, expected
|
|
7515
|
-
if (Date.parse(state.expires_at) <= now)
|
|
7516
|
-
throw new Error("authority capability has expired");
|
|
7620
|
+
validateAndProject(state, expected) {
|
|
7517
7621
|
const actionDigest = digestOfAction(expected.action);
|
|
7518
7622
|
if (state.action_digest !== actionDigest)
|
|
7519
7623
|
throw new Error("authority capability action digest mismatch");
|
|
@@ -7540,8 +7644,7 @@ function createMutationAuthorityRegistry() {
|
|
|
7540
7644
|
authority_kind: state.authority_kind,
|
|
7541
7645
|
actor_id: canonicalActorId(state.actor_id),
|
|
7542
7646
|
confirmation_ref: state.confirmation_ref,
|
|
7543
|
-
issued_at: state.issued_at
|
|
7544
|
-
expires_at: state.expires_at
|
|
7647
|
+
issued_at: state.issued_at
|
|
7545
7648
|
},
|
|
7546
7649
|
action_digest: actionDigest,
|
|
7547
7650
|
...state.run_id !== undefined ? { run_id: state.run_id } : {},
|
|
@@ -7556,20 +7659,20 @@ function createMutationAuthorityRegistry() {
|
|
|
7556
7659
|
return {
|
|
7557
7660
|
brand: inner.brand,
|
|
7558
7661
|
issue: inner.issue.bind(inner),
|
|
7559
|
-
inspect(capability, expected
|
|
7662
|
+
inspect(capability, expected) {
|
|
7560
7663
|
if (!capability)
|
|
7561
7664
|
throw new Error("privileged action requires an opaque authority capability");
|
|
7562
7665
|
try {
|
|
7563
|
-
return inner.inspect(capability, expected
|
|
7666
|
+
return inner.inspect(capability, expected);
|
|
7564
7667
|
} catch (err) {
|
|
7565
7668
|
if (err instanceof Error && err.message.includes("not recognized by this registry"))
|
|
7566
7669
|
throw new Error("privileged action requires an opaque authority capability");
|
|
7567
7670
|
throw err;
|
|
7568
7671
|
}
|
|
7569
7672
|
},
|
|
7570
|
-
consume(capability, expected
|
|
7673
|
+
consume(capability, expected) {
|
|
7571
7674
|
try {
|
|
7572
|
-
return inner.consume(capability, expected
|
|
7675
|
+
return inner.consume(capability, expected);
|
|
7573
7676
|
} catch (err) {
|
|
7574
7677
|
if (err instanceof Error && err.message.includes("not recognized by this registry"))
|
|
7575
7678
|
throw new Error("privileged action requires an opaque authority capability");
|
|
@@ -7584,7 +7687,7 @@ function createMutationAuthorityRegistry() {
|
|
|
7584
7687
|
var ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollment-capability-brand");
|
|
7585
7688
|
function createEnrollmentAuthorityRegistry() {
|
|
7586
7689
|
return createCapabilityRegistry(ENROLLMENT_CAPABILITY_BRAND, {
|
|
7587
|
-
validateBinding(binding
|
|
7690
|
+
validateBinding(binding) {
|
|
7588
7691
|
const missing = [];
|
|
7589
7692
|
for (const [key, value] of Object.entries(binding)) {
|
|
7590
7693
|
if (value === undefined || value === null || value === "")
|
|
@@ -7592,12 +7695,8 @@ function createEnrollmentAuthorityRegistry() {
|
|
|
7592
7695
|
}
|
|
7593
7696
|
if (missing.length > 0)
|
|
7594
7697
|
throw new Error(`enrollment capability binding is incomplete: ${missing.join(", ")}`);
|
|
7595
|
-
if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
|
|
7596
|
-
throw new Error("enrollment capability must have a future expiry");
|
|
7597
7698
|
},
|
|
7598
|
-
validateAndProject(state, expected
|
|
7599
|
-
if (Date.parse(state.expires_at) <= now)
|
|
7600
|
-
throw new Error("enrollment capability has expired");
|
|
7699
|
+
validateAndProject(state, expected) {
|
|
7601
7700
|
for (const key of Object.keys(expected)) {
|
|
7602
7701
|
if (state[key] !== expected[key])
|
|
7603
7702
|
throw new Error(`enrollment capability ${key} mismatch`);
|
|
@@ -7611,13 +7710,16 @@ function createEnrollmentAuthorityRegistry() {
|
|
|
7611
7710
|
actor_id: state.actor_id,
|
|
7612
7711
|
confirmation_ref: state.confirmation_ref,
|
|
7613
7712
|
issued_at: state.issued_at,
|
|
7614
|
-
expires_at: state.expires_at,
|
|
7615
7713
|
nonce: state.nonce
|
|
7616
7714
|
};
|
|
7617
7715
|
}
|
|
7618
7716
|
}, "enrollment");
|
|
7619
7717
|
}
|
|
7620
7718
|
|
|
7719
|
+
// plugins/immune-brain/runtime/kernel/batch_authority.ts
|
|
7720
|
+
import { createHash as createHash16 } from "node:crypto";
|
|
7721
|
+
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
7722
|
+
|
|
7621
7723
|
// plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts
|
|
7622
7724
|
import { createHash as createHash15 } from "node:crypto";
|
|
7623
7725
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
@@ -7719,6 +7821,238 @@ function revalidatePiCanary(root, input, previous) {
|
|
|
7719
7821
|
return { unchanged: current.digest === previous.digest, current };
|
|
7720
7822
|
}
|
|
7721
7823
|
|
|
7824
|
+
// plugins/immune-brain/runtime/kernel/batch_authority.ts
|
|
7825
|
+
var BATCH_AUTHORITY_CAPABILITY_BRAND = Symbol.for("assurance-kernel.batch-authority-capability-brand");
|
|
7826
|
+
var GIT_COMMIT_ID2 = /^[a-f0-9]{40}$/;
|
|
7827
|
+
function sha256Hex3(bytes) {
|
|
7828
|
+
return createHash16("sha256").update(bytes).digest("hex");
|
|
7829
|
+
}
|
|
7830
|
+
function stableStringify3(value) {
|
|
7831
|
+
if (value === null || typeof value !== "object")
|
|
7832
|
+
return JSON.stringify(value);
|
|
7833
|
+
if (Array.isArray(value))
|
|
7834
|
+
return `[${value.map((entry) => stableStringify3(entry)).join(",")}]`;
|
|
7835
|
+
const record = value;
|
|
7836
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify3(record[key])}`).join(",")}}`;
|
|
7837
|
+
}
|
|
7838
|
+
function computeBatchPlanDigest(children) {
|
|
7839
|
+
const canonical = children.map((child) => ({
|
|
7840
|
+
blocked_by: [...child.blocked_by],
|
|
7841
|
+
intent_content_hash: child.intent_content_hash,
|
|
7842
|
+
intent_path: child.intent_path,
|
|
7843
|
+
intent_revision: child.intent_revision,
|
|
7844
|
+
task_id: child.task_id
|
|
7845
|
+
}));
|
|
7846
|
+
return `sha256:${sha256Hex3(stableStringify3(canonical))}`;
|
|
7847
|
+
}
|
|
7848
|
+
function requireNonEmpty(binding) {
|
|
7849
|
+
const missing = [];
|
|
7850
|
+
for (const key of [
|
|
7851
|
+
"batch_id",
|
|
7852
|
+
"initiative_slug",
|
|
7853
|
+
"plan_digest",
|
|
7854
|
+
"branch",
|
|
7855
|
+
"base_head",
|
|
7856
|
+
"actor_id",
|
|
7857
|
+
"confirmation_ref",
|
|
7858
|
+
"nonce"
|
|
7859
|
+
]) {
|
|
7860
|
+
const value = binding[key];
|
|
7861
|
+
if (value === undefined || value === null || value === "")
|
|
7862
|
+
missing.push(key);
|
|
7863
|
+
}
|
|
7864
|
+
if (!binding.budget || typeof binding.budget !== "object")
|
|
7865
|
+
missing.push("budget");
|
|
7866
|
+
if (missing.length > 0)
|
|
7867
|
+
throw new Error(`batch authorization binding is incomplete: ${missing.join(", ")}`);
|
|
7868
|
+
}
|
|
7869
|
+
function validateBudget(budget) {
|
|
7870
|
+
if (!Number.isInteger(budget.max_children) || budget.max_children <= 0)
|
|
7871
|
+
throw new Error("batch budget max_children must be a positive integer");
|
|
7872
|
+
if (!Number.isInteger(budget.qa_failure_limit) || budget.qa_failure_limit <= 0)
|
|
7873
|
+
throw new Error("batch budget qa_failure_limit must be a positive integer");
|
|
7874
|
+
}
|
|
7875
|
+
function validateChildren(children, planDigest) {
|
|
7876
|
+
if (!Array.isArray(children) || children.length === 0)
|
|
7877
|
+
throw new Error("batch authorization requires a non-empty child plan");
|
|
7878
|
+
const seen = new Set;
|
|
7879
|
+
for (const child of children) {
|
|
7880
|
+
if (!child || typeof child !== "object")
|
|
7881
|
+
throw new Error("batch plan child must be an object");
|
|
7882
|
+
for (const key of ["task_id", "intent_path", "intent_content_hash"]) {
|
|
7883
|
+
if (typeof child[key] !== "string" || child[key] === "")
|
|
7884
|
+
throw new Error(`batch plan child ${key} must be a non-empty string`);
|
|
7885
|
+
}
|
|
7886
|
+
if (!Number.isInteger(child.intent_revision) || child.intent_revision <= 0)
|
|
7887
|
+
throw new Error("batch plan child intent_revision must be a positive integer");
|
|
7888
|
+
if (!Array.isArray(child.blocked_by) || child.blocked_by.some((id) => typeof id !== "string" || id === ""))
|
|
7889
|
+
throw new Error("batch plan child blocked_by must be an array of task ids");
|
|
7890
|
+
if (seen.has(child.task_id))
|
|
7891
|
+
throw new Error(`batch plan child ${child.task_id} appears more than once`);
|
|
7892
|
+
seen.add(child.task_id);
|
|
7893
|
+
}
|
|
7894
|
+
for (const child of children) {
|
|
7895
|
+
for (const blocker of child.blocked_by) {
|
|
7896
|
+
if (!seen.has(blocker))
|
|
7897
|
+
throw new Error(`batch plan child ${child.task_id} is blocked by ${blocker}, which is not in the confirmed plan`);
|
|
7898
|
+
}
|
|
7899
|
+
}
|
|
7900
|
+
const dependencies = new Map(children.map((child) => [child.task_id, child.blocked_by]));
|
|
7901
|
+
const visiting = new Set;
|
|
7902
|
+
const visited = new Set;
|
|
7903
|
+
function visit(taskId) {
|
|
7904
|
+
if (visiting.has(taskId))
|
|
7905
|
+
throw new Error(`batch plan dependency cycle at ${taskId}`);
|
|
7906
|
+
if (visited.has(taskId))
|
|
7907
|
+
return;
|
|
7908
|
+
visiting.add(taskId);
|
|
7909
|
+
for (const blocker of dependencies.get(taskId))
|
|
7910
|
+
visit(blocker);
|
|
7911
|
+
visiting.delete(taskId);
|
|
7912
|
+
visited.add(taskId);
|
|
7913
|
+
}
|
|
7914
|
+
for (const child of children)
|
|
7915
|
+
visit(child.task_id);
|
|
7916
|
+
if (computeBatchPlanDigest(children) !== planDigest)
|
|
7917
|
+
throw new Error("batch plan digest does not match the confirmed child plan");
|
|
7918
|
+
}
|
|
7919
|
+
function createBatchAuthorityRegistry() {
|
|
7920
|
+
const inner = createCapabilityRegistry(BATCH_AUTHORITY_CAPABILITY_BRAND, {
|
|
7921
|
+
validateBinding(binding) {
|
|
7922
|
+
requireNonEmpty(binding);
|
|
7923
|
+
if (!isLiteralUserActor(binding.actor_id))
|
|
7924
|
+
throw new Error("batch authorization requires a literal-user actor_id");
|
|
7925
|
+
if (!GIT_COMMIT_ID2.test(binding.base_head))
|
|
7926
|
+
throw new Error("batch authorization base_head must be a committed 40-hex commit id");
|
|
7927
|
+
validateBudget(binding.budget);
|
|
7928
|
+
},
|
|
7929
|
+
validateAndProject(state, expected) {
|
|
7930
|
+
for (const key of Object.keys(expected)) {
|
|
7931
|
+
if (key === "budget") {
|
|
7932
|
+
const a = state.budget ?? {};
|
|
7933
|
+
const b = expected.budget ?? {};
|
|
7934
|
+
if (a.max_children !== b.max_children || a.qa_failure_limit !== b.qa_failure_limit)
|
|
7935
|
+
throw new Error("batch authorization budget mismatch");
|
|
7936
|
+
continue;
|
|
7937
|
+
}
|
|
7938
|
+
if (state[key] !== expected[key])
|
|
7939
|
+
throw new Error(`batch authorization ${key} mismatch`);
|
|
7940
|
+
}
|
|
7941
|
+
return {
|
|
7942
|
+
batch_id: state.batch_id,
|
|
7943
|
+
initiative_slug: state.initiative_slug,
|
|
7944
|
+
plan_digest: state.plan_digest,
|
|
7945
|
+
branch: state.branch,
|
|
7946
|
+
base_head: state.base_head,
|
|
7947
|
+
budget: { ...state.budget },
|
|
7948
|
+
actor_id: state.actor_id,
|
|
7949
|
+
confirmation_ref: state.confirmation_ref,
|
|
7950
|
+
issued_at: state.issued_at,
|
|
7951
|
+
nonce: state.nonce
|
|
7952
|
+
};
|
|
7953
|
+
}
|
|
7954
|
+
}, "batch authorization");
|
|
7955
|
+
const plans = new WeakMap;
|
|
7956
|
+
const consumed = new WeakMap;
|
|
7957
|
+
function planOf(capability) {
|
|
7958
|
+
const plan = plans.get(capability);
|
|
7959
|
+
if (!plan)
|
|
7960
|
+
throw new Error("batch authorization capability is not recognized by this registry");
|
|
7961
|
+
return plan;
|
|
7962
|
+
}
|
|
7963
|
+
function slotsOf(capability) {
|
|
7964
|
+
const slots = consumed.get(capability);
|
|
7965
|
+
if (!slots)
|
|
7966
|
+
throw new Error("batch authorization capability is not recognized by this registry");
|
|
7967
|
+
return slots;
|
|
7968
|
+
}
|
|
7969
|
+
return {
|
|
7970
|
+
brand: inner.brand,
|
|
7971
|
+
issue(binding, children, issuedAt = new Date().toISOString()) {
|
|
7972
|
+
requireNonEmpty(binding);
|
|
7973
|
+
validateChildren(children, binding.plan_digest);
|
|
7974
|
+
const capability = inner.issue(binding, issuedAt);
|
|
7975
|
+
plans.set(capability, children.map((child) => ({ ...child, blocked_by: [...child.blocked_by] })));
|
|
7976
|
+
consumed.set(capability, new Set);
|
|
7977
|
+
return capability;
|
|
7978
|
+
},
|
|
7979
|
+
inspect(capability, expected) {
|
|
7980
|
+
planOf(capability);
|
|
7981
|
+
return inner.inspect(capability, expected);
|
|
7982
|
+
},
|
|
7983
|
+
children(capability) {
|
|
7984
|
+
return planOf(capability).map((child) => ({ ...child, blocked_by: [...child.blocked_by] }));
|
|
7985
|
+
},
|
|
7986
|
+
consumedChildren(capability) {
|
|
7987
|
+
return [...slotsOf(capability)];
|
|
7988
|
+
},
|
|
7989
|
+
isChildConsumed(capability, taskId) {
|
|
7990
|
+
return slotsOf(capability).has(taskId);
|
|
7991
|
+
},
|
|
7992
|
+
consumeChild(capability, expected, taskId) {
|
|
7993
|
+
const validated = this.inspect(capability, expected);
|
|
7994
|
+
const plan = planOf(capability);
|
|
7995
|
+
if (!plan.some((child) => child.task_id === taskId))
|
|
7996
|
+
throw new Error(`batch_child_not_in_plan: ${taskId}`);
|
|
7997
|
+
const slots = slotsOf(capability);
|
|
7998
|
+
if (slots.has(taskId))
|
|
7999
|
+
throw new Error(`batch_child_slot_consumed: ${taskId}`);
|
|
8000
|
+
slots.add(taskId);
|
|
8001
|
+
return validated;
|
|
8002
|
+
},
|
|
8003
|
+
releaseChild(capability, taskId) {
|
|
8004
|
+
slotsOf(capability).delete(taskId);
|
|
8005
|
+
},
|
|
8006
|
+
isExhausted(capability) {
|
|
8007
|
+
return slotsOf(capability).size >= planOf(capability).length;
|
|
8008
|
+
}
|
|
8009
|
+
};
|
|
8010
|
+
}
|
|
8011
|
+
function assertBatchLineageOrigin(root, baseHead, expectedHead) {
|
|
8012
|
+
if (expectedHead === baseHead)
|
|
8013
|
+
return;
|
|
8014
|
+
const descends = spawnSync4("git", ["-C", root, "merge-base", "--is-ancestor", baseHead, expectedHead], {
|
|
8015
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
8016
|
+
}).status === 0;
|
|
8017
|
+
if (!descends)
|
|
8018
|
+
throw new Error(`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${baseHead} or a fast-forward of it, not ${expectedHead}`);
|
|
8019
|
+
}
|
|
8020
|
+
function deriveChildEnrollment(root, registry, input) {
|
|
8021
|
+
const validated = registry.inspect(input.capability, input.binding);
|
|
8022
|
+
const child = registry.children(input.capability).find((entry) => entry.task_id === input.task_id);
|
|
8023
|
+
if (!child)
|
|
8024
|
+
throw new Error(`batch_child_not_in_plan: ${input.task_id}`);
|
|
8025
|
+
if (registry.isChildConsumed(input.capability, input.task_id))
|
|
8026
|
+
throw new Error(`batch_child_slot_consumed: ${input.task_id}`);
|
|
8027
|
+
if (!GIT_COMMIT_ID2.test(input.expected_head))
|
|
8028
|
+
throw new Error("batch_head_lineage_broken: expected_head is not a commit id");
|
|
8029
|
+
const preparation = preparePiCanary(root, { task_id: input.task_id, now: input.now });
|
|
8030
|
+
if (!preparation.git_base_head)
|
|
8031
|
+
throw new Error(preparation.git_error ?? "enrollment requires a committed Git HEAD");
|
|
8032
|
+
if (preparation.git_base_head !== input.expected_head)
|
|
8033
|
+
throw new Error(`batch_head_lineage_broken: expected ${input.expected_head}, found ${preparation.git_base_head}`);
|
|
8034
|
+
if (!preparation.intent)
|
|
8035
|
+
throw new Error(`batch_child_intent_changed: ${input.task_id} intent sidecar is unreadable`);
|
|
8036
|
+
if (preparation.intent.path !== child.intent_path || preparation.intent.revision !== child.intent_revision || preparation.intent.content_hash !== child.intent_content_hash)
|
|
8037
|
+
throw new Error(`batch_child_intent_changed: ${input.task_id}`);
|
|
8038
|
+
if (registry.consumedChildren(input.capability).length === 0)
|
|
8039
|
+
assertBatchLineageOrigin(root, validated.base_head, input.expected_head);
|
|
8040
|
+
return {
|
|
8041
|
+
child,
|
|
8042
|
+
preparation,
|
|
8043
|
+
binding: {
|
|
8044
|
+
task_id: child.task_id,
|
|
8045
|
+
intent_path: child.intent_path,
|
|
8046
|
+
intent_revision: child.intent_revision,
|
|
8047
|
+
intent_content_hash: child.intent_content_hash,
|
|
8048
|
+
preparation_digest: preparation.digest,
|
|
8049
|
+
actor_id: validated.actor_id,
|
|
8050
|
+
confirmation_ref: validated.confirmation_ref,
|
|
8051
|
+
nonce: `${validated.nonce}:${child.task_id}`
|
|
8052
|
+
}
|
|
8053
|
+
};
|
|
8054
|
+
}
|
|
8055
|
+
|
|
7722
8056
|
// plugins/immune-brain/runtime/kernel/enrollment.ts
|
|
7723
8057
|
function runEnrollmentPreconditionChecks(root, input, capability, registry, mode, beforeLock, onReady) {
|
|
7724
8058
|
const blockers = [];
|
|
@@ -7770,10 +8104,21 @@ function runEnrollmentPreconditionChecks(root, input, capability, registry, mode
|
|
|
7770
8104
|
} catch (error) {
|
|
7771
8105
|
fail(`git base: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
7772
8106
|
}
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
8107
|
+
if (intent && gitBaseHead) {
|
|
8108
|
+
try {
|
|
8109
|
+
const dirty = dirtyScopePaths(root, intent.intent.scope_hint, input.task_id);
|
|
8110
|
+
if (dirty.length > 0) {
|
|
8111
|
+
const message = `task scope is already dirty before Enrollment: ${dirty.join(", ")}; commit, stash or revert these paths first`;
|
|
8112
|
+
fail(message, new Error(message));
|
|
8113
|
+
}
|
|
8114
|
+
} catch (error) {
|
|
8115
|
+
fail(`scope: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
8116
|
+
}
|
|
8117
|
+
}
|
|
8118
|
+
const state = {
|
|
8119
|
+
validated,
|
|
8120
|
+
current,
|
|
8121
|
+
workspace,
|
|
7777
8122
|
intent,
|
|
7778
8123
|
gitBaseHead
|
|
7779
8124
|
};
|
|
@@ -7866,16 +8211,16 @@ function enrollCanaryTask(root, input, registry) {
|
|
|
7866
8211
|
if (checks.gitBaseHead !== gitBaseHead)
|
|
7867
8212
|
throw new Error("Git HEAD moved after the enrollment confirmation");
|
|
7868
8213
|
if (input.batch) {
|
|
7869
|
-
const batch = input.batch.registry.inspect(input.batch.capability, input.batch.binding
|
|
7870
|
-
if (input.batch.registry.consumedChildren(input.batch.capability).length === 0
|
|
7871
|
-
|
|
8214
|
+
const batch = input.batch.registry.inspect(input.batch.capability, input.batch.binding);
|
|
8215
|
+
if (input.batch.registry.consumedChildren(input.batch.capability).length === 0)
|
|
8216
|
+
assertBatchLineageOrigin(root, batch.base_head, input.batch.expected_head);
|
|
7872
8217
|
if (checks.gitBaseHead !== input.batch.expected_head)
|
|
7873
8218
|
throw new Error(`batch_head_lineage_broken: expected ${input.batch.expected_head}, found ${checks.gitBaseHead}`);
|
|
7874
8219
|
}
|
|
7875
8220
|
registry.consume(input.capability, input.capability_binding);
|
|
7876
8221
|
consumed = true;
|
|
7877
8222
|
if (input.batch)
|
|
7878
|
-
input.batch.registry.consumeChild(input.batch.capability, input.batch.binding, input.task_id
|
|
8223
|
+
input.batch.registry.consumeChild(input.batch.capability, input.batch.binding, input.task_id);
|
|
7879
8224
|
if (!gitBaseHead)
|
|
7880
8225
|
throw new Error("enrollment requires a committed Git HEAD");
|
|
7881
8226
|
const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
|
|
@@ -7929,7 +8274,7 @@ function enrollCanaryTask(root, input, registry) {
|
|
|
7929
8274
|
}
|
|
7930
8275
|
|
|
7931
8276
|
// plugins/immune-brain/runtime/assurance/enrollment_git_base.ts
|
|
7932
|
-
import { execFileSync as execFileSync4, spawnSync as
|
|
8277
|
+
import { execFileSync as execFileSync4, spawnSync as spawnSync5 } from "node:child_process";
|
|
7933
8278
|
function git2(root, args, input, env = process.env) {
|
|
7934
8279
|
return execFileSync4("git", args, { cwd: root, encoding: "utf8", input, env, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
7935
8280
|
}
|
|
@@ -7942,8 +8287,8 @@ function inspectEnrollmentGitBase(root) {
|
|
|
7942
8287
|
const branch = git2(root, ["symbolic-ref", "--no-recurse", "HEAD"]);
|
|
7943
8288
|
if (!branch.startsWith("refs/heads/"))
|
|
7944
8289
|
throw new Error("Enrollment HEAD must name a local branch");
|
|
7945
|
-
const ref =
|
|
7946
|
-
const symbolic =
|
|
8290
|
+
const ref = spawnSync5("git", ["show-ref", "--verify", "--quiet", branch], { cwd: root });
|
|
8291
|
+
const symbolic = spawnSync5("git", ["symbolic-ref", "-q", branch], { cwd: root });
|
|
7947
8292
|
if (ref.status !== 1 || symbolic.status !== 1)
|
|
7948
8293
|
throw new Error("Enrollment Git HEAD is invalid; only an absent branch can be initialized");
|
|
7949
8294
|
return { state: "unborn", branch };
|
|
@@ -7953,7 +8298,7 @@ function enrollmentGitBaseNotice(base) {
|
|
|
7953
8298
|
}
|
|
7954
8299
|
function identityEnv(root) {
|
|
7955
8300
|
const configured = (key) => {
|
|
7956
|
-
const result =
|
|
8301
|
+
const result = spawnSync5("git", ["config", "--get", key], { cwd: root, encoding: "utf8" });
|
|
7957
8302
|
if (result.status !== 0 && result.status !== 1)
|
|
7958
8303
|
throw new Error(`Cannot read Git identity: ${key}`);
|
|
7959
8304
|
return result.stdout?.trim() ?? "";
|
|
@@ -8002,10 +8347,10 @@ function initializeEnrollmentGitBase(root, input, previous, base, signal) {
|
|
|
8002
8347
|
}
|
|
8003
8348
|
|
|
8004
8349
|
// plugins/immune-brain/runtime/assurance/qa.ts
|
|
8005
|
-
import { createHash as
|
|
8350
|
+
import { createHash as createHash18 } from "node:crypto";
|
|
8006
8351
|
// plugins/immune-brain/runtime/assurance/delivery_workspace.ts
|
|
8007
8352
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
8008
|
-
import { createHash as
|
|
8353
|
+
import { createHash as createHash17 } from "node:crypto";
|
|
8009
8354
|
import { chmodSync as chmodSync2, lstatSync as lstatSync8, mkdirSync as mkdirSync5, mkdtempSync as mkdtempSync2, readdirSync as readdirSync5, readFileSync as readFileSync10, readlinkSync as readlinkSync2, realpathSync as realpathSync9, rmSync as rmSync5 } from "node:fs";
|
|
8010
8355
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
8011
8356
|
import { dirname as dirname6, isAbsolute as isAbsolute5, join as join8, parse, relative as relative4, resolve as resolve9, sep as sep5 } from "node:path";
|
|
@@ -8155,7 +8500,7 @@ function workspaceFiles(root, dir = root, result = Object.create(null)) {
|
|
|
8155
8500
|
result[rel] = `${stat.mode & 4095}:directory`;
|
|
8156
8501
|
workspaceFiles(root, path, result);
|
|
8157
8502
|
} else if (stat.isFile())
|
|
8158
|
-
result[rel] = `${stat.mode & 4095}:${
|
|
8503
|
+
result[rel] = `${stat.mode & 4095}:${createHash17("sha256").update(readFileSync10(path)).digest("hex")}`;
|
|
8159
8504
|
else
|
|
8160
8505
|
throw new DeliveryWorkspaceError("delivery contains an unsupported filesystem entry");
|
|
8161
8506
|
}
|
|
@@ -8257,7 +8602,7 @@ function deliveryTreeForSnapshot(snapshot, writeTree) {
|
|
|
8257
8602
|
if (!record || record.contract !== "assurance_kernel/task_record/v4" || !record.git_base_head)
|
|
8258
8603
|
throw new Error("QA delivery requires a TaskRecord v4 git_base_head");
|
|
8259
8604
|
const captured = captureGitTaskRevisionSnapshot(snapshot.root, record.intent_snapshot.scope_hint, record.git_base_head, snapshot.task_id);
|
|
8260
|
-
const digest = `sha256:${
|
|
8605
|
+
const digest = `sha256:${createHash18("sha256").update(JSON.stringify(captured)).digest("hex")}`;
|
|
8261
8606
|
if (digest !== snapshot.diff_hash)
|
|
8262
8607
|
throw new Error("QA delivery identity does not match the frozen snapshot");
|
|
8263
8608
|
return writeDeliveryTree(snapshot.root, captured);
|
|
@@ -8292,7 +8637,7 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
|
8292
8637
|
const diagnostics = (stage, outcome, affected = group, commandStage = "check", elapsed = 0, result) => affected.map((item) => ({
|
|
8293
8638
|
acceptance_id: item.id,
|
|
8294
8639
|
descriptor_ref: `acceptance/${item.index}/verification/${commandStage === "prepare" ? "environment/prepare" : "command"}`,
|
|
8295
|
-
descriptor_digest: `sha256:${
|
|
8640
|
+
descriptor_digest: `sha256:${createHash18("sha256").update(JSON.stringify(item.descriptor)).digest("hex")}`,
|
|
8296
8641
|
stage,
|
|
8297
8642
|
outcome,
|
|
8298
8643
|
elapsed_ms: elapsed,
|
|
@@ -8366,8 +8711,8 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
|
8366
8711
|
}
|
|
8367
8712
|
evidence.push({
|
|
8368
8713
|
stage,
|
|
8369
|
-
environment:
|
|
8370
|
-
command:
|
|
8714
|
+
environment: createHash18("sha256").update(environmentKey).digest("hex"),
|
|
8715
|
+
command: createHash18("sha256").update(JSON.stringify(command)).digest("hex"),
|
|
8371
8716
|
entry: frozen?.entry?.content_hash ?? null,
|
|
8372
8717
|
interpreter: frozen?.interpreter?.content_hash ?? null,
|
|
8373
8718
|
exit_code: result.exit_code,
|
|
@@ -8445,7 +8790,7 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
|
8445
8790
|
};
|
|
8446
8791
|
if (findings.length)
|
|
8447
8792
|
return { ...base, decision: "rework", findings };
|
|
8448
|
-
const executionDigest =
|
|
8793
|
+
const executionDigest = createHash18("sha256").update(JSON.stringify({
|
|
8449
8794
|
tree,
|
|
8450
8795
|
platform: process.platform,
|
|
8451
8796
|
arch: process.arch,
|
|
@@ -8530,11 +8875,6 @@ var BATCH_REASONS = Object.freeze({
|
|
|
8530
8875
|
reason: "native confirmation port is unavailable",
|
|
8531
8876
|
recovery_action: "retry through a fresh native gate in the current Host"
|
|
8532
8877
|
},
|
|
8533
|
-
confirmation_timed_out: {
|
|
8534
|
-
state: "rejected",
|
|
8535
|
-
reason: "native confirmation timed out waiting for user interaction",
|
|
8536
|
-
recovery_action: "retry through a fresh native gate in the current Host"
|
|
8537
|
-
},
|
|
8538
8878
|
confirmation_cancelled: {
|
|
8539
8879
|
state: "cancelled",
|
|
8540
8880
|
reason: "native interaction cancelled",
|
|
@@ -8600,23 +8940,6 @@ function batchReason(key, detail = "") {
|
|
|
8600
8940
|
};
|
|
8601
8941
|
}
|
|
8602
8942
|
|
|
8603
|
-
// plugins/immune-brain/runtime/unattended/confirmation_deadline.ts
|
|
8604
|
-
var CONFIRMATION_TIMEOUT_DEFAULT_MS = 60000;
|
|
8605
|
-
var CONFIRMATION_TIMEOUT_ENV = "IMMUNE_BRAIN_BATCH_TIMEOUT_MS";
|
|
8606
|
-
function startConfirmationDeadline(input) {
|
|
8607
|
-
const configured = Number(input.env?.[CONFIRMATION_TIMEOUT_ENV]);
|
|
8608
|
-
const timeoutMs = Number.isFinite(configured) && configured > 0 ? configured : CONFIRMATION_TIMEOUT_DEFAULT_MS;
|
|
8609
|
-
const controller = new AbortController;
|
|
8610
|
-
const timer = setTimeout(() => {
|
|
8611
|
-
controller.abort(new Error("native confirmation timed out waiting for user interaction"));
|
|
8612
|
-
}, timeoutMs);
|
|
8613
|
-
return {
|
|
8614
|
-
signal: input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal,
|
|
8615
|
-
timedOut: () => controller.signal.aborted && !input.signal?.aborted,
|
|
8616
|
-
clear: () => clearTimeout(timer)
|
|
8617
|
-
};
|
|
8618
|
-
}
|
|
8619
|
-
|
|
8620
8943
|
// plugins/immune-brain/runtime/authorization_operation.ts
|
|
8621
8944
|
function deriveAuthorizationOperation(input) {
|
|
8622
8945
|
if (input.readiness.state === "resolve_user_decision")
|
|
@@ -8846,6 +9169,17 @@ function createGhTransport(binary = "gh") {
|
|
|
8846
9169
|
return new Promise((complete) => {
|
|
8847
9170
|
let stdout = Buffer.alloc(0);
|
|
8848
9171
|
let stderr = Buffer.alloc(0);
|
|
9172
|
+
if (options.signal?.aborted) {
|
|
9173
|
+
complete({
|
|
9174
|
+
exit_code: 1,
|
|
9175
|
+
stdout: "",
|
|
9176
|
+
stderr: "operation cancelled by the caller",
|
|
9177
|
+
timed_out: true,
|
|
9178
|
+
output_exceeded: false
|
|
9179
|
+
});
|
|
9180
|
+
return;
|
|
9181
|
+
}
|
|
9182
|
+
const remaining = GH_TIMEOUT_MS;
|
|
8849
9183
|
let timedOut = false;
|
|
8850
9184
|
let outputExceeded = false;
|
|
8851
9185
|
let timer;
|
|
@@ -8903,7 +9237,7 @@ function createGhTransport(binary = "gh") {
|
|
|
8903
9237
|
timer = setTimeout(() => {
|
|
8904
9238
|
timedOut = true;
|
|
8905
9239
|
child.kill("SIGKILL");
|
|
8906
|
-
},
|
|
9240
|
+
}, remaining);
|
|
8907
9241
|
child.once("close", (code) => {
|
|
8908
9242
|
finish(code ?? 1);
|
|
8909
9243
|
});
|
|
@@ -9007,6 +9341,33 @@ function initiativeLookup(issues, repositoryId, initiativeId) {
|
|
|
9007
9341
|
const initiative = marker("initiative-id", initiativeId);
|
|
9008
9342
|
return findIssue(issues, [initiative, KIND_INITIATIVE_MARKER], [marker("repo-id", repositoryId), initiative]);
|
|
9009
9343
|
}
|
|
9344
|
+
function publishedIntentHashMarkers(body) {
|
|
9345
|
+
return [...body.matchAll(/<!-- immune-brain:intent-hash=([A-Za-z0-9][A-Za-z0-9._:-]{0,127}) -->/g)].map((match) => match[1]);
|
|
9346
|
+
}
|
|
9347
|
+
function publishedIntentIdentity(body) {
|
|
9348
|
+
const hashes = publishedIntentHashMarkers(body);
|
|
9349
|
+
if (hashes.length === 0)
|
|
9350
|
+
return { kind: "absent" };
|
|
9351
|
+
if (hashes.length === 1)
|
|
9352
|
+
return { kind: "bound", hash: hashes[0] };
|
|
9353
|
+
return { kind: "ambiguous", hashes };
|
|
9354
|
+
}
|
|
9355
|
+
function publishedIntentMismatch(body, taskId, expected) {
|
|
9356
|
+
const identity = publishedIntentIdentity(body);
|
|
9357
|
+
if (identity.kind === "ambiguous")
|
|
9358
|
+
return `published Task ${taskId} carries ${identity.hashes.length} conflicting intent-hash markers; its published Intent identity is ambiguous`;
|
|
9359
|
+
if (identity.kind === "absent" || expected === undefined)
|
|
9360
|
+
return null;
|
|
9361
|
+
if (identity.hash !== expected)
|
|
9362
|
+
return `published Task ${taskId} was published from a different TaskIntent revision; the published identity no longer matches the Intent being published`;
|
|
9363
|
+
return null;
|
|
9364
|
+
}
|
|
9365
|
+
function sameTrackedBody(current, expected) {
|
|
9366
|
+
return stripPublishedIntentHash(current) === stripPublishedIntentHash(expected);
|
|
9367
|
+
}
|
|
9368
|
+
function stripPublishedIntentHash(body) {
|
|
9369
|
+
return body.replace(/<!-- immune-brain:intent-hash=[A-Za-z0-9][A-Za-z0-9._:-]{0,127} -->\n?/g, "");
|
|
9370
|
+
}
|
|
9010
9371
|
function ownershipMarkerValue(body, name) {
|
|
9011
9372
|
const values = [...body.matchAll(new RegExp(`<!-- immune-brain:${name}=([A-Za-z0-9][A-Za-z0-9._-]{0,127}) -->`, "g"))];
|
|
9012
9373
|
return values.length === 1 ? values[0][1] : null;
|
|
@@ -9206,7 +9567,7 @@ ${provenance}## How to use this Issue
|
|
|
9206
9567
|
|
|
9207
9568
|
- Edit planning prose and Slice ordering directly after creation.
|
|
9208
9569
|
- Keep each Slice marker attached to exactly one stable Slice entry.
|
|
9209
|
-
- The tracker never rewrites
|
|
9570
|
+
- The tracker never rewrites this Parent after creation; the tracker closes it as completed once every Slice Child is completed, and never closes it otherwise.
|
|
9210
9571
|
|
|
9211
9572
|
## Problem
|
|
9212
9573
|
|
|
@@ -9307,7 +9668,8 @@ function childBody(repository, operation, parent) {
|
|
|
9307
9668
|
marker("repo-id", repository.id),
|
|
9308
9669
|
marker("initiative-id", operation.initiative_id),
|
|
9309
9670
|
marker("slice-id", operation.slice_id),
|
|
9310
|
-
marker("task-id", operation.task_id)
|
|
9671
|
+
marker("task-id", operation.task_id),
|
|
9672
|
+
...operation.intent_hash ? [marker("intent-hash", operation.intent_hash)] : []
|
|
9311
9673
|
].join(`
|
|
9312
9674
|
`)}
|
|
9313
9675
|
|
|
@@ -9354,7 +9716,7 @@ ${publicText(projection.agent_handoff ?? "Implement only the bounded TaskIntent
|
|
|
9354
9716
|
${ISSUE_FOOTER}
|
|
9355
9717
|
`;
|
|
9356
9718
|
}
|
|
9357
|
-
async function upsertTask(root, gh, operation, source
|
|
9719
|
+
async function upsertTask(root, gh, operation, source) {
|
|
9358
9720
|
const labelFailure = await labelAvailabilityFailure(root, gh, source.repository, desiredTaskLabels(operation));
|
|
9359
9721
|
if (labelFailure)
|
|
9360
9722
|
return labelFailure;
|
|
@@ -9369,8 +9731,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
|
|
|
9369
9731
|
const found = lookup(source.issues);
|
|
9370
9732
|
if (found.kind === "ambiguous")
|
|
9371
9733
|
return result(operation.op, "ambiguous_remote_state", found.message);
|
|
9372
|
-
if (found.kind === "missing" && pendingBinding !== null && pendingBinding !== undefined)
|
|
9373
|
-
return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} is bound to Issue #${pendingBinding.issue_number} but that Issue no longer holds its Task marker; amend the binding or restore the marker instead of recreating`, parent.issue);
|
|
9374
9734
|
const blockerIds = operation.projection?.blocked_by ?? [];
|
|
9375
9735
|
const blockers = [];
|
|
9376
9736
|
for (const blockerId of blockerIds) {
|
|
@@ -9395,32 +9755,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
|
|
|
9395
9755
|
let createdChild = false;
|
|
9396
9756
|
let labelsConverged = false;
|
|
9397
9757
|
if (found.kind === "missing") {
|
|
9398
|
-
if (amendmentContext !== undefined) {
|
|
9399
|
-
const reRead = await snapshot(root, gh, operation.op);
|
|
9400
|
-
if ("contract" in reRead)
|
|
9401
|
-
return reRead;
|
|
9402
|
-
const reReadParent = initiativeLookup(reRead.issues, reRead.repository.id, operation.initiative_id);
|
|
9403
|
-
if (reReadParent.kind !== "found")
|
|
9404
|
-
return result(operation.op, "ambiguous_remote_state", reReadParent.kind === "ambiguous" ? reReadParent.message : "amendment Parent is not observable before creating a new Child", parent.issue);
|
|
9405
|
-
if (reReadParent.issue.number !== amendmentContext.parentIssueNumber)
|
|
9406
|
-
return result(operation.op, "ambiguous_remote_state", `amendment Parent is bound to Issue #${amendmentContext.parentIssueNumber} but observed Issue #${reReadParent.issue.number} before creating a new Child`, reReadParent.issue);
|
|
9407
|
-
if (reReadParent.issue.state !== "open")
|
|
9408
|
-
return result(operation.op, "ambiguous_remote_state", "amendment Parent is no longer open before creating a new Child", reReadParent.issue);
|
|
9409
|
-
if (amendmentContext.parent.title !== reReadParent.issue.title || amendmentContext.parent.body !== reReadParent.issue.body)
|
|
9410
|
-
return result(operation.op, "ambiguous_remote_state", "amendment Parent content changed before creating a new Child", reReadParent.issue);
|
|
9411
|
-
if (sliceCount(reReadParent.issue.body, operation.slice_id) !== 1)
|
|
9412
|
-
return result(operation.op, "ambiguous_remote_state", `Parent Issue #${reReadParent.issue.number} lost its exact Slice marker ${operation.slice_id} before creating a new Child`, reReadParent.issue);
|
|
9413
|
-
const raced = lookup(reRead.issues);
|
|
9414
|
-
if (raced.kind === "ambiguous")
|
|
9415
|
-
return result(operation.op, "ambiguous_remote_state", raced.message);
|
|
9416
|
-
if (raced.kind === "found") {
|
|
9417
|
-
const approved = amendmentContext.pendingContent.get(operation.task_id);
|
|
9418
|
-
const resumable = approved !== undefined && raced.issue.state === "open" && carriesApprovedContent(raced.issue.title, raced.issue.body, approved);
|
|
9419
|
-
if (!resumable)
|
|
9420
|
-
return result(operation.op, "ambiguous_remote_state", `new pending Task ${operation.task_id} is unbound but Issue #${raced.issue.number} already exists with divergent content; bind it to amend`, raced.issue);
|
|
9421
|
-
return updatePendingChild(root, gh, reRead, operation, raced.issue, undefined, blockers, approved, amendmentContext);
|
|
9422
|
-
}
|
|
9423
|
-
}
|
|
9424
9758
|
const mutation = await gh.run([
|
|
9425
9759
|
"issue",
|
|
9426
9760
|
"create",
|
|
@@ -9443,11 +9777,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
|
|
|
9443
9777
|
}
|
|
9444
9778
|
if (created.issue.body !== body || created.issue.title !== title)
|
|
9445
9779
|
return result(operation.op, "retryable_failure", "Task Issue did not converge to the requested title and body", created.issue);
|
|
9446
|
-
if (amendmentContext !== undefined) {
|
|
9447
|
-
if (created.issue.state !== "open")
|
|
9448
|
-
return result(operation.op, "ambiguous_remote_state", `new pending Task ${operation.task_id} (Issue #${created.issue.number}) is not open after creation`, created.issue);
|
|
9449
|
-
return updatePendingChild(root, gh, source, operation, created.issue, undefined, blockers, amendmentContext.pendingContent.get(operation.task_id), amendmentContext);
|
|
9450
|
-
}
|
|
9451
9780
|
child = created.issue;
|
|
9452
9781
|
createdChild = true;
|
|
9453
9782
|
} else {
|
|
@@ -9455,29 +9784,11 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
|
|
|
9455
9784
|
if (owned.kind !== "found")
|
|
9456
9785
|
return result(operation.op, "ambiguous_remote_state", owned.kind === "ambiguous" ? owned.message : "Task Issue ownership changed during publication", found.issue);
|
|
9457
9786
|
child = owned.issue;
|
|
9458
|
-
if (
|
|
9459
|
-
if (child.state !== "open")
|
|
9460
|
-
return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} is closed and cannot be amended as pending work`, found.issue);
|
|
9461
|
-
if (pendingBinding !== undefined && pendingBinding.issue_number !== child.number)
|
|
9462
|
-
return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} is bound to Issue #${pendingBinding.issue_number} but observed Issue #${child.number}`, found.issue);
|
|
9463
|
-
const approvedFinal = amendmentContext?.pendingContent.get(operation.task_id);
|
|
9464
|
-
const isFinal = approvedFinal !== undefined && carriesApprovedContent(found.issue.title, found.issue.body, approvedFinal);
|
|
9465
|
-
if (!isFinal) {
|
|
9466
|
-
const baselineMatches = pendingBinding !== undefined && pendingBinding.title === found.issue.title && carriesApprovedContent(found.issue.title, found.issue.body, pendingBinding);
|
|
9467
|
-
if (!baselineMatches)
|
|
9468
|
-
return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} changed since the approved amendment baseline`, found.issue);
|
|
9469
|
-
}
|
|
9470
|
-
return updatePendingChild(root, gh, source, operation, child, pendingBinding?.issue_number, blockers, approvedFinal, amendmentContext);
|
|
9471
|
-
}
|
|
9472
|
-
if (amendmentContext !== undefined) {
|
|
9473
|
-
const approvedFinal = amendmentContext.pendingContent.get(operation.task_id);
|
|
9474
|
-
const resumable = approvedFinal !== undefined && child.state === "open" && carriesApprovedContent(found.issue.title, found.issue.body, approvedFinal);
|
|
9475
|
-
if (!resumable)
|
|
9476
|
-
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
|
-
return updatePendingChild(root, gh, source, operation, child, undefined, blockers, approvedFinal, amendmentContext);
|
|
9478
|
-
}
|
|
9479
|
-
if (found.issue.body !== body || found.issue.title !== title)
|
|
9787
|
+
if (!sameTrackedBody(found.issue.body, body) || found.issue.title !== title)
|
|
9480
9788
|
return result(operation.op, "permanent_failure", "Task Issue already exists with a different title or Agent Brief; edit the GitHub source or retry the original projection before changing native relations", found.issue);
|
|
9789
|
+
const intentMismatch = publishedIntentMismatch(found.issue.body, operation.task_id, operation.intent_hash);
|
|
9790
|
+
if (intentMismatch)
|
|
9791
|
+
return result(operation.op, "ambiguous_remote_state", intentMismatch, found.issue);
|
|
9481
9792
|
const observedLabelArgs = labelMutationArgs(child.labels, desiredTaskLabels(operation));
|
|
9482
9793
|
if (observedLabelArgs.length) {
|
|
9483
9794
|
const edited = await gh.run([
|
|
@@ -9513,8 +9824,11 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
|
|
|
9513
9824
|
if ("contract" in finalSource)
|
|
9514
9825
|
return finalSource;
|
|
9515
9826
|
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
|
|
9827
|
+
if (finalChild.kind !== "found" || finalChild.issue.id !== child.id || finalChild.issue.title !== title || !sameTrackedBody(finalChild.issue.body, body))
|
|
9517
9828
|
return result(operation.op, "ambiguous_remote_state", "Task Issue changed identity, title, or body during dependency publication", child);
|
|
9829
|
+
const finalIdentityMismatch = publishedIntentMismatch(finalChild.issue.body, operation.task_id, operation.intent_hash);
|
|
9830
|
+
if (finalIdentityMismatch)
|
|
9831
|
+
return result(operation.op, "ambiguous_remote_state", finalIdentityMismatch, child);
|
|
9518
9832
|
const finalChildOwnership = await confirmTerminalOwnership(root, gh, operation.op, finalSource, finalChild.issue);
|
|
9519
9833
|
if (!("owned" in finalChildOwnership))
|
|
9520
9834
|
return finalChildOwnership;
|
|
@@ -9576,7 +9890,7 @@ async function closeTerminalIssue(root, gh, operation, source, issue, lookup) {
|
|
|
9576
9890
|
return result(operation.op, "updated", "terminal Task Issue closure confirmed", found.issue);
|
|
9577
9891
|
return close.exit_code !== 0 ? ghFailure(operation.op, close, "terminal Issue closure failed") : result(operation.op, "retryable_failure", "terminal Issue closure did not converge", found.issue);
|
|
9578
9892
|
}
|
|
9579
|
-
async function
|
|
9893
|
+
async function markChildTerminal(root, gh, operation, source) {
|
|
9580
9894
|
const lookup = (issues) => taskLookup(issues, source.repository.id, operation.task_id);
|
|
9581
9895
|
const found = lookup(source.issues);
|
|
9582
9896
|
if (found.kind === "missing")
|
|
@@ -9626,6 +9940,69 @@ async function markTerminal(root, gh, operation, source) {
|
|
|
9626
9940
|
return refreshedOwnership;
|
|
9627
9941
|
return closeTerminalIssue(root, gh, operation, refreshed, reread.issue, lookup);
|
|
9628
9942
|
}
|
|
9943
|
+
async function closeParentWhenComplete(root, gh, operation, source, childResult) {
|
|
9944
|
+
const child = taskLookup(source.issues, source.repository.id, operation.task_id);
|
|
9945
|
+
if (child.kind !== "found")
|
|
9946
|
+
return childResult;
|
|
9947
|
+
const initiativeId = ownershipMarkerValue(child.issue.body, "initiative-id");
|
|
9948
|
+
if (!initiativeId)
|
|
9949
|
+
return childResult;
|
|
9950
|
+
const parentLookup = initiativeLookup(source.issues, source.repository.id, initiativeId);
|
|
9951
|
+
if (parentLookup.kind !== "found")
|
|
9952
|
+
return childResult;
|
|
9953
|
+
const parentNumber = parentLookup.issue.number;
|
|
9954
|
+
const retry = (message, status = "retryable_failure") => result(operation.op, status, `Parent #${parentNumber}: ${message}; the Task closure is unchanged, retry the same mark-terminal`, child.issue);
|
|
9955
|
+
const endpoint = `repos/${source.repository.name_with_owner}/issues/${parentNumber}`;
|
|
9956
|
+
const readParent = async () => {
|
|
9957
|
+
const read = await gh.run(["api", endpoint], { cwd: root });
|
|
9958
|
+
if (read.exit_code !== 0 || read.output_exceeded) {
|
|
9959
|
+
const failed = ghFailure(operation.op, read, "cannot read the Initiative Parent");
|
|
9960
|
+
return retry(failed.message, failed.status);
|
|
9961
|
+
}
|
|
9962
|
+
try {
|
|
9963
|
+
const [issue] = parseIssues(`[${read.stdout}]`);
|
|
9964
|
+
return issue ?? retry("gh returned a malformed Issue", "permanent_failure");
|
|
9965
|
+
} catch (error) {
|
|
9966
|
+
return retry(error instanceof Error ? error.message : String(error), "permanent_failure");
|
|
9967
|
+
}
|
|
9968
|
+
};
|
|
9969
|
+
const parent = await readParent();
|
|
9970
|
+
if ("contract" in parent)
|
|
9971
|
+
return parent;
|
|
9972
|
+
if (parent.state === "closed")
|
|
9973
|
+
return childResult;
|
|
9974
|
+
const listed = await gh.run(["api", "--paginate", "--slurp", `${endpoint}/sub_issues?per_page=100`], { cwd: root });
|
|
9975
|
+
if (listed.exit_code !== 0 || listed.output_exceeded) {
|
|
9976
|
+
const failed = ghFailure(operation.op, listed, "cannot read the Initiative Sub-issues");
|
|
9977
|
+
return retry(failed.message, failed.status);
|
|
9978
|
+
}
|
|
9979
|
+
let children;
|
|
9980
|
+
try {
|
|
9981
|
+
children = parseIssues(listed.stdout);
|
|
9982
|
+
} catch (error) {
|
|
9983
|
+
return retry(error instanceof Error ? error.message : String(error), "permanent_failure");
|
|
9984
|
+
}
|
|
9985
|
+
const sliceChildren = children.filter((candidate) => candidate.body.includes(marker("initiative-id", initiativeId)) && /<!-- immune-brain:slice-id=[^>]+ -->/.test(candidate.body));
|
|
9986
|
+
if (sliceChildren.length === 0 || !sliceChildren.every((candidate) => candidate.state === "closed" && candidate.state_reason === "completed"))
|
|
9987
|
+
return childResult;
|
|
9988
|
+
const close = await gh.run(["issue", "close", String(parentNumber), "--repo", source.repository.name_with_owner, "--reason", "completed"], { cwd: root });
|
|
9989
|
+
const confirmed = await readParent();
|
|
9990
|
+
if ("contract" in confirmed)
|
|
9991
|
+
return confirmed;
|
|
9992
|
+
if (confirmed.state === "closed" && confirmed.state_reason === "completed")
|
|
9993
|
+
return result(operation.op, "updated", `${childResult.message}; Initiative Parent #${parentNumber} closed as completed`, child.issue);
|
|
9994
|
+
if (close.exit_code !== 0 || close.output_exceeded) {
|
|
9995
|
+
const failed = ghFailure(operation.op, close, "Initiative Parent closure failed");
|
|
9996
|
+
return retry(failed.message, failed.status);
|
|
9997
|
+
}
|
|
9998
|
+
return retry("closure did not converge");
|
|
9999
|
+
}
|
|
10000
|
+
async function markTerminal(root, gh, operation, source) {
|
|
10001
|
+
const terminal = await markChildTerminal(root, gh, operation, source);
|
|
10002
|
+
if (operation.phase !== "done" || terminal.status !== "updated" && terminal.status !== "already_current" || !terminal.association_found)
|
|
10003
|
+
return terminal;
|
|
10004
|
+
return closeParentWhenComplete(root, gh, operation, source, terminal);
|
|
10005
|
+
}
|
|
9629
10006
|
function normalizedList(value, name, max = 2000) {
|
|
9630
10007
|
if (value === undefined)
|
|
9631
10008
|
return;
|
|
@@ -9766,756 +10143,116 @@ async function runGithubTrackerOperation(root, input, gh = createGhTransport())
|
|
|
9766
10143
|
return markTerminal(absoluteRoot, gh, operation, source);
|
|
9767
10144
|
}
|
|
9768
10145
|
}
|
|
9769
|
-
function issueTerminalEventId(body) {
|
|
9770
|
-
const suffixMatch = [...body.matchAll(/<!-- immune-brain:terminal-event=([A-Za-z0-9._:-]+) -->/g)];
|
|
9771
|
-
if (suffixMatch.length === 0)
|
|
9772
|
-
return null;
|
|
9773
|
-
if (suffixMatch.length > 1)
|
|
9774
|
-
return "multiple";
|
|
9775
|
-
const eventId = suffixMatch[0][1];
|
|
9776
|
-
if (eventId.length > MAX_TERMINAL_EVENT_ID || !body.endsWith(terminalSuffix(eventId)))
|
|
9777
|
-
return "malformed";
|
|
9778
|
-
return eventId;
|
|
9779
|
-
}
|
|
9780
|
-
function stripTerminalSuffixFromBytes(body) {
|
|
9781
|
-
if (body === undefined)
|
|
9782
|
-
return null;
|
|
9783
|
-
const eventId = issueTerminalEventId(body);
|
|
9784
|
-
if (eventId === null || eventId === "multiple" || eventId === "malformed")
|
|
9785
|
-
return eventId;
|
|
9786
|
-
const suffix = terminalSuffix(eventId);
|
|
9787
|
-
if (!body.endsWith(suffix))
|
|
9788
|
-
return null;
|
|
9789
|
-
return body.slice(0, body.length - suffix.length);
|
|
9790
|
-
}
|
|
9791
|
-
function carriesApprovedContent(title, body, approved) {
|
|
9792
|
-
if (title !== approved.title)
|
|
9793
|
-
return false;
|
|
9794
|
-
if (body === approved.body)
|
|
9795
|
-
return true;
|
|
9796
|
-
const stripped = stripTerminalSuffixFromBytes(body);
|
|
9797
|
-
return typeof stripped === "string" && stripped.trimEnd() === approved.body.trimEnd();
|
|
9798
|
-
}
|
|
9799
|
-
async function updatePendingChild(root, gh, source, op, child, boundNumber, desiredBlockers, approvedFinal, amendmentContext = undefined) {
|
|
9800
|
-
const parent = initiativeLookup(source.issues, source.repository.id, op.initiative_id);
|
|
9801
|
-
const body = childBody(source.repository, op, parent.kind === "found" ? parent.issue : child);
|
|
9802
|
-
const oversized = bodyLimitFailure(op.op, body, MAX_TERMINAL_SUFFIX_BYTES);
|
|
9803
|
-
if (oversized)
|
|
9804
|
-
return oversized;
|
|
9805
|
-
const title = taskIssueTitle(op, sliceOrdinalFromChecklist(parent.kind === "found" ? parent.issue.body : child.body, op.slice_id, op.projection?.slice_ordinal ?? 1));
|
|
9806
|
-
const parentApprovedContent = amendmentContext?.parent;
|
|
9807
|
-
const parentBoundNumber = amendmentContext?.parentIssueNumber;
|
|
9808
|
-
const baseBody = child.body;
|
|
9809
|
-
const suffixEvent = issueTerminalEventId(baseBody);
|
|
9810
|
-
if (suffixEvent === "multiple")
|
|
9811
|
-
return result(op.op, "ambiguous_remote_state", "pending Task has multiple terminal markers", child);
|
|
9812
|
-
if (suffixEvent === "malformed")
|
|
9813
|
-
return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a malformed terminal marker (marker without its exact canonical suffix)`, child);
|
|
9814
|
-
let finalBody = suffixEvent !== null ? `${body.trimEnd()}${terminalSuffix(suffixEvent)}` : body;
|
|
9815
|
-
const approvedMatches = approvedFinal !== undefined && carriesApprovedContent(title, body, approvedFinal);
|
|
9816
|
-
if (!approvedMatches)
|
|
9817
|
-
return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a terminal suffix that diverges from the approved amendment content`, child);
|
|
9818
|
-
const approvedNow = approvedFinal !== undefined && carriesApprovedContent(child.title, child.body, approvedFinal);
|
|
9819
|
-
if (approvedNow) {
|
|
9820
|
-
const currentParent = initiativeLookup(source.issues, source.repository.id, op.initiative_id);
|
|
9821
|
-
if (currentParent.kind !== "found")
|
|
9822
|
-
return result(op.op, "ambiguous_remote_state", "pending Task Parent is not observable before attachment convergence", child);
|
|
9823
|
-
const revalidated = await revalidatePendingChildBeforeWrite(root, gh, child.number, op.task_id, approvedFinal, undefined, true, parentApprovedContent, parentBoundNumber);
|
|
9824
|
-
if ("contract" in revalidated)
|
|
9825
|
-
return revalidated;
|
|
9826
|
-
const targetParentNumber = parentBoundNumber ?? currentParent.issue.number;
|
|
9827
|
-
const attachment = await confirmAttachment(root, gh, op.op, source.repository, targetParentNumber, child.number);
|
|
9828
|
-
if (!("attached" in attachment))
|
|
9829
|
-
return attachment;
|
|
9830
|
-
if (!attachment.attached) {
|
|
9831
|
-
const attached = await attachSubIssue(root, gh, op.op, source.repository, targetParentNumber, child);
|
|
9832
|
-
if (!("attached" in attached))
|
|
9833
|
-
return attached;
|
|
9834
|
-
}
|
|
9835
|
-
}
|
|
9836
|
-
let observed = child;
|
|
9837
|
-
const desiredLabels = desiredTaskLabels(op);
|
|
9838
|
-
if (child.title !== title || child.body !== finalBody || labelMutationArgs(child.labels, desiredLabels).length) {
|
|
9839
|
-
const revalidated = await revalidatePendingChildBeforeWrite(root, gh, child.number, op.task_id, approvedFinal, { title: child.title, body: child.body }, false, parentApprovedContent, parentBoundNumber);
|
|
9840
|
-
if ("contract" in revalidated)
|
|
9841
|
-
return revalidated;
|
|
9842
|
-
observed = revalidated;
|
|
9843
|
-
const observedEvent = issueTerminalEventId(observed.body);
|
|
9844
|
-
if (observedEvent === "malformed")
|
|
9845
|
-
return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a malformed terminal marker (marker without its exact canonical suffix)`, child);
|
|
9846
|
-
const writeBody = typeof observedEvent === "string" ? `${body.trimEnd()}${terminalSuffix(observedEvent)}` : finalBody;
|
|
9847
|
-
const labelArgs = labelMutationArgs(observed.labels, desiredLabels);
|
|
9848
|
-
if (observed.title !== title || observed.body !== writeBody || labelArgs.length) {
|
|
9849
|
-
const edited = await gh.run([
|
|
9850
|
-
"issue",
|
|
9851
|
-
"edit",
|
|
9852
|
-
String(child.number),
|
|
9853
|
-
"--repo",
|
|
9854
|
-
source.repository.name_with_owner,
|
|
9855
|
-
"--title",
|
|
9856
|
-
title,
|
|
9857
|
-
"--body-file",
|
|
9858
|
-
"-",
|
|
9859
|
-
...labelArgs
|
|
9860
|
-
], { cwd: root, stdin: writeBody });
|
|
9861
|
-
if (edited.exit_code !== 0 || edited.output_exceeded)
|
|
9862
|
-
return ghFailure(op.op, edited, `pending Task Issue #${child.number} update failed`);
|
|
9863
|
-
finalBody = writeBody;
|
|
9864
|
-
}
|
|
9865
|
-
}
|
|
9866
|
-
const dependencies = await convergePendingDependencies(root, gh, source, child.number, op.task_id, desiredBlockers, approvedFinal, parentApprovedContent, parentBoundNumber);
|
|
9867
|
-
if (!("complete" in dependencies))
|
|
9868
|
-
return dependencies;
|
|
9869
|
-
const refreshed = await snapshot(root, gh, op.op);
|
|
9870
|
-
if ("contract" in refreshed)
|
|
9871
|
-
return refreshed;
|
|
9872
|
-
const reread = ownedTaskLookup(refreshed.issues, refreshed.repository.id, op.task_id, op.initiative_id, op.slice_id);
|
|
9873
|
-
if (reread.kind !== "found" || reread.issue.number !== child.number)
|
|
9874
|
-
return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} changed identity during amendment`, child);
|
|
9875
|
-
if (reread.issue.title !== title || reread.issue.body !== finalBody)
|
|
9876
|
-
return result(op.op, "retryable_failure", `pending Task ${op.task_id} update did not converge`, reread.issue);
|
|
9877
|
-
const labelsCurrent = !desiredLabels.some((label) => !reread.issue.labels.includes(label));
|
|
9878
|
-
if (!labelsCurrent)
|
|
9879
|
-
return result(op.op, "retryable_failure", `pending Task ${op.task_id} labels did not converge`, reread.issue);
|
|
9880
|
-
const currentDependencies = await confirmBlockedBy(root, gh, op.op, refreshed.repository, reread.issue.number, desiredBlockers);
|
|
9881
|
-
if (!("complete" in currentDependencies))
|
|
9882
|
-
return currentDependencies;
|
|
9883
|
-
if (!currentDependencies.complete)
|
|
9884
|
-
return result(op.op, "retryable_failure", `pending Task ${op.task_id} dependencies did not converge`, reread.issue);
|
|
9885
|
-
const contentCurrent = child.title === title && child.body === finalBody && labelMutationArgs(child.labels, desiredLabels).length === 0;
|
|
9886
|
-
return contentCurrent ? result(op.op, "already_current", `pending Task ${op.task_id} already carries the approved amendment content`, reread.issue) : result(op.op, "updated", `pending Task ${op.task_id} Agent Brief updated with approved amendment content`, reread.issue);
|
|
9887
|
-
}
|
|
9888
|
-
async function convergePendingDependencies(root, gh, source, childNumber, childTaskId, requestedBlockers, approvedFinal, parentApprovedContent = undefined, parentBoundNumber = undefined) {
|
|
9889
|
-
const expected = requestedBlockers.map((blocker) => blocker.id);
|
|
9890
|
-
const existing = await readBlockedByIds(root, gh, "upsert-task", source.repository, childNumber);
|
|
9891
|
-
if (!Array.isArray(existing))
|
|
9892
|
-
return existing;
|
|
9893
|
-
const removed = existing.filter((id) => !expected.includes(id));
|
|
9894
|
-
for (const id of removed) {
|
|
9895
|
-
const revalidated = await revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, undefined, false, parentApprovedContent, parentBoundNumber);
|
|
9896
|
-
if ("contract" in revalidated)
|
|
9897
|
-
return revalidated;
|
|
9898
|
-
const mutation = await gh.run([
|
|
9899
|
-
"api",
|
|
9900
|
-
"--method",
|
|
9901
|
-
"DELETE",
|
|
9902
|
-
`repos/${source.repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by/${id}`
|
|
9903
|
-
], { cwd: root });
|
|
9904
|
-
if (mutation.exit_code !== 0 || mutation.output_exceeded)
|
|
9905
|
-
return ghFailure("upsert-task", mutation, `native blocked_by removal failed for Issue #${childNumber}`);
|
|
9906
|
-
}
|
|
9907
|
-
const additions = requestedBlockers.filter((blocker) => !existing.includes(blocker.id));
|
|
9908
|
-
for (const blocker of additions) {
|
|
9909
|
-
const revalidated = await revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, undefined, false, parentApprovedContent, parentBoundNumber);
|
|
9910
|
-
if ("contract" in revalidated)
|
|
9911
|
-
return revalidated;
|
|
9912
|
-
const mutation = await gh.run([
|
|
9913
|
-
"api",
|
|
9914
|
-
"-F",
|
|
9915
|
-
`issue_id=${blocker.id}`,
|
|
9916
|
-
`repos/${source.repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by`
|
|
9917
|
-
], { cwd: root });
|
|
9918
|
-
if (mutation.exit_code !== 0 || mutation.output_exceeded)
|
|
9919
|
-
return ghFailure("upsert-task", mutation, `native blocked_by attachment failed for Issue #${blocker.number}`);
|
|
9920
|
-
}
|
|
9921
|
-
const confirm = await confirmBlockedBy(root, gh, "upsert-task", source.repository, childNumber, requestedBlockers);
|
|
9922
|
-
if (!("complete" in confirm))
|
|
9923
|
-
return confirm;
|
|
9924
|
-
return confirm.complete ? { complete: true } : { complete: true };
|
|
9925
|
-
}
|
|
9926
|
-
async function revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, allowedBaseline = undefined, skipAttachmentCheck = false, parentApproved = undefined, parentExpectedNumber = undefined) {
|
|
9927
|
-
const refreshed = await snapshot(root, gh, "upsert-task");
|
|
9928
|
-
if ("contract" in refreshed)
|
|
9929
|
-
return refreshed;
|
|
9930
|
-
const resolved = taskLookup(refreshed.issues, refreshed.repository.id, childTaskId);
|
|
9931
|
-
if (resolved.kind !== "found")
|
|
9932
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} is not uniquely owned before a write: ${resolved.kind}`);
|
|
9933
|
-
const child = resolved.issue;
|
|
9934
|
-
if (child.number !== childNumber)
|
|
9935
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} resolves to Issue #${child.number}, not the bound Issue #${childNumber}`, child);
|
|
9936
|
-
if (child.state !== "open")
|
|
9937
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) is no longer open before a dependency write`, child);
|
|
9938
|
-
const contentFinal = approvedFinal !== undefined && carriesApprovedContent(child.title, child.body, approvedFinal);
|
|
9939
|
-
const contentBaseline = allowedBaseline !== undefined && child.title === allowedBaseline.title && child.body === allowedBaseline.body;
|
|
9940
|
-
if (!contentFinal && !contentBaseline)
|
|
9941
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) content is no longer the approved final bytes before a dependency write`, child);
|
|
9942
|
-
const initiativeId = [...child.body.matchAll(/<!-- immune-brain:initiative-id=([A-Za-z0-9._:-]+) -->/g)].map((match) => match[1])[0];
|
|
9943
|
-
if (!initiativeId)
|
|
9944
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its Initiative marker before a dependency write`, child);
|
|
9945
|
-
const parent = initiativeLookup(refreshed.issues, refreshed.repository.id, initiativeId);
|
|
9946
|
-
if (parent.kind !== "found" || parent.issue.state !== "open")
|
|
9947
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its open Parent before a dependency write`, child);
|
|
9948
|
-
if (parentExpectedNumber !== undefined && parent.issue.number !== parentExpectedNumber)
|
|
9949
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) Parent resolves to Issue #${parent.issue.number}, not the bound Parent Issue #${parentExpectedNumber} before a dependency write`, child);
|
|
9950
|
-
if (parentApproved !== undefined && (parent.issue.title !== parentApproved.title || parent.issue.body !== parentApproved.body))
|
|
9951
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) Parent changed since the approved amendment content before a dependency write`, child);
|
|
9952
|
-
if (!skipAttachmentCheck) {
|
|
9953
|
-
const attachedNow = await readSubIssueNumbers(root, gh, "upsert-task", refreshed.repository, parent.issue.number);
|
|
9954
|
-
if (!Array.isArray(attachedNow))
|
|
9955
|
-
return attachedNow;
|
|
9956
|
-
if (!attachedNow.includes(childNumber))
|
|
9957
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) is no longer attached to the Parent before a dependency write`, child);
|
|
9958
|
-
}
|
|
9959
|
-
const sliceId = ownershipMarkerValue(child.body, "slice-id");
|
|
9960
|
-
if (sliceId && sliceCount(parent.issue.body, sliceId) !== 1)
|
|
9961
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its exact Slice in the Parent before a dependency write`, child);
|
|
9962
|
-
return child;
|
|
9963
|
-
}
|
|
9964
10146
|
|
|
9965
10147
|
// plugins/immune-brain/runtime/unattended/batch_preflight.ts
|
|
9966
10148
|
import { existsSync as existsSync7, readdirSync as readdirSync6, readFileSync as readFileSync13 } from "node:fs";
|
|
9967
10149
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
9968
10150
|
import { join as join12 } from "node:path";
|
|
9969
|
-
import { spawnSync as
|
|
10151
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
9970
10152
|
|
|
9971
|
-
// plugins/immune-brain/runtime/
|
|
9972
|
-
import {
|
|
9973
|
-
var BATCH_AUTHORITY_CAPABILITY_BRAND = Symbol.for("assurance-kernel.batch-authority-capability-brand");
|
|
9974
|
-
var GIT_COMMIT_ID2 = /^[a-f0-9]{40}$/;
|
|
10153
|
+
// plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts
|
|
10154
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
9975
10155
|
|
|
9976
|
-
|
|
9977
|
-
|
|
9978
|
-
|
|
9979
|
-
|
|
9980
|
-
|
|
9981
|
-
|
|
9982
|
-
|
|
9983
|
-
|
|
10156
|
+
// plugins/immune-brain/runtime/unattended/batch_state.ts
|
|
10157
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, openSync as openSync6, closeSync as closeSync6, writeFileSync as writeFileSync6, renameSync as renameSync3, lstatSync as lstatSync9, constants as constants5, rmSync as rmSync6 } from "node:fs";
|
|
10158
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
10159
|
+
import { dirname as dirname7, join as join11 } from "node:path";
|
|
10160
|
+
var BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10161
|
+
var CHILD_RUN_STATES = new Set([
|
|
10162
|
+
"pending",
|
|
10163
|
+
"enrolled",
|
|
10164
|
+
"settled",
|
|
10165
|
+
"committed",
|
|
10166
|
+
"needs_human",
|
|
10167
|
+
"skipped_blocked"
|
|
10168
|
+
]);
|
|
10169
|
+
var BATCH_RUN_STATES = new Set([
|
|
10170
|
+
"prepared",
|
|
10171
|
+
"running",
|
|
10172
|
+
"needs_human",
|
|
10173
|
+
"completed",
|
|
10174
|
+
"budget_stopped",
|
|
10175
|
+
"failed",
|
|
10176
|
+
"rejected"
|
|
10177
|
+
]);
|
|
10178
|
+
var ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
10179
|
+
function isCanonicalTimestamp(value) {
|
|
10180
|
+
if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN.test(value))
|
|
10181
|
+
return false;
|
|
10182
|
+
const milliseconds = Date.parse(value);
|
|
10183
|
+
return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === value;
|
|
9984
10184
|
}
|
|
9985
|
-
function
|
|
9986
|
-
if (
|
|
9987
|
-
|
|
9988
|
-
if (Array.isArray(value))
|
|
9989
|
-
return `[${value.map((entry) => stableStringify3(entry)).join(",")}]`;
|
|
9990
|
-
const record = value;
|
|
9991
|
-
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify3(record[key])}`).join(",")}}`;
|
|
10185
|
+
function validateBatchId(batchId) {
|
|
10186
|
+
if (typeof batchId !== "string" || !BATCH_ID_PATTERN.test(batchId))
|
|
10187
|
+
throw new Error("batch id is not a safe file identity");
|
|
9992
10188
|
}
|
|
9993
|
-
function
|
|
9994
|
-
|
|
9995
|
-
|
|
9996
|
-
intent_content_hash: child.intent_content_hash,
|
|
9997
|
-
intent_path: child.intent_path,
|
|
9998
|
-
intent_revision: child.intent_revision,
|
|
9999
|
-
task_id: child.task_id
|
|
10000
|
-
}));
|
|
10001
|
-
return `sha256:${sha256Hex3(stableStringify3(canonical))}`;
|
|
10189
|
+
function statePath(batchId) {
|
|
10190
|
+
validateBatchId(batchId);
|
|
10191
|
+
return join11(".imm", "state", "batches", `${batchId}.json`);
|
|
10002
10192
|
}
|
|
10003
|
-
function
|
|
10004
|
-
const
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
"initiative_slug",
|
|
10008
|
-
"plan_digest",
|
|
10009
|
-
"branch",
|
|
10010
|
-
"base_head",
|
|
10011
|
-
"actor_id",
|
|
10012
|
-
"confirmation_ref",
|
|
10013
|
-
"expires_at",
|
|
10014
|
-
"nonce"
|
|
10015
|
-
]) {
|
|
10016
|
-
const value = binding[key];
|
|
10017
|
-
if (value === undefined || value === null || value === "")
|
|
10018
|
-
missing.push(key);
|
|
10019
|
-
}
|
|
10020
|
-
if (!binding.budget || typeof binding.budget !== "object")
|
|
10021
|
-
missing.push("budget");
|
|
10022
|
-
if (missing.length > 0)
|
|
10023
|
-
throw new Error(`batch authorization binding is incomplete: ${missing.join(", ")}`);
|
|
10193
|
+
function withoutRetiredClock(record) {
|
|
10194
|
+
const { authorization_expires_at: _expiry, ...rest } = record;
|
|
10195
|
+
const { deadline_at: _deadline, ...budget } = rest.budget;
|
|
10196
|
+
return { ...rest, budget };
|
|
10024
10197
|
}
|
|
10025
|
-
function
|
|
10026
|
-
|
|
10027
|
-
|
|
10028
|
-
if (!Number.isInteger(budget.qa_failure_limit) || budget.qa_failure_limit <= 0)
|
|
10029
|
-
throw new Error("batch budget qa_failure_limit must be a positive integer");
|
|
10030
|
-
const deadline = Date.parse(budget.deadline_at);
|
|
10031
|
-
if (Number.isNaN(deadline) || deadline <= Date.parse(issuedAt))
|
|
10032
|
-
throw new Error("batch budget must have a future deadline_at");
|
|
10198
|
+
function canonicalBytes(record) {
|
|
10199
|
+
return `${JSON.stringify(withoutRetiredClock(record), null, 2)}
|
|
10200
|
+
`;
|
|
10033
10201
|
}
|
|
10034
|
-
function
|
|
10035
|
-
if (
|
|
10036
|
-
throw new Error(
|
|
10037
|
-
const
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10202
|
+
function validateRecordShape(value, batchId) {
|
|
10203
|
+
if (typeof value !== "object" || value === null)
|
|
10204
|
+
throw new Error(`batch run state ${batchId} is not an object`);
|
|
10205
|
+
const record = value;
|
|
10206
|
+
if (record.contract !== "assurance_kernel/batch_run_state/v1")
|
|
10207
|
+
throw new Error(`batch run state ${batchId} has an unknown contract`);
|
|
10208
|
+
if (record.batch_id !== batchId)
|
|
10209
|
+
throw new Error(`batch run state ${batchId} carries batch_id ${String(record.batch_id)}`);
|
|
10210
|
+
if (typeof record.plan_digest !== "string" || !record.plan_digest)
|
|
10211
|
+
throw new Error(`batch run state ${batchId} has an invalid plan_digest`);
|
|
10212
|
+
if (typeof record.base_head !== "string" || !record.base_head)
|
|
10213
|
+
throw new Error(`batch run state ${batchId} has an invalid base_head`);
|
|
10214
|
+
if (record.branch !== undefined && (typeof record.branch !== "string" || !record.branch))
|
|
10215
|
+
throw new Error(`batch run state ${batchId} has an invalid branch`);
|
|
10216
|
+
if (!isCanonicalTimestamp(record.confirmation_time))
|
|
10217
|
+
throw new Error(`batch run state ${batchId} has an invalid confirmation_time`);
|
|
10218
|
+
if (!isCanonicalTimestamp(record.created_at) || !isCanonicalTimestamp(record.updated_at))
|
|
10219
|
+
throw new Error(`batch run state ${batchId} has invalid state timestamps`);
|
|
10220
|
+
if (!Array.isArray(record.children) || record.children.length === 0)
|
|
10221
|
+
throw new Error(`batch run state ${batchId} has no children`);
|
|
10222
|
+
if (!BATCH_RUN_STATES.has(String(record.batch_state)))
|
|
10223
|
+
throw new Error(`batch run state ${batchId} has an invalid batch_state`);
|
|
10224
|
+
if (typeof record.consecutive_qa_failures !== "number" || !Number.isInteger(record.consecutive_qa_failures) || record.consecutive_qa_failures < 0)
|
|
10225
|
+
throw new Error(`batch run state ${batchId} has an invalid consecutive_qa_failures`);
|
|
10226
|
+
const budget = record.budget;
|
|
10227
|
+
if (typeof record.budget !== "object" || record.budget === null || typeof budget.max_children !== "number" || !Number.isInteger(budget.max_children) || budget.max_children <= 0 || typeof budget.qa_failure_limit !== "number" || !Number.isInteger(budget.qa_failure_limit) || budget.qa_failure_limit <= 0)
|
|
10228
|
+
throw new Error(`batch run state ${batchId} has an invalid budget`);
|
|
10229
|
+
if (!Array.isArray(record.commits) || record.commits.some((c) => typeof c !== "string"))
|
|
10230
|
+
throw new Error(`batch run state ${batchId} has an invalid commits list`);
|
|
10231
|
+
if (record.adopted_heads !== undefined && (!Array.isArray(record.adopted_heads) || record.adopted_heads.some((a) => typeof a !== "object" || a === null || typeof a.from !== "string" || !a.from || typeof a.to !== "string" || !a.to)))
|
|
10232
|
+
throw new Error(`batch run state ${batchId} has an invalid adopted_heads list`);
|
|
10233
|
+
const seenTaskIds = new Set;
|
|
10234
|
+
for (const child of record.children) {
|
|
10235
|
+
if (typeof child !== "object" || child === null || typeof child.task_id !== "string" || !child.task_id || typeof child.slice_id !== "string" || !child.slice_id)
|
|
10236
|
+
throw new Error(`batch run state ${batchId} has an invalid child entry`);
|
|
10237
|
+
if (!CHILD_RUN_STATES.has(String(child.state)))
|
|
10238
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid state`);
|
|
10239
|
+
if (!Array.isArray(child.blocked_by) || child.blocked_by.some((b) => typeof b !== "string"))
|
|
10240
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid blocked_by`);
|
|
10241
|
+
if (child.reason !== null && typeof child.reason !== "string" || child.commit !== null && typeof child.commit !== "string")
|
|
10242
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has invalid terminal fields`);
|
|
10243
|
+
if (child.state === "committed" && child.commit === null)
|
|
10244
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} is committed without a commit`);
|
|
10245
|
+
if (child.state !== "committed" && child.commit !== null)
|
|
10246
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} has a commit but is not committed`);
|
|
10247
|
+
if ((child.state === "needs_human" || child.state === "skipped_blocked") && child.reason === null)
|
|
10248
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} needs a terminal reason`);
|
|
10249
|
+
if (seenTaskIds.has(child.task_id))
|
|
10250
|
+
throw new Error(`batch run state ${batchId} has a duplicate child ${child.task_id}`);
|
|
10251
|
+
seenTaskIds.add(child.task_id);
|
|
10052
10252
|
}
|
|
10053
|
-
for (const child of children) {
|
|
10054
|
-
|
|
10055
|
-
|
|
10056
|
-
throw new Error(`batch plan child ${child.task_id} is blocked by ${blocker}, which is not in the confirmed plan`);
|
|
10057
|
-
}
|
|
10058
|
-
}
|
|
10059
|
-
const dependencies = new Map(children.map((child) => [child.task_id, child.blocked_by]));
|
|
10060
|
-
const visiting = new Set;
|
|
10061
|
-
const visited = new Set;
|
|
10062
|
-
function visit(taskId) {
|
|
10063
|
-
if (visiting.has(taskId))
|
|
10064
|
-
throw new Error(`batch plan dependency cycle at ${taskId}`);
|
|
10065
|
-
if (visited.has(taskId))
|
|
10066
|
-
return;
|
|
10067
|
-
visiting.add(taskId);
|
|
10068
|
-
for (const blocker of dependencies.get(taskId))
|
|
10069
|
-
visit(blocker);
|
|
10070
|
-
visiting.delete(taskId);
|
|
10071
|
-
visited.add(taskId);
|
|
10072
|
-
}
|
|
10073
|
-
for (const child of children)
|
|
10074
|
-
visit(child.task_id);
|
|
10075
|
-
if (computeBatchPlanDigest(children) !== planDigest)
|
|
10076
|
-
throw new Error("batch plan digest does not match the confirmed child plan");
|
|
10077
|
-
}
|
|
10078
|
-
function createBatchAuthorityRegistry() {
|
|
10079
|
-
const inner = createCapabilityRegistry(BATCH_AUTHORITY_CAPABILITY_BRAND, {
|
|
10080
|
-
validateBinding(binding, issuedAt) {
|
|
10081
|
-
requireNonEmpty(binding);
|
|
10082
|
-
if (!isLiteralUserActor(binding.actor_id))
|
|
10083
|
-
throw new Error("batch authorization requires a literal-user actor_id");
|
|
10084
|
-
if (!GIT_COMMIT_ID2.test(binding.base_head))
|
|
10085
|
-
throw new Error("batch authorization base_head must be a committed 40-hex commit id");
|
|
10086
|
-
const expires = Date.parse(binding.expires_at);
|
|
10087
|
-
if (Number.isNaN(expires) || expires <= Date.parse(issuedAt))
|
|
10088
|
-
throw new Error("batch authorization must have a future expiry");
|
|
10089
|
-
validateBudget(binding.budget, issuedAt);
|
|
10090
|
-
},
|
|
10091
|
-
validateAndProject(state, expected, now) {
|
|
10092
|
-
if (!Number.isFinite(now))
|
|
10093
|
-
throw new Error("batch authorization requires a valid clock");
|
|
10094
|
-
const expires = Date.parse(state.expires_at);
|
|
10095
|
-
if (Number.isNaN(expires) || expires <= now)
|
|
10096
|
-
throw new BatchAuthorizationExpiryError("batch authorization has expired");
|
|
10097
|
-
if (Date.parse(state.budget.deadline_at) <= now)
|
|
10098
|
-
throw new BatchAuthorizationExpiryError("batch authorization deadline has expired");
|
|
10099
|
-
for (const key of Object.keys(expected)) {
|
|
10100
|
-
if (key === "budget") {
|
|
10101
|
-
const a = state.budget ?? {};
|
|
10102
|
-
const b = expected.budget ?? {};
|
|
10103
|
-
if (a.max_children !== b.max_children || a.deadline_at !== b.deadline_at || a.qa_failure_limit !== b.qa_failure_limit)
|
|
10104
|
-
throw new Error("batch authorization budget mismatch");
|
|
10105
|
-
continue;
|
|
10106
|
-
}
|
|
10107
|
-
if (state[key] !== expected[key])
|
|
10108
|
-
throw new Error(`batch authorization ${key} mismatch`);
|
|
10109
|
-
}
|
|
10110
|
-
return {
|
|
10111
|
-
batch_id: state.batch_id,
|
|
10112
|
-
initiative_slug: state.initiative_slug,
|
|
10113
|
-
plan_digest: state.plan_digest,
|
|
10114
|
-
branch: state.branch,
|
|
10115
|
-
base_head: state.base_head,
|
|
10116
|
-
budget: { ...state.budget },
|
|
10117
|
-
actor_id: state.actor_id,
|
|
10118
|
-
confirmation_ref: state.confirmation_ref,
|
|
10119
|
-
issued_at: state.issued_at,
|
|
10120
|
-
expires_at: state.expires_at,
|
|
10121
|
-
nonce: state.nonce
|
|
10122
|
-
};
|
|
10123
|
-
}
|
|
10124
|
-
}, "batch authorization");
|
|
10125
|
-
const plans = new WeakMap;
|
|
10126
|
-
const consumed = new WeakMap;
|
|
10127
|
-
function planOf(capability) {
|
|
10128
|
-
const plan = plans.get(capability);
|
|
10129
|
-
if (!plan)
|
|
10130
|
-
throw new Error("batch authorization capability is not recognized by this registry");
|
|
10131
|
-
return plan;
|
|
10132
|
-
}
|
|
10133
|
-
function slotsOf(capability) {
|
|
10134
|
-
const slots = consumed.get(capability);
|
|
10135
|
-
if (!slots)
|
|
10136
|
-
throw new Error("batch authorization capability is not recognized by this registry");
|
|
10137
|
-
return slots;
|
|
10138
|
-
}
|
|
10139
|
-
return {
|
|
10140
|
-
brand: inner.brand,
|
|
10141
|
-
issue(binding, children, issuedAt = new Date().toISOString()) {
|
|
10142
|
-
requireNonEmpty(binding);
|
|
10143
|
-
validateChildren(children, binding.plan_digest);
|
|
10144
|
-
const capability = inner.issue(binding, issuedAt);
|
|
10145
|
-
plans.set(capability, children.map((child) => ({ ...child, blocked_by: [...child.blocked_by] })));
|
|
10146
|
-
consumed.set(capability, new Set);
|
|
10147
|
-
return capability;
|
|
10148
|
-
},
|
|
10149
|
-
inspect(capability, expected, now = Date.now()) {
|
|
10150
|
-
planOf(capability);
|
|
10151
|
-
return inner.inspect(capability, expected, now);
|
|
10152
|
-
},
|
|
10153
|
-
children(capability) {
|
|
10154
|
-
return planOf(capability).map((child) => ({ ...child, blocked_by: [...child.blocked_by] }));
|
|
10155
|
-
},
|
|
10156
|
-
consumedChildren(capability) {
|
|
10157
|
-
return [...slotsOf(capability)];
|
|
10158
|
-
},
|
|
10159
|
-
isChildConsumed(capability, taskId) {
|
|
10160
|
-
return slotsOf(capability).has(taskId);
|
|
10161
|
-
},
|
|
10162
|
-
consumeChild(capability, expected, taskId, now = Date.now()) {
|
|
10163
|
-
const validated = this.inspect(capability, expected, now);
|
|
10164
|
-
const plan = planOf(capability);
|
|
10165
|
-
if (!plan.some((child) => child.task_id === taskId))
|
|
10166
|
-
throw new Error(`batch_child_not_in_plan: ${taskId}`);
|
|
10167
|
-
const slots = slotsOf(capability);
|
|
10168
|
-
if (slots.has(taskId))
|
|
10169
|
-
throw new Error(`batch_child_slot_consumed: ${taskId}`);
|
|
10170
|
-
slots.add(taskId);
|
|
10171
|
-
return validated;
|
|
10172
|
-
},
|
|
10173
|
-
releaseChild(capability, taskId) {
|
|
10174
|
-
slotsOf(capability).delete(taskId);
|
|
10175
|
-
},
|
|
10176
|
-
isExhausted(capability) {
|
|
10177
|
-
return slotsOf(capability).size >= planOf(capability).length;
|
|
10178
|
-
}
|
|
10179
|
-
};
|
|
10180
|
-
}
|
|
10181
|
-
function deriveChildEnrollment(root, registry, input) {
|
|
10182
|
-
const validated = registry.inspect(input.capability, input.binding, Date.parse(input.now));
|
|
10183
|
-
const child = registry.children(input.capability).find((entry) => entry.task_id === input.task_id);
|
|
10184
|
-
if (!child)
|
|
10185
|
-
throw new Error(`batch_child_not_in_plan: ${input.task_id}`);
|
|
10186
|
-
if (registry.isChildConsumed(input.capability, input.task_id))
|
|
10187
|
-
throw new Error(`batch_child_slot_consumed: ${input.task_id}`);
|
|
10188
|
-
if (!GIT_COMMIT_ID2.test(input.expected_head))
|
|
10189
|
-
throw new Error("batch_head_lineage_broken: expected_head is not a commit id");
|
|
10190
|
-
const preparation = preparePiCanary(root, { task_id: input.task_id, now: input.now });
|
|
10191
|
-
if (!preparation.git_base_head)
|
|
10192
|
-
throw new Error(preparation.git_error ?? "enrollment requires a committed Git HEAD");
|
|
10193
|
-
if (preparation.git_base_head !== input.expected_head)
|
|
10194
|
-
throw new Error(`batch_head_lineage_broken: expected ${input.expected_head}, found ${preparation.git_base_head}`);
|
|
10195
|
-
if (!preparation.intent)
|
|
10196
|
-
throw new Error(`batch_child_intent_changed: ${input.task_id} intent sidecar is unreadable`);
|
|
10197
|
-
if (preparation.intent.path !== child.intent_path || preparation.intent.revision !== child.intent_revision || preparation.intent.content_hash !== child.intent_content_hash)
|
|
10198
|
-
throw new Error(`batch_child_intent_changed: ${input.task_id}`);
|
|
10199
|
-
if (registry.consumedChildren(input.capability).length === 0 && input.expected_head !== validated.base_head)
|
|
10200
|
-
throw new Error(`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${validated.base_head}, not ${input.expected_head}`);
|
|
10201
|
-
return {
|
|
10202
|
-
child,
|
|
10203
|
-
preparation,
|
|
10204
|
-
binding: {
|
|
10205
|
-
task_id: child.task_id,
|
|
10206
|
-
intent_path: child.intent_path,
|
|
10207
|
-
intent_revision: child.intent_revision,
|
|
10208
|
-
intent_content_hash: child.intent_content_hash,
|
|
10209
|
-
preparation_digest: preparation.digest,
|
|
10210
|
-
actor_id: validated.actor_id,
|
|
10211
|
-
confirmation_ref: validated.confirmation_ref,
|
|
10212
|
-
expires_at: validated.expires_at,
|
|
10213
|
-
nonce: `${validated.nonce}:${child.task_id}`
|
|
10214
|
-
}
|
|
10215
|
-
};
|
|
10216
|
-
}
|
|
10217
|
-
|
|
10218
|
-
// plugins/immune-brain/runtime/unattended/batch_plan.ts
|
|
10219
|
-
import { createHash as createHash19 } from "node:crypto";
|
|
10220
|
-
var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10221
|
-
var DEFAULT_DEADLINE_MS = 8 * 60 * 60 * 1000;
|
|
10222
|
-
var DEFAULT_QA_FAILURE_LIMIT = 2;
|
|
10223
|
-
function specBindingReason(inspection) {
|
|
10224
|
-
if (inspection.missing.length > 0)
|
|
10225
|
-
return `spec_binding_incomplete: ${inspection.missing.join(", ")}`;
|
|
10226
|
-
return inspection.code === "binding_ambiguous" ? "spec_binding_ambiguous" : "spec_binding_missing";
|
|
10227
|
-
}
|
|
10228
|
-
function compareIds(left, right) {
|
|
10229
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
10230
|
-
}
|
|
10231
|
-
function positiveSafeInteger(value, name) {
|
|
10232
|
-
if (!Number.isSafeInteger(value) || value <= 0)
|
|
10233
|
-
throw new Error(`${name} must be a positive safe integer`);
|
|
10234
|
-
return value;
|
|
10235
|
-
}
|
|
10236
|
-
var ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
10237
|
-
function timestamp(value, name) {
|
|
10238
|
-
if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN.test(value))
|
|
10239
|
-
throw new Error(`${name} must be an ISO timestamp`);
|
|
10240
|
-
const milliseconds = Date.parse(value);
|
|
10241
|
-
if (!Number.isFinite(milliseconds))
|
|
10242
|
-
throw new Error(`${name} must be an ISO timestamp`);
|
|
10243
|
-
return { milliseconds, iso: new Date(milliseconds).toISOString() };
|
|
10244
|
-
}
|
|
10245
|
-
function normalizeObservation(value, initiativeSlug) {
|
|
10246
|
-
if (!value || value.contract !== "immune_brain/github_initiative_observation/v1")
|
|
10247
|
-
throw new Error("tracker returned an invalid Initiative observation contract");
|
|
10248
|
-
if (value.initiative_id !== initiativeSlug)
|
|
10249
|
-
throw new Error("tracker returned an observation for another Initiative");
|
|
10250
|
-
if (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0)
|
|
10251
|
-
throw new Error("tracker returned an invalid Initiative Issue number");
|
|
10252
|
-
if (!Array.isArray(value.tasks))
|
|
10253
|
-
throw new Error("tracker returned an invalid Initiative Task list");
|
|
10254
|
-
const taskIds = new Set;
|
|
10255
|
-
const sliceIds = new Set;
|
|
10256
|
-
const tasks = value.tasks.map((task, index) => {
|
|
10257
|
-
if (!task || typeof task !== "object")
|
|
10258
|
-
throw new Error(`tracker Task ${index} is invalid`);
|
|
10259
|
-
if (!ID_PATTERN2.test(task.task_id))
|
|
10260
|
-
throw new Error(`tracker Task ${index} has an invalid task_id`);
|
|
10261
|
-
if (!ID_PATTERN2.test(task.slice_id))
|
|
10262
|
-
throw new Error(`tracker Task ${index} has an invalid slice_id`);
|
|
10263
|
-
if (!Number.isSafeInteger(task.issue_number) || task.issue_number <= 0)
|
|
10264
|
-
throw new Error(`tracker Task ${task.task_id} has an invalid Issue number`);
|
|
10265
|
-
if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN2.test(id)))
|
|
10266
|
-
throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
|
|
10267
|
-
if (taskIds.has(task.task_id))
|
|
10268
|
-
throw new Error(`tracker returned duplicate Task ${task.task_id}`);
|
|
10269
|
-
if (sliceIds.has(task.slice_id))
|
|
10270
|
-
throw new Error(`tracker returned duplicate Slice ${task.slice_id}`);
|
|
10271
|
-
taskIds.add(task.task_id);
|
|
10272
|
-
sliceIds.add(task.slice_id);
|
|
10273
|
-
const blockedBy = [...task.blocked_by].sort();
|
|
10274
|
-
if (new Set(blockedBy).size !== blockedBy.length || blockedBy.includes(task.task_id))
|
|
10275
|
-
throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
|
|
10276
|
-
return { ...task, blocked_by: blockedBy };
|
|
10277
|
-
});
|
|
10278
|
-
for (const task of tasks) {
|
|
10279
|
-
const unknown = task.blocked_by.find((id) => !taskIds.has(id));
|
|
10280
|
-
if (unknown)
|
|
10281
|
-
throw new Error(`tracker Task ${task.task_id} depends on unknown Task ${unknown}`);
|
|
10282
|
-
}
|
|
10283
|
-
return { ...value, tasks: tasks.sort((left, right) => compareIds(left.task_id, right.task_id)) };
|
|
10284
|
-
}
|
|
10285
|
-
function dependencyOrder(observation) {
|
|
10286
|
-
const remaining = new Map(observation.tasks.map((task) => [task.task_id, task]));
|
|
10287
|
-
const done = new Set;
|
|
10288
|
-
const order = [];
|
|
10289
|
-
while (remaining.size) {
|
|
10290
|
-
const ready = [...remaining.values()].filter((task) => task.blocked_by.every((id) => done.has(id))).sort((left, right) => compareIds(left.task_id, right.task_id));
|
|
10291
|
-
if (!ready.length)
|
|
10292
|
-
throw new Error("Initiative Task dependencies must form an acyclic graph");
|
|
10293
|
-
for (const task of ready) {
|
|
10294
|
-
remaining.delete(task.task_id);
|
|
10295
|
-
done.add(task.task_id);
|
|
10296
|
-
order.push(task);
|
|
10297
|
-
}
|
|
10298
|
-
}
|
|
10299
|
-
return order;
|
|
10300
|
-
}
|
|
10301
|
-
function dependencyClosures(order) {
|
|
10302
|
-
const closures = new Map;
|
|
10303
|
-
for (const task of order) {
|
|
10304
|
-
const closure = new Set;
|
|
10305
|
-
for (const dependency of task.blocked_by) {
|
|
10306
|
-
closure.add(dependency);
|
|
10307
|
-
for (const transitive of closures.get(dependency) ?? [])
|
|
10308
|
-
closure.add(transitive);
|
|
10309
|
-
}
|
|
10310
|
-
closures.set(task.task_id, [...closure].sort());
|
|
10311
|
-
}
|
|
10312
|
-
return closures;
|
|
10313
|
-
}
|
|
10314
|
-
function budget(input, enrollableCount, confirmationTime) {
|
|
10315
|
-
const maxChildren = input.budget?.max_children === undefined ? positiveSafeInteger(enrollableCount, "budget.max_children") : positiveSafeInteger(input.budget.max_children, "budget.max_children");
|
|
10316
|
-
if (maxChildren > enrollableCount)
|
|
10317
|
-
throw new Error("budget.max_children exceeds the enrollable Child count");
|
|
10318
|
-
const qaFailureLimit = input.budget?.qa_failure_limit === undefined ? DEFAULT_QA_FAILURE_LIMIT : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
|
|
10319
|
-
const deadline = input.budget?.deadline_at === undefined ? new Date(confirmationTime.milliseconds + DEFAULT_DEADLINE_MS).toISOString() : timestamp(input.budget.deadline_at, "budget.deadline_at").iso;
|
|
10320
|
-
if (Date.parse(deadline) <= confirmationTime.milliseconds)
|
|
10321
|
-
throw new Error("budget.deadline_at must be later than confirmation_time");
|
|
10322
|
-
return { max_children: maxChildren, deadline_at: deadline, qa_failure_limit: qaFailureLimit };
|
|
10323
|
-
}
|
|
10324
|
-
async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeGithubInitiative) {
|
|
10325
|
-
if (!ID_PATTERN2.test(initiativeSlug))
|
|
10326
|
-
throw new Error("initiative_slug is invalid");
|
|
10327
|
-
if (!input || typeof input !== "object")
|
|
10328
|
-
throw new Error("batch plan input is required");
|
|
10329
|
-
const confirmationTime = timestamp(input.confirmation_time, "confirmation_time");
|
|
10330
|
-
const trackerObservation = normalizeObservation(await readInitiative(root, initiativeSlug), initiativeSlug);
|
|
10331
|
-
const order = dependencyOrder(trackerObservation);
|
|
10332
|
-
const closures = dependencyClosures(order);
|
|
10333
|
-
const children = [];
|
|
10334
|
-
for (const task of order) {
|
|
10335
|
-
const blockedBy = closures.get(task.task_id);
|
|
10336
|
-
const child = { task_id: task.task_id, slice_id: task.slice_id, blocked_by: blockedBy };
|
|
10337
|
-
const tombstone = readTaskTombstone(root, task.task_id);
|
|
10338
|
-
const record = readTaskRecordRaw(root, task.task_id).record;
|
|
10339
|
-
if (tombstone && record)
|
|
10340
|
-
throw new Error(`Task ${task.task_id} has conflicting settled and active state`);
|
|
10341
|
-
if (tombstone) {
|
|
10342
|
-
children.push({ ...child, status: "already_settled", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
|
|
10343
|
-
continue;
|
|
10344
|
-
}
|
|
10345
|
-
if (record) {
|
|
10346
|
-
children.push({ ...child, status: "already_owned", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
|
|
10347
|
-
continue;
|
|
10348
|
-
}
|
|
10349
|
-
const intentPath = `docs/plans/${task.task_id}.intent.json`;
|
|
10350
|
-
try {
|
|
10351
|
-
const read = observeTaskIntent(root, task.task_id, intentPath);
|
|
10352
|
-
if (read.intent.risk === "critical") {
|
|
10353
|
-
children.push({
|
|
10354
|
-
...child,
|
|
10355
|
-
status: "needs_human",
|
|
10356
|
-
reason: "critical",
|
|
10357
|
-
intent_path: read.intent_ref.path,
|
|
10358
|
-
intent_revision: read.intent_ref.revision,
|
|
10359
|
-
intent_content_hash: read.content_hash
|
|
10360
|
-
});
|
|
10361
|
-
continue;
|
|
10362
|
-
}
|
|
10363
|
-
const binding = inspectSpecBinding(read.intent);
|
|
10364
|
-
if (!binding.ok) {
|
|
10365
|
-
children.push({
|
|
10366
|
-
...child,
|
|
10367
|
-
status: "needs_human",
|
|
10368
|
-
reason: specBindingReason(binding),
|
|
10369
|
-
intent_path: read.intent_ref.path,
|
|
10370
|
-
intent_revision: read.intent_ref.revision,
|
|
10371
|
-
intent_content_hash: read.content_hash
|
|
10372
|
-
});
|
|
10373
|
-
continue;
|
|
10374
|
-
}
|
|
10375
|
-
children.push({
|
|
10376
|
-
...child,
|
|
10377
|
-
status: "enrollable",
|
|
10378
|
-
reason: null,
|
|
10379
|
-
intent_path: read.intent_ref.path,
|
|
10380
|
-
intent_revision: read.intent_ref.revision,
|
|
10381
|
-
intent_content_hash: read.content_hash
|
|
10382
|
-
});
|
|
10383
|
-
} catch (error) {
|
|
10384
|
-
if (!(error instanceof TaskIntentObservationError))
|
|
10385
|
-
throw error;
|
|
10386
|
-
children.push({ ...child, status: "needs_human", reason: "invalid_intent", intent_path: null, intent_revision: null, intent_content_hash: null });
|
|
10387
|
-
}
|
|
10388
|
-
}
|
|
10389
|
-
const directDependencies = new Map(order.map((task) => [task.task_id, task.blocked_by]));
|
|
10390
|
-
const childById = new Map(children.map((child) => [child.task_id, child]));
|
|
10391
|
-
for (const child of children) {
|
|
10392
|
-
if (child.status !== "enrollable")
|
|
10393
|
-
continue;
|
|
10394
|
-
if (directDependencies.get(child.task_id).some((id) => {
|
|
10395
|
-
const status = childById.get(id).status;
|
|
10396
|
-
return status === "already_owned" || status === "needs_human" || status === "blocked";
|
|
10397
|
-
})) {
|
|
10398
|
-
child.status = "blocked";
|
|
10399
|
-
child.reason = "dependency_unavailable";
|
|
10400
|
-
}
|
|
10401
|
-
}
|
|
10402
|
-
const enrollable = children.filter((child) => child.status === "enrollable").map((child) => ({
|
|
10403
|
-
task_id: child.task_id,
|
|
10404
|
-
intent_path: child.intent_path,
|
|
10405
|
-
intent_revision: child.intent_revision,
|
|
10406
|
-
intent_content_hash: child.intent_content_hash,
|
|
10407
|
-
blocked_by: child.blocked_by.filter((id) => childById.get(id)?.status === "enrollable")
|
|
10408
|
-
}));
|
|
10409
|
-
if (!enrollable.length)
|
|
10410
|
-
throw new Error("batch plan has no enrollable children; nothing to confirm");
|
|
10411
|
-
const planDigest = `sha256:${createHash19("sha256").update(stableStringify(enrollable)).digest("hex")}`;
|
|
10412
|
-
return {
|
|
10413
|
-
contract: "assurance_kernel/batch_plan/v1",
|
|
10414
|
-
initiative_slug: initiativeSlug,
|
|
10415
|
-
confirmation_time: confirmationTime.iso,
|
|
10416
|
-
tracker_observation: trackerObservation,
|
|
10417
|
-
children,
|
|
10418
|
-
enrollable,
|
|
10419
|
-
plan_digest: planDigest,
|
|
10420
|
-
budget: budget(input, enrollable.length, confirmationTime)
|
|
10421
|
-
};
|
|
10422
|
-
}
|
|
10423
|
-
|
|
10424
|
-
// plugins/immune-brain/runtime/unattended/batch_state.ts
|
|
10425
|
-
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";
|
|
10426
|
-
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
10427
|
-
import { dirname as dirname7, join as join11 } from "node:path";
|
|
10428
|
-
var BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10429
|
-
var CHILD_RUN_STATES = new Set([
|
|
10430
|
-
"pending",
|
|
10431
|
-
"enrolled",
|
|
10432
|
-
"settled",
|
|
10433
|
-
"committed",
|
|
10434
|
-
"needs_human",
|
|
10435
|
-
"skipped_blocked"
|
|
10436
|
-
]);
|
|
10437
|
-
var BATCH_RUN_STATES = new Set([
|
|
10438
|
-
"prepared",
|
|
10439
|
-
"running",
|
|
10440
|
-
"needs_human",
|
|
10441
|
-
"completed",
|
|
10442
|
-
"budget_stopped",
|
|
10443
|
-
"failed",
|
|
10444
|
-
"rejected"
|
|
10445
|
-
]);
|
|
10446
|
-
var ISO_TIMESTAMP_PATTERN2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
10447
|
-
function isCanonicalTimestamp(value) {
|
|
10448
|
-
if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN2.test(value))
|
|
10449
|
-
return false;
|
|
10450
|
-
const milliseconds = Date.parse(value);
|
|
10451
|
-
return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === value;
|
|
10452
|
-
}
|
|
10453
|
-
function validateBatchId(batchId) {
|
|
10454
|
-
if (typeof batchId !== "string" || !BATCH_ID_PATTERN.test(batchId))
|
|
10455
|
-
throw new Error("batch id is not a safe file identity");
|
|
10456
|
-
}
|
|
10457
|
-
function statePath(batchId) {
|
|
10458
|
-
validateBatchId(batchId);
|
|
10459
|
-
return join11(".imm", "state", "batches", `${batchId}.json`);
|
|
10460
|
-
}
|
|
10461
|
-
function canonicalBytes(record) {
|
|
10462
|
-
return `${JSON.stringify(record, null, 2)}
|
|
10463
|
-
`;
|
|
10464
|
-
}
|
|
10465
|
-
function validateRecordShape(value, batchId) {
|
|
10466
|
-
if (typeof value !== "object" || value === null)
|
|
10467
|
-
throw new Error(`batch run state ${batchId} is not an object`);
|
|
10468
|
-
const record = value;
|
|
10469
|
-
if (record.contract !== "assurance_kernel/batch_run_state/v1")
|
|
10470
|
-
throw new Error(`batch run state ${batchId} has an unknown contract`);
|
|
10471
|
-
if (record.batch_id !== batchId)
|
|
10472
|
-
throw new Error(`batch run state ${batchId} carries batch_id ${String(record.batch_id)}`);
|
|
10473
|
-
if (typeof record.plan_digest !== "string" || !record.plan_digest)
|
|
10474
|
-
throw new Error(`batch run state ${batchId} has an invalid plan_digest`);
|
|
10475
|
-
if (typeof record.base_head !== "string" || !record.base_head)
|
|
10476
|
-
throw new Error(`batch run state ${batchId} has an invalid base_head`);
|
|
10477
|
-
if (record.branch !== undefined && (typeof record.branch !== "string" || !record.branch))
|
|
10478
|
-
throw new Error(`batch run state ${batchId} has an invalid branch`);
|
|
10479
|
-
if (!isCanonicalTimestamp(record.confirmation_time))
|
|
10480
|
-
throw new Error(`batch run state ${batchId} has an invalid confirmation_time`);
|
|
10481
|
-
if (!isCanonicalTimestamp(record.authorization_expires_at))
|
|
10482
|
-
throw new Error(`batch run state ${batchId} has an invalid authorization_expires_at`);
|
|
10483
|
-
if (!isCanonicalTimestamp(record.created_at) || !isCanonicalTimestamp(record.updated_at))
|
|
10484
|
-
throw new Error(`batch run state ${batchId} has invalid state timestamps`);
|
|
10485
|
-
if (!Array.isArray(record.children) || record.children.length === 0)
|
|
10486
|
-
throw new Error(`batch run state ${batchId} has no children`);
|
|
10487
|
-
if (!BATCH_RUN_STATES.has(String(record.batch_state)))
|
|
10488
|
-
throw new Error(`batch run state ${batchId} has an invalid batch_state`);
|
|
10489
|
-
if (typeof record.consecutive_qa_failures !== "number" || !Number.isInteger(record.consecutive_qa_failures) || record.consecutive_qa_failures < 0)
|
|
10490
|
-
throw new Error(`batch run state ${batchId} has an invalid consecutive_qa_failures`);
|
|
10491
|
-
const budget = record.budget;
|
|
10492
|
-
if (typeof record.budget !== "object" || record.budget === null || typeof budget.max_children !== "number" || !Number.isInteger(budget.max_children) || budget.max_children <= 0 || !isCanonicalTimestamp(budget.deadline_at) || typeof budget.qa_failure_limit !== "number" || !Number.isInteger(budget.qa_failure_limit) || budget.qa_failure_limit <= 0)
|
|
10493
|
-
throw new Error(`batch run state ${batchId} has an invalid budget`);
|
|
10494
|
-
if (!Array.isArray(record.commits) || record.commits.some((c) => typeof c !== "string"))
|
|
10495
|
-
throw new Error(`batch run state ${batchId} has an invalid commits list`);
|
|
10496
|
-
const seenTaskIds = new Set;
|
|
10497
|
-
for (const child of record.children) {
|
|
10498
|
-
if (typeof child !== "object" || child === null || typeof child.task_id !== "string" || !child.task_id || typeof child.slice_id !== "string" || !child.slice_id)
|
|
10499
|
-
throw new Error(`batch run state ${batchId} has an invalid child entry`);
|
|
10500
|
-
if (!CHILD_RUN_STATES.has(String(child.state)))
|
|
10501
|
-
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid state`);
|
|
10502
|
-
if (!Array.isArray(child.blocked_by) || child.blocked_by.some((b) => typeof b !== "string"))
|
|
10503
|
-
throw new Error(`batch run state ${batchId} child ${child.task_id} has an invalid blocked_by`);
|
|
10504
|
-
if (child.reason !== null && typeof child.reason !== "string" || child.commit !== null && typeof child.commit !== "string")
|
|
10505
|
-
throw new Error(`batch run state ${batchId} child ${child.task_id} has invalid terminal fields`);
|
|
10506
|
-
if (child.state === "committed" && child.commit === null)
|
|
10507
|
-
throw new Error(`batch run state ${batchId} child ${child.task_id} is committed without a commit`);
|
|
10508
|
-
if (child.state !== "committed" && child.commit !== null)
|
|
10509
|
-
throw new Error(`batch run state ${batchId} child ${child.task_id} has a commit but is not committed`);
|
|
10510
|
-
if ((child.state === "needs_human" || child.state === "skipped_blocked") && child.reason === null)
|
|
10511
|
-
throw new Error(`batch run state ${batchId} child ${child.task_id} needs a terminal reason`);
|
|
10512
|
-
if (seenTaskIds.has(child.task_id))
|
|
10513
|
-
throw new Error(`batch run state ${batchId} has a duplicate child ${child.task_id}`);
|
|
10514
|
-
seenTaskIds.add(child.task_id);
|
|
10515
|
-
}
|
|
10516
|
-
for (const child of record.children) {
|
|
10517
|
-
if (child.state === "committed" && child.commit !== null && !record.commits.includes(child.commit))
|
|
10518
|
-
throw new Error(`batch run state ${batchId} child ${child.task_id} commit is missing from commits`);
|
|
10253
|
+
for (const child of record.children) {
|
|
10254
|
+
if (child.state === "committed" && child.commit !== null && !record.commits.includes(child.commit))
|
|
10255
|
+
throw new Error(`batch run state ${batchId} child ${child.task_id} commit is missing from commits`);
|
|
10519
10256
|
}
|
|
10520
10257
|
if ((record.batch_state === "budget_stopped" || record.batch_state === "failed" || record.batch_state === "rejected") && record.children.some((child) => child.state === "enrolled" || child.state === "settled"))
|
|
10521
10258
|
throw new Error(`batch run state ${batchId} is ${String(record.batch_state)} but a child is still mid-flight`);
|
|
@@ -10532,7 +10269,6 @@ function prepareBatchRunState(input) {
|
|
|
10532
10269
|
base_head: input.base_head,
|
|
10533
10270
|
branch: input.branch ?? `imm/${input.initiative_slug}`,
|
|
10534
10271
|
confirmation_time: input.confirmation_time,
|
|
10535
|
-
authorization_expires_at: input.authorization_expires_at,
|
|
10536
10272
|
budget: input.budget,
|
|
10537
10273
|
batch_state: "prepared",
|
|
10538
10274
|
children: input.children.map((child) => ({
|
|
@@ -10550,13 +10286,17 @@ function prepareBatchRunState(input) {
|
|
|
10550
10286
|
};
|
|
10551
10287
|
return prepared;
|
|
10552
10288
|
}
|
|
10289
|
+
function parseBatchRunState(raw, batchId) {
|
|
10290
|
+
validateBatchId(batchId);
|
|
10291
|
+
const parsed = JSON.parse(raw);
|
|
10292
|
+
validateRecordShape(parsed, batchId);
|
|
10293
|
+
return withoutRetiredClock(parsed);
|
|
10294
|
+
}
|
|
10553
10295
|
function readBatchRunState(root, batchId) {
|
|
10554
10296
|
const path = statePath(batchId);
|
|
10555
10297
|
if (!existsSync6(join11(root, path)))
|
|
10556
10298
|
return null;
|
|
10557
|
-
|
|
10558
|
-
validateRecordShape(parsed, batchId);
|
|
10559
|
-
return parsed;
|
|
10299
|
+
return parseBatchRunState(readSecureProjectFile(root, path), batchId);
|
|
10560
10300
|
}
|
|
10561
10301
|
function ensureSecureDirectory2(root, relative) {
|
|
10562
10302
|
const target = join11(root, relative);
|
|
@@ -10596,51 +10336,478 @@ function writeFileAtomically(root, relative, bytes) {
|
|
|
10596
10336
|
}
|
|
10597
10337
|
}
|
|
10598
10338
|
}
|
|
10599
|
-
function
|
|
10600
|
-
const path = statePath(
|
|
10601
|
-
validateRecordShape(
|
|
10602
|
-
return withKernelStoreLock(root, () => {
|
|
10603
|
-
|
|
10604
|
-
|
|
10605
|
-
|
|
10606
|
-
const stored = {
|
|
10607
|
-
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
|
|
10612
|
-
|
|
10613
|
-
|
|
10614
|
-
|
|
10615
|
-
|
|
10616
|
-
|
|
10617
|
-
|
|
10618
|
-
|
|
10619
|
-
|
|
10620
|
-
|
|
10621
|
-
|
|
10622
|
-
|
|
10623
|
-
|
|
10624
|
-
|
|
10625
|
-
|
|
10626
|
-
|
|
10627
|
-
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
|
|
10631
|
-
|
|
10339
|
+
function replaceBatchRunState(root, expected, next, validate) {
|
|
10340
|
+
const path = statePath(next.batch_id);
|
|
10341
|
+
validateRecordShape(next, next.batch_id);
|
|
10342
|
+
return withKernelStoreLock(root, () => {
|
|
10343
|
+
if (!readSecureProjectBytes(root, path).equals(expected))
|
|
10344
|
+
throw new Error("batch state CAS mismatch");
|
|
10345
|
+
validate();
|
|
10346
|
+
const stored = withoutRetiredClock({ ...next, updated_at: new Date().toISOString() });
|
|
10347
|
+
writeFileAtomically(root, path, canonicalBytes(stored));
|
|
10348
|
+
return stored;
|
|
10349
|
+
});
|
|
10350
|
+
}
|
|
10351
|
+
function writeBatchRunState(root, record) {
|
|
10352
|
+
const path = statePath(record.batch_id);
|
|
10353
|
+
validateRecordShape(record, record.batch_id);
|
|
10354
|
+
return withKernelStoreLock(root, () => {
|
|
10355
|
+
const existing = existsSync6(join11(root, path)) ? readSecureProjectFile(root, path) : null;
|
|
10356
|
+
if (existing !== null && existing === canonicalBytes(record))
|
|
10357
|
+
return record;
|
|
10358
|
+
const stored = withoutRetiredClock({
|
|
10359
|
+
...record,
|
|
10360
|
+
updated_at: new Date().toISOString()
|
|
10361
|
+
});
|
|
10362
|
+
ensureSecureDirectory2(root, join11(".imm", "state", "batches"));
|
|
10363
|
+
writeFileAtomically(root, path, canonicalBytes(stored));
|
|
10364
|
+
return stored;
|
|
10365
|
+
});
|
|
10366
|
+
}
|
|
10367
|
+
function reportPath(batchId) {
|
|
10368
|
+
validateBatchId(batchId);
|
|
10369
|
+
return join11(".imm", "state", "batches", `${batchId}.report.json`);
|
|
10370
|
+
}
|
|
10371
|
+
function writeBatchRunReport(root, report) {
|
|
10372
|
+
const relative = reportPath(report.batch_id);
|
|
10373
|
+
return withKernelStoreLock(root, () => {
|
|
10374
|
+
const path = join11(root, relative);
|
|
10375
|
+
if (existsSync6(path)) {
|
|
10376
|
+
const original = JSON.parse(readSecureProjectFile(root, relative));
|
|
10377
|
+
if (typeof original !== "object" || original === null || original.contract !== "assurance_kernel/batch_run_report/v1")
|
|
10378
|
+
throw new Error(`batch run report ${report.batch_id} has an unknown contract`);
|
|
10379
|
+
const prior = original;
|
|
10380
|
+
if (canonicalReportBytes(prior) === canonicalReportBytes(report))
|
|
10381
|
+
return prior;
|
|
10382
|
+
if (prior.batch_state !== "needs_human")
|
|
10383
|
+
return prior;
|
|
10384
|
+
}
|
|
10385
|
+
ensureSecureDirectory2(root, join11(".imm", "state", "batches"));
|
|
10386
|
+
writeFileAtomically(root, relative, canonicalReportBytes(report));
|
|
10387
|
+
return report;
|
|
10388
|
+
});
|
|
10389
|
+
}
|
|
10390
|
+
function canonicalReportBytes(report) {
|
|
10391
|
+
return `${JSON.stringify(report, null, 2)}
|
|
10392
|
+
`;
|
|
10393
|
+
}
|
|
10394
|
+
function isTerminalBatchState(state) {
|
|
10395
|
+
return state === "completed" || state === "budget_stopped" || state === "failed" || state === "rejected";
|
|
10396
|
+
}
|
|
10397
|
+
|
|
10398
|
+
// plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts
|
|
10399
|
+
var oid = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
10400
|
+
var id = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10401
|
+
var batchAuthor = process.env.GIT_AUTHOR_NAME || "Immune-Brain Batch";
|
|
10402
|
+
var decode = (bytes) => new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
10403
|
+
function refuse() {
|
|
10404
|
+
throw new Error("plan_digest mismatch: batch plan reconfirmation is not eligible");
|
|
10405
|
+
}
|
|
10406
|
+
function matchesLocalProof(localJson, exported) {
|
|
10407
|
+
return localJson !== null && JSON.stringify(Object.entries(JSON.parse(localJson)).sort()) === JSON.stringify(Object.entries(exported).sort());
|
|
10408
|
+
}
|
|
10409
|
+
function git4(root, args) {
|
|
10410
|
+
const r = spawnSync6("git", ["-C", root, ...args], { encoding: "buffer", maxBuffer: 262144, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
|
|
10411
|
+
if (r.status !== 0)
|
|
10412
|
+
refuse();
|
|
10413
|
+
return decode(r.stdout);
|
|
10414
|
+
}
|
|
10415
|
+
function ownUnpersistedBatchHead(root, record, head) {
|
|
10416
|
+
try {
|
|
10417
|
+
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))
|
|
10418
|
+
return false;
|
|
10419
|
+
const task = record.children[0].task_id;
|
|
10420
|
+
if (!id.test(task))
|
|
10421
|
+
return false;
|
|
10422
|
+
const evidence = JSON.parse(decode(readSecureProjectBytes(root, `.imm/state/batches/commits/${record.batch_id}-${task}.json`)));
|
|
10423
|
+
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)
|
|
10424
|
+
return false;
|
|
10425
|
+
if (git4(root, ["symbolic-ref", "--short", "HEAD"]).trim() !== record.branch)
|
|
10426
|
+
return false;
|
|
10427
|
+
const parents = git4(root, ["rev-list", "--parents", "-n", "1", head]).trim().split(/\s+/).slice(1);
|
|
10428
|
+
if (parents.length !== 1 || parents[0] !== record.base_head)
|
|
10429
|
+
return false;
|
|
10430
|
+
const metadata = git4(root, ["show", "-s", "--format=%an%x00%s%x00%(trailers:key=Immune-Brain-Batch,valueonly)", head]).split("\x00");
|
|
10431
|
+
if (metadata[0] !== batchAuthor || !metadata[1]?.startsWith(`imm(${task}):`) || metadata[2]?.trim() !== record.batch_id)
|
|
10432
|
+
return false;
|
|
10433
|
+
const run = localRunId(root, task);
|
|
10434
|
+
if (!run || !id.test(run))
|
|
10435
|
+
return false;
|
|
10436
|
+
const pair = readAuditTaskPair(root, task, run);
|
|
10437
|
+
if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4" || pair.record.lifecycle !== "done" || pair.proof.terminal_lifecycle !== "done")
|
|
10438
|
+
return false;
|
|
10439
|
+
const local = withKernelRead(root, (db) => readRunRowByTask(db, task));
|
|
10440
|
+
if (!local || local.run_id !== run || local.state !== "done" || !matchesLocalProof(local.terminal_proof_json, pair.proof))
|
|
10441
|
+
return false;
|
|
10442
|
+
for (const file of ["task-record.json", "terminal-proof.json"]) {
|
|
10443
|
+
const path = `.imm/audit/${task}/${run}/${file}`;
|
|
10444
|
+
if (!readSecureProjectBytes(root, path).equals(Buffer.from(git4(root, ["show", `${head}:${path}`]))))
|
|
10445
|
+
return false;
|
|
10446
|
+
}
|
|
10447
|
+
const paths = git4(root, ["diff-tree", "--no-commit-id", "--name-only", "--no-renames", "-z", "-r", head]).split("\x00").filter(Boolean);
|
|
10448
|
+
if (!paths.length || paths.some((path) => !path.startsWith(`.imm/audit/${task}/${run}/`) && !pair.record.intent_snapshot.scope_hint.some((s) => pathMatchesScope(path, s))))
|
|
10449
|
+
return false;
|
|
10450
|
+
const identity = taskCommitRevisionIdentity(root, pair.record.intent_snapshot.scope_hint, pair.record.git_base_head, head);
|
|
10451
|
+
if (!projectTask(pair.record.intent_snapshot, pair.record, identity.diff_hash, pair.record.intent_ref.content_hash, identity.changed_paths).complete)
|
|
10452
|
+
return false;
|
|
10453
|
+
return git4(root, ["rev-parse", "HEAD"]).trim() === head;
|
|
10454
|
+
} catch {
|
|
10455
|
+
return false;
|
|
10456
|
+
}
|
|
10457
|
+
}
|
|
10458
|
+
var confirmations = new Map;
|
|
10459
|
+
function retainReconfirmation(nonce, snapshot) {
|
|
10460
|
+
if (confirmations.has(nonce))
|
|
10461
|
+
refuse();
|
|
10462
|
+
confirmations.set(nonce, snapshot);
|
|
10463
|
+
}
|
|
10464
|
+
function takeReconfirmation(nonce) {
|
|
10465
|
+
const found = confirmations.get(nonce);
|
|
10466
|
+
confirmations.delete(nonce);
|
|
10467
|
+
if (!found)
|
|
10468
|
+
refuse();
|
|
10469
|
+
return found;
|
|
10470
|
+
}
|
|
10471
|
+
async function captureBatchReconfirmation(root, record, children) {
|
|
10472
|
+
if (!id.test(record.batch_id) || !oid.test(record.base_head) || isTerminalBatchState(record.batch_state) || record.commits.length || record.children.some((c) => c.commit !== null || c.state === "committed") || record.children.length !== children.length || !children.length)
|
|
10473
|
+
refuse();
|
|
10474
|
+
const assertNoCommitEvidence = () => {
|
|
10475
|
+
for (const child of record.children) {
|
|
10476
|
+
if (!id.test(child.task_id))
|
|
10477
|
+
refuse();
|
|
10478
|
+
const path = `.imm/state/batches/commits/${record.batch_id}-${child.task_id}.json`;
|
|
10479
|
+
let missing = false;
|
|
10480
|
+
try {
|
|
10481
|
+
readSecureProjectBytes(root, path);
|
|
10482
|
+
} catch (error) {
|
|
10483
|
+
if (error instanceof Error && error.message === `source_missing: ${path}`)
|
|
10484
|
+
missing = true;
|
|
10485
|
+
else
|
|
10486
|
+
refuse();
|
|
10487
|
+
}
|
|
10488
|
+
if (!missing)
|
|
10489
|
+
refuse();
|
|
10490
|
+
}
|
|
10491
|
+
};
|
|
10492
|
+
assertNoCommitEvidence();
|
|
10493
|
+
const statePath = `.imm/state/batches/${record.batch_id}.json`;
|
|
10494
|
+
const stateBytes = readSecureProjectBytes(root, statePath);
|
|
10495
|
+
if (JSON.stringify(JSON.parse(decode(stateBytes))) !== JSON.stringify(record))
|
|
10496
|
+
refuse();
|
|
10497
|
+
const files = new Map;
|
|
10498
|
+
const capture = (path) => {
|
|
10499
|
+
const bytes = readSecureProjectBytes(root, path);
|
|
10500
|
+
files.set(path, bytes);
|
|
10501
|
+
return bytes;
|
|
10502
|
+
};
|
|
10503
|
+
const oldChildren = [];
|
|
10504
|
+
const changed = [];
|
|
10505
|
+
let scope = null;
|
|
10506
|
+
const workspace = readWorkspaceStateRaw(root);
|
|
10507
|
+
if (workspace.state.current_working !== null)
|
|
10508
|
+
refuse();
|
|
10509
|
+
for (const [index, child] of children.entries()) {
|
|
10510
|
+
const previous = record.children[index];
|
|
10511
|
+
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`)
|
|
10512
|
+
refuse();
|
|
10513
|
+
const old = parseTaskIntentV1(JSON.parse(git4(root, ["show", `${record.base_head}:${child.intent_path}`])));
|
|
10514
|
+
if (old.task_id !== child.task_id)
|
|
10515
|
+
refuse();
|
|
10516
|
+
const oldHash = canonicalIntentHash(old);
|
|
10517
|
+
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 });
|
|
10518
|
+
const currentBytes = capture(child.intent_path);
|
|
10519
|
+
const current = parseTaskIntentV1(JSON.parse(decode(currentBytes)));
|
|
10520
|
+
const currentHash = canonicalIntentHash(current);
|
|
10521
|
+
const tracked = readTaskIntent(root, child.task_id, child.intent_path);
|
|
10522
|
+
if (currentHash !== tracked.content_hash || currentHash !== child.intent_content_hash || current.revision !== child.intent_revision)
|
|
10523
|
+
refuse();
|
|
10524
|
+
if (currentHash === oldHash)
|
|
10525
|
+
continue;
|
|
10526
|
+
if (scope !== null || previous.state === "pending" || previous.state === "skipped_blocked" || current.revision <= old.revision || current.goal !== old.goal || current.owner !== old.owner)
|
|
10527
|
+
refuse();
|
|
10528
|
+
const run = localRunId(root, child.task_id);
|
|
10529
|
+
if (!run || !id.test(run))
|
|
10530
|
+
refuse();
|
|
10531
|
+
const local = withKernelRead(root, (db) => readRunRowByTask(db, child.task_id));
|
|
10532
|
+
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))
|
|
10533
|
+
refuse();
|
|
10534
|
+
const pair = readAuditTaskPair(root, child.task_id, run);
|
|
10535
|
+
if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4")
|
|
10536
|
+
refuse();
|
|
10537
|
+
const r = pair.record;
|
|
10538
|
+
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")
|
|
10539
|
+
refuse();
|
|
10540
|
+
const terminal = r.history.at(-1);
|
|
10541
|
+
if (terminal?.type !== "complete" || terminal.from_state !== "active:frozen" || terminal.to_state !== "done:frozen" || terminal.id !== pair.proof.terminal_event_id)
|
|
10542
|
+
refuse();
|
|
10543
|
+
const recordPath = `.imm/audit/${child.task_id}/${run}/task-record.json`;
|
|
10544
|
+
const proofPath = `.imm/audit/${child.task_id}/${run}/terminal-proof.json`;
|
|
10545
|
+
if (!capture(recordPath).equals(Buffer.from(local.record_json)))
|
|
10546
|
+
refuse();
|
|
10547
|
+
const capturedProof = JSON.parse(decode(capture(proofPath)));
|
|
10548
|
+
if (!matchesLocalProof(local.terminal_proof_json, capturedProof) || !matchesLocalProof(local.terminal_proof_json, pair.proof))
|
|
10549
|
+
refuse();
|
|
10550
|
+
const identity = taskRevisionIdentity(root, r.intent_snapshot.scope_hint, r.git_base_head, r.task_id);
|
|
10551
|
+
const projection = await projectAssurance(root, child.task_id, (cwd, task) => {
|
|
10552
|
+
if (task.contract !== "assurance_kernel/task_record/v4" || !task.git_base_head)
|
|
10553
|
+
refuse();
|
|
10554
|
+
return taskRevisionIdentity(cwd, task.intent_snapshot.scope_hint, task.git_base_head, task.task_id);
|
|
10555
|
+
});
|
|
10556
|
+
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"))
|
|
10557
|
+
refuse();
|
|
10558
|
+
scope = r.intent_snapshot.scope_hint;
|
|
10559
|
+
changed.push({ task: child.task_id, run, identity: JSON.stringify(identity), local: JSON.stringify(local) });
|
|
10560
|
+
}
|
|
10561
|
+
if (computeBatchPlanDigest(oldChildren) !== record.plan_digest || changed.length !== 1 || scope === null)
|
|
10562
|
+
refuse();
|
|
10563
|
+
const confirmedScope = scope;
|
|
10564
|
+
const owner = changed[0];
|
|
10565
|
+
const status = () => git4(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignore-submodules=none"]);
|
|
10566
|
+
const statusBytes = status();
|
|
10567
|
+
const entries = statusBytes.split("\x00").filter(Boolean);
|
|
10568
|
+
for (const entry of entries) {
|
|
10569
|
+
const code = entry.slice(0, 2), path = entry.slice(3);
|
|
10570
|
+
if (code[1] !== " " || !["A", "M", "D"].includes(code[0]) || !path.startsWith(`.imm/audit/${owner.task}/${owner.run}/`) && !confirmedScope.some((s) => pathMatchesScope(path, s)))
|
|
10571
|
+
refuse();
|
|
10572
|
+
}
|
|
10573
|
+
const flagsBytes = git4(root, ["ls-files", "-v"]);
|
|
10574
|
+
const flags = flagsBytes.split(`
|
|
10575
|
+
`).filter(Boolean);
|
|
10576
|
+
if (flags.some((line) => line[0] === "S" || line[0] !== line[0]?.toUpperCase()))
|
|
10577
|
+
refuse();
|
|
10578
|
+
const head = () => git4(root, ["rev-parse", "HEAD"]).trim();
|
|
10579
|
+
const branch = () => git4(root, ["symbolic-ref", "--short", "HEAD"]).trim();
|
|
10580
|
+
if (head() !== record.base_head || branch() !== record.branch)
|
|
10581
|
+
refuse();
|
|
10582
|
+
const assertDeliveryUnchanged = () => {
|
|
10583
|
+
for (const c of changed) {
|
|
10584
|
+
const pair = readAuditTaskPair(root, c.task, c.run);
|
|
10585
|
+
if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4" || !pair.record.git_base_head)
|
|
10586
|
+
refuse();
|
|
10587
|
+
const local = withKernelRead(root, (db) => readRunRowByTask(db, c.task));
|
|
10588
|
+
const diff = taskRevisionIdentity(root, pair.record.intent_snapshot.scope_hint, pair.record.git_base_head, c.task);
|
|
10589
|
+
if (localRunId(root, c.task) !== c.run || JSON.stringify(local) !== c.local || JSON.stringify(diff) !== c.identity)
|
|
10590
|
+
refuse();
|
|
10591
|
+
}
|
|
10592
|
+
};
|
|
10593
|
+
const assertUnchanged = () => {
|
|
10594
|
+
assertNoCommitEvidence();
|
|
10595
|
+
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)
|
|
10596
|
+
refuse();
|
|
10597
|
+
for (const [path, bytes] of files)
|
|
10598
|
+
if (!readSecureProjectBytes(root, path).equals(bytes))
|
|
10599
|
+
refuse();
|
|
10600
|
+
assertDeliveryUnchanged();
|
|
10601
|
+
};
|
|
10602
|
+
assertUnchanged();
|
|
10603
|
+
const plan_digest = computeBatchPlanDigest(oldChildren.map((old, index) => ({
|
|
10604
|
+
...old,
|
|
10605
|
+
intent_revision: children[index].intent_revision,
|
|
10606
|
+
intent_content_hash: children[index].intent_content_hash
|
|
10607
|
+
})));
|
|
10608
|
+
return { root, plan_digest, stateBytes, assertUnchanged };
|
|
10609
|
+
}
|
|
10610
|
+
|
|
10611
|
+
// plugins/immune-brain/runtime/unattended/batch_plan.ts
|
|
10612
|
+
import { createHash as createHash19 } from "node:crypto";
|
|
10613
|
+
var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10614
|
+
var DEFAULT_QA_FAILURE_LIMIT = 2;
|
|
10615
|
+
function specBindingReason(inspection) {
|
|
10616
|
+
if (inspection.missing.length > 0)
|
|
10617
|
+
return `spec_binding_incomplete: ${inspection.missing.join(", ")}`;
|
|
10618
|
+
return inspection.code === "binding_ambiguous" ? "spec_binding_ambiguous" : "spec_binding_missing";
|
|
10619
|
+
}
|
|
10620
|
+
function compareIds(left, right) {
|
|
10621
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
10622
|
+
}
|
|
10623
|
+
function positiveSafeInteger(value, name) {
|
|
10624
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
10625
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
10626
|
+
return value;
|
|
10627
|
+
}
|
|
10628
|
+
var ISO_TIMESTAMP_PATTERN2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
10629
|
+
function timestamp(value, name) {
|
|
10630
|
+
if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN2.test(value))
|
|
10631
|
+
throw new Error(`${name} must be an ISO timestamp`);
|
|
10632
|
+
const milliseconds = Date.parse(value);
|
|
10633
|
+
if (!Number.isFinite(milliseconds))
|
|
10634
|
+
throw new Error(`${name} must be an ISO timestamp`);
|
|
10635
|
+
return { milliseconds, iso: new Date(milliseconds).toISOString() };
|
|
10636
|
+
}
|
|
10637
|
+
function normalizeObservation(value, initiativeSlug) {
|
|
10638
|
+
if (!value || value.contract !== "immune_brain/github_initiative_observation/v1")
|
|
10639
|
+
throw new Error("tracker returned an invalid Initiative observation contract");
|
|
10640
|
+
if (value.initiative_id !== initiativeSlug)
|
|
10641
|
+
throw new Error("tracker returned an observation for another Initiative");
|
|
10642
|
+
if (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0)
|
|
10643
|
+
throw new Error("tracker returned an invalid Initiative Issue number");
|
|
10644
|
+
if (!Array.isArray(value.tasks))
|
|
10645
|
+
throw new Error("tracker returned an invalid Initiative Task list");
|
|
10646
|
+
const taskIds = new Set;
|
|
10647
|
+
const sliceIds = new Set;
|
|
10648
|
+
const tasks = value.tasks.map((task, index) => {
|
|
10649
|
+
if (!task || typeof task !== "object")
|
|
10650
|
+
throw new Error(`tracker Task ${index} is invalid`);
|
|
10651
|
+
if (!ID_PATTERN2.test(task.task_id))
|
|
10652
|
+
throw new Error(`tracker Task ${index} has an invalid task_id`);
|
|
10653
|
+
if (!ID_PATTERN2.test(task.slice_id))
|
|
10654
|
+
throw new Error(`tracker Task ${index} has an invalid slice_id`);
|
|
10655
|
+
if (!Number.isSafeInteger(task.issue_number) || task.issue_number <= 0)
|
|
10656
|
+
throw new Error(`tracker Task ${task.task_id} has an invalid Issue number`);
|
|
10657
|
+
if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN2.test(id)))
|
|
10658
|
+
throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
|
|
10659
|
+
if (taskIds.has(task.task_id))
|
|
10660
|
+
throw new Error(`tracker returned duplicate Task ${task.task_id}`);
|
|
10661
|
+
if (sliceIds.has(task.slice_id))
|
|
10662
|
+
throw new Error(`tracker returned duplicate Slice ${task.slice_id}`);
|
|
10663
|
+
taskIds.add(task.task_id);
|
|
10664
|
+
sliceIds.add(task.slice_id);
|
|
10665
|
+
const blockedBy = [...task.blocked_by].sort();
|
|
10666
|
+
if (new Set(blockedBy).size !== blockedBy.length || blockedBy.includes(task.task_id))
|
|
10667
|
+
throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
|
|
10668
|
+
return { ...task, blocked_by: blockedBy };
|
|
10669
|
+
});
|
|
10670
|
+
for (const task of tasks) {
|
|
10671
|
+
const unknown = task.blocked_by.find((id) => !taskIds.has(id));
|
|
10672
|
+
if (unknown)
|
|
10673
|
+
throw new Error(`tracker Task ${task.task_id} depends on unknown Task ${unknown}`);
|
|
10674
|
+
}
|
|
10675
|
+
return { ...value, tasks: tasks.sort((left, right) => compareIds(left.task_id, right.task_id)) };
|
|
10676
|
+
}
|
|
10677
|
+
function dependencyOrder(observation) {
|
|
10678
|
+
const remaining = new Map(observation.tasks.map((task) => [task.task_id, task]));
|
|
10679
|
+
const done = new Set;
|
|
10680
|
+
const order = [];
|
|
10681
|
+
while (remaining.size) {
|
|
10682
|
+
const ready = [...remaining.values()].filter((task) => task.blocked_by.every((id) => done.has(id))).sort((left, right) => compareIds(left.task_id, right.task_id));
|
|
10683
|
+
if (!ready.length)
|
|
10684
|
+
throw new Error("Initiative Task dependencies must form an acyclic graph");
|
|
10685
|
+
for (const task of ready) {
|
|
10686
|
+
remaining.delete(task.task_id);
|
|
10687
|
+
done.add(task.task_id);
|
|
10688
|
+
order.push(task);
|
|
10689
|
+
}
|
|
10690
|
+
}
|
|
10691
|
+
return order;
|
|
10692
|
+
}
|
|
10693
|
+
function dependencyClosures(order) {
|
|
10694
|
+
const closures = new Map;
|
|
10695
|
+
for (const task of order) {
|
|
10696
|
+
const closure = new Set;
|
|
10697
|
+
for (const dependency of task.blocked_by) {
|
|
10698
|
+
closure.add(dependency);
|
|
10699
|
+
for (const transitive of closures.get(dependency) ?? [])
|
|
10700
|
+
closure.add(transitive);
|
|
10632
10701
|
}
|
|
10633
|
-
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
});
|
|
10702
|
+
closures.set(task.task_id, [...closure].sort());
|
|
10703
|
+
}
|
|
10704
|
+
return closures;
|
|
10637
10705
|
}
|
|
10638
|
-
function
|
|
10639
|
-
|
|
10640
|
-
|
|
10706
|
+
function budget(input, enrollableCount) {
|
|
10707
|
+
const maxChildren = input.budget?.max_children === undefined ? positiveSafeInteger(enrollableCount, "budget.max_children") : positiveSafeInteger(input.budget.max_children, "budget.max_children");
|
|
10708
|
+
if (maxChildren > enrollableCount)
|
|
10709
|
+
throw new Error("budget.max_children exceeds the enrollable Child count");
|
|
10710
|
+
const qaFailureLimit = input.budget?.qa_failure_limit === undefined ? DEFAULT_QA_FAILURE_LIMIT : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
|
|
10711
|
+
return { max_children: maxChildren, qa_failure_limit: qaFailureLimit };
|
|
10641
10712
|
}
|
|
10642
|
-
function
|
|
10643
|
-
|
|
10713
|
+
async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeGithubInitiative) {
|
|
10714
|
+
if (!ID_PATTERN2.test(initiativeSlug))
|
|
10715
|
+
throw new Error("initiative_slug is invalid");
|
|
10716
|
+
if (!input || typeof input !== "object")
|
|
10717
|
+
throw new Error("batch plan input is required");
|
|
10718
|
+
const confirmationTime = timestamp(input.confirmation_time, "confirmation_time");
|
|
10719
|
+
const trackerObservation = normalizeObservation(await readInitiative(root, initiativeSlug), initiativeSlug);
|
|
10720
|
+
const order = dependencyOrder(trackerObservation);
|
|
10721
|
+
const closures = dependencyClosures(order);
|
|
10722
|
+
const children = [];
|
|
10723
|
+
for (const task of order) {
|
|
10724
|
+
const blockedBy = closures.get(task.task_id);
|
|
10725
|
+
const child = { task_id: task.task_id, slice_id: task.slice_id, blocked_by: blockedBy };
|
|
10726
|
+
const tombstone = readTaskTombstone(root, task.task_id);
|
|
10727
|
+
const record = readTaskRecordRaw(root, task.task_id).record;
|
|
10728
|
+
if (tombstone && record)
|
|
10729
|
+
throw new Error(`Task ${task.task_id} has conflicting settled and active state`);
|
|
10730
|
+
if (tombstone) {
|
|
10731
|
+
children.push({ ...child, status: "already_settled", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
|
|
10732
|
+
continue;
|
|
10733
|
+
}
|
|
10734
|
+
if (record) {
|
|
10735
|
+
children.push({ ...child, status: "already_owned", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
|
|
10736
|
+
continue;
|
|
10737
|
+
}
|
|
10738
|
+
const intentPath = `docs/plans/${task.task_id}.intent.json`;
|
|
10739
|
+
try {
|
|
10740
|
+
const read = observeTaskIntent(root, task.task_id, intentPath);
|
|
10741
|
+
if (read.intent.risk === "critical") {
|
|
10742
|
+
children.push({
|
|
10743
|
+
...child,
|
|
10744
|
+
status: "needs_human",
|
|
10745
|
+
reason: "critical",
|
|
10746
|
+
intent_path: read.intent_ref.path,
|
|
10747
|
+
intent_revision: read.intent_ref.revision,
|
|
10748
|
+
intent_content_hash: read.content_hash
|
|
10749
|
+
});
|
|
10750
|
+
continue;
|
|
10751
|
+
}
|
|
10752
|
+
const binding = inspectSpecBinding(read.intent);
|
|
10753
|
+
if (!binding.ok) {
|
|
10754
|
+
children.push({
|
|
10755
|
+
...child,
|
|
10756
|
+
status: "needs_human",
|
|
10757
|
+
reason: specBindingReason(binding),
|
|
10758
|
+
intent_path: read.intent_ref.path,
|
|
10759
|
+
intent_revision: read.intent_ref.revision,
|
|
10760
|
+
intent_content_hash: read.content_hash
|
|
10761
|
+
});
|
|
10762
|
+
continue;
|
|
10763
|
+
}
|
|
10764
|
+
children.push({
|
|
10765
|
+
...child,
|
|
10766
|
+
status: "enrollable",
|
|
10767
|
+
reason: null,
|
|
10768
|
+
intent_path: read.intent_ref.path,
|
|
10769
|
+
intent_revision: read.intent_ref.revision,
|
|
10770
|
+
intent_content_hash: read.content_hash
|
|
10771
|
+
});
|
|
10772
|
+
} catch (error) {
|
|
10773
|
+
if (!(error instanceof TaskIntentObservationError))
|
|
10774
|
+
throw error;
|
|
10775
|
+
children.push({ ...child, status: "needs_human", reason: "invalid_intent", intent_path: null, intent_revision: null, intent_content_hash: null });
|
|
10776
|
+
}
|
|
10777
|
+
}
|
|
10778
|
+
const directDependencies = new Map(order.map((task) => [task.task_id, task.blocked_by]));
|
|
10779
|
+
const childById = new Map(children.map((child) => [child.task_id, child]));
|
|
10780
|
+
for (const child of children) {
|
|
10781
|
+
if (child.status !== "enrollable")
|
|
10782
|
+
continue;
|
|
10783
|
+
if (directDependencies.get(child.task_id).some((id) => {
|
|
10784
|
+
const status = childById.get(id).status;
|
|
10785
|
+
return status === "already_owned" || status === "needs_human" || status === "blocked";
|
|
10786
|
+
})) {
|
|
10787
|
+
child.status = "blocked";
|
|
10788
|
+
child.reason = "dependency_unavailable";
|
|
10789
|
+
}
|
|
10790
|
+
}
|
|
10791
|
+
const enrollable = children.filter((child) => child.status === "enrollable").map((child) => ({
|
|
10792
|
+
task_id: child.task_id,
|
|
10793
|
+
intent_path: child.intent_path,
|
|
10794
|
+
intent_revision: child.intent_revision,
|
|
10795
|
+
intent_content_hash: child.intent_content_hash,
|
|
10796
|
+
blocked_by: child.blocked_by.filter((id) => childById.get(id)?.status === "enrollable")
|
|
10797
|
+
}));
|
|
10798
|
+
if (!enrollable.length)
|
|
10799
|
+
throw new Error("batch plan has no enrollable children; nothing to confirm");
|
|
10800
|
+
const planDigest = `sha256:${createHash19("sha256").update(stableStringify(enrollable)).digest("hex")}`;
|
|
10801
|
+
return {
|
|
10802
|
+
contract: "assurance_kernel/batch_plan/v1",
|
|
10803
|
+
initiative_slug: initiativeSlug,
|
|
10804
|
+
confirmation_time: confirmationTime.iso,
|
|
10805
|
+
tracker_observation: trackerObservation,
|
|
10806
|
+
children,
|
|
10807
|
+
enrollable,
|
|
10808
|
+
plan_digest: planDigest,
|
|
10809
|
+
budget: budget(input, enrollable.length)
|
|
10810
|
+
};
|
|
10644
10811
|
}
|
|
10645
10812
|
|
|
10646
10813
|
// plugins/immune-brain/runtime/unattended/batch_preflight.ts
|
|
@@ -10661,6 +10828,27 @@ function readActiveClaimTaskId(root) {
|
|
|
10661
10828
|
const claim = readBackendClaim(root);
|
|
10662
10829
|
return workspace.state.current_working || (claim?.lifecycle_status === "active" ? claim.task_id : null);
|
|
10663
10830
|
}
|
|
10831
|
+
function findResumableBatchSlugForTask(root, taskId) {
|
|
10832
|
+
const batchesDir = join12(root, ".imm", "state", "batches");
|
|
10833
|
+
if (!existsSync7(batchesDir))
|
|
10834
|
+
return null;
|
|
10835
|
+
for (const file of readdirSync6(batchesDir)) {
|
|
10836
|
+
if (!file.endsWith(".json"))
|
|
10837
|
+
continue;
|
|
10838
|
+
try {
|
|
10839
|
+
const record = JSON.parse(readFileSync13(join12(batchesDir, file), "utf8"));
|
|
10840
|
+
if (record?.contract !== "assurance_kernel/batch_run_state/v1")
|
|
10841
|
+
continue;
|
|
10842
|
+
if (record.batch_state !== "running" && record.batch_state !== "needs_human")
|
|
10843
|
+
continue;
|
|
10844
|
+
if (record.children.some((child) => child.task_id === taskId && (child.state === "enrolled" || child.state === "settled")))
|
|
10845
|
+
return record.initiative_slug;
|
|
10846
|
+
} catch {
|
|
10847
|
+
continue;
|
|
10848
|
+
}
|
|
10849
|
+
}
|
|
10850
|
+
return null;
|
|
10851
|
+
}
|
|
10664
10852
|
function findExistingActiveBatch(root, initiativeSlug) {
|
|
10665
10853
|
const batchesDir = join12(root, ".imm", "state", "batches");
|
|
10666
10854
|
if (!existsSync7(batchesDir))
|
|
@@ -10732,9 +10920,51 @@ function findSettledBatchRecord(root, initiativeSlug) {
|
|
|
10732
10920
|
}
|
|
10733
10921
|
return newest;
|
|
10734
10922
|
}
|
|
10923
|
+
function classifyBatchLineage(input) {
|
|
10924
|
+
const { root, branch, expectedHead, childCommits } = input;
|
|
10925
|
+
const run = (args) => spawnSync7("git", ["-C", root, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
10926
|
+
const broken = (reason) => ({
|
|
10927
|
+
kind: "broken",
|
|
10928
|
+
message: `batch_head_lineage_broken: ${reason}`
|
|
10929
|
+
});
|
|
10930
|
+
const branchResult = run(["symbolic-ref", "--short", "HEAD"]);
|
|
10931
|
+
const currentBranch = branchResult.stdout.trim();
|
|
10932
|
+
if (branchResult.status !== 0 || currentBranch !== branch)
|
|
10933
|
+
return broken(`current branch ${currentBranch} does not match expected branch ${branch}`);
|
|
10934
|
+
const headResult = run(["rev-parse", "HEAD"]);
|
|
10935
|
+
const head = headResult.stdout.trim();
|
|
10936
|
+
if (headResult.status !== 0 || !head)
|
|
10937
|
+
return broken("current HEAD is unreadable");
|
|
10938
|
+
if (head !== expectedHead && run(["merge-base", "--is-ancestor", expectedHead, head]).status !== 0)
|
|
10939
|
+
return broken(`current HEAD ${head} does not descend from expected batch head ${expectedHead}`);
|
|
10940
|
+
for (const commit of childCommits) {
|
|
10941
|
+
if (run(["merge-base", "--is-ancestor", commit, head]).status !== 0)
|
|
10942
|
+
return broken(`recorded commit ${commit} is no longer reachable from HEAD`);
|
|
10943
|
+
}
|
|
10944
|
+
if (head === expectedHead)
|
|
10945
|
+
return { kind: "equal", head };
|
|
10946
|
+
if (input.batchId) {
|
|
10947
|
+
const claimed = run([
|
|
10948
|
+
"log",
|
|
10949
|
+
"--fixed-strings",
|
|
10950
|
+
`--grep=Immune-Brain-Batch: ${input.batchId}`,
|
|
10951
|
+
"--format=%H",
|
|
10952
|
+
`${expectedHead}..${head}`
|
|
10953
|
+
]);
|
|
10954
|
+
const claimant = claimed.stdout.split(/\s+/).find(Boolean);
|
|
10955
|
+
if (claimed.status !== 0 || claimant)
|
|
10956
|
+
return broken(`commit ${claimant ?? head} claims batch ${input.batchId} without recorded evidence`);
|
|
10957
|
+
}
|
|
10958
|
+
return { kind: "fast_forward", head };
|
|
10959
|
+
}
|
|
10735
10960
|
function expectedBatchHead(record) {
|
|
10736
10961
|
const commits = Array.isArray(record.commits) ? record.commits : [];
|
|
10737
|
-
|
|
10962
|
+
let head = commits.length > 0 ? commits[commits.length - 1] : record.base_head;
|
|
10963
|
+
for (const adoption of record.adopted_heads ?? []) {
|
|
10964
|
+
if (adoption.from === head)
|
|
10965
|
+
head = adoption.to;
|
|
10966
|
+
}
|
|
10967
|
+
return head;
|
|
10738
10968
|
}
|
|
10739
10969
|
function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
|
|
10740
10970
|
let claim = null;
|
|
@@ -10748,7 +10978,7 @@ function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
|
|
|
10748
10978
|
const currentTaskId = workspaceOwner || (claim?.lifecycle_status === "active" ? claim?.task_id : null);
|
|
10749
10979
|
if (currentTaskId !== taskId || !claim)
|
|
10750
10980
|
return false;
|
|
10751
|
-
const branch =
|
|
10981
|
+
const branch = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" }).stdout.trim();
|
|
10752
10982
|
if (branch !== batchBranch)
|
|
10753
10983
|
return false;
|
|
10754
10984
|
const childInBatch = existingBatch.children.find((c) => c.task_id === taskId);
|
|
@@ -10764,7 +10994,7 @@ function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
|
|
|
10764
10994
|
}
|
|
10765
10995
|
if (!rec)
|
|
10766
10996
|
return false;
|
|
10767
|
-
const lineageHeads = [existingBatch.base_head].concat(Array.isArray(existingBatch.commits) ? existingBatch.commits : []);
|
|
10997
|
+
const lineageHeads = [existingBatch.base_head].concat(Array.isArray(existingBatch.commits) ? existingBatch.commits : []).concat((existingBatch.adopted_heads ?? []).map((adoption) => adoption.to));
|
|
10768
10998
|
if (!lineageHeads.includes(rec.git_base_head))
|
|
10769
10999
|
return false;
|
|
10770
11000
|
if (claim.enrollment_event_id !== `enroll-${taskId}-${claim.created_at}`)
|
|
@@ -10805,7 +11035,7 @@ function authorizedScopeOf(root, taskId, state) {
|
|
|
10805
11035
|
return scope;
|
|
10806
11036
|
}
|
|
10807
11037
|
function porcelainEntries(root) {
|
|
10808
|
-
const statusProc =
|
|
11038
|
+
const statusProc = spawnSync7("git", ["-C", root, "status", "--porcelain=v1", "-z", "--no-renames", "--untracked-files=all", "--ignore-submodules=none"], { encoding: "utf8", env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
|
|
10809
11039
|
if (statusProc.status !== 0)
|
|
10810
11040
|
return null;
|
|
10811
11041
|
const entries = [];
|
|
@@ -10824,9 +11054,10 @@ async function projectPlanSurface(input) {
|
|
|
10824
11054
|
const riskByTask = new Map;
|
|
10825
11055
|
let budget;
|
|
10826
11056
|
if (isResuming && existingBatch) {
|
|
10827
|
-
budget = {
|
|
10828
|
-
|
|
10829
|
-
|
|
11057
|
+
budget = {
|
|
11058
|
+
max_children: existingBatch.budget.max_children,
|
|
11059
|
+
qa_failure_limit: existingBatch.budget.qa_failure_limit
|
|
11060
|
+
};
|
|
10830
11061
|
try {
|
|
10831
11062
|
recoveryChildren = existingBatch.children.map((c) => {
|
|
10832
11063
|
const intentPath = `docs/plans/${c.task_id}.intent.json`;
|
|
@@ -10943,7 +11174,7 @@ async function projectBatchPreflight(options) {
|
|
|
10943
11174
|
} catch (err) {
|
|
10944
11175
|
return reject("git_head_unreadable", err instanceof Error ? err.message : String(err));
|
|
10945
11176
|
}
|
|
10946
|
-
const branchExists =
|
|
11177
|
+
const branchExists = spawnSync7("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${batchBranch}`]);
|
|
10947
11178
|
if (branchExists.status === 0 && !existingBatch)
|
|
10948
11179
|
return reject("branch_already_exists", batchBranch);
|
|
10949
11180
|
const statusEntries = porcelainEntries(root);
|
|
@@ -10992,21 +11223,29 @@ async function projectBatchPreflight(options) {
|
|
|
10992
11223
|
});
|
|
10993
11224
|
if (!planSurface.ok)
|
|
10994
11225
|
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
|
|
11226
|
+
let reconfirmation;
|
|
11227
|
+
if (activeRecord && activeRecord.plan_digest !== planSurface.surface.plan_digest) {
|
|
11228
|
+
try {
|
|
11229
|
+
reconfirmation = await captureBatchReconfirmation(root, activeRecord, planSurface.surface.recovery_children);
|
|
11230
|
+
} catch {
|
|
11231
|
+
return reject("plan_projection_failed", "batch plan reconfirmation is not eligible");
|
|
11008
11232
|
}
|
|
11233
|
+
}
|
|
11234
|
+
const projection = {
|
|
11235
|
+
initiative_slug: initiativeSlug,
|
|
11236
|
+
batch_branch: batchBranch,
|
|
11237
|
+
is_resuming: isResuming,
|
|
11238
|
+
existing_batch: existingBatch,
|
|
11239
|
+
base_head: baseHead,
|
|
11240
|
+
budget: planSurface.surface.budget,
|
|
11241
|
+
plan_digest: planSurface.surface.plan_digest,
|
|
11242
|
+
recovery_children: planSurface.surface.recovery_children,
|
|
11243
|
+
risk_by_task: planSurface.surface.risk_by_task,
|
|
11244
|
+
excluded: planSurface.surface.excluded
|
|
11009
11245
|
};
|
|
11246
|
+
if (reconfirmation)
|
|
11247
|
+
Object.defineProperty(projection, "reconfirmation", { value: reconfirmation });
|
|
11248
|
+
return { ok: true, projection };
|
|
11010
11249
|
}
|
|
11011
11250
|
async function projectBatchDrift(options) {
|
|
11012
11251
|
const { root, initiative_slug: initiativeSlug, readInitiative } = options;
|
|
@@ -11046,34 +11285,37 @@ async function authorizeBatch(options) {
|
|
|
11046
11285
|
is_resuming: isResuming
|
|
11047
11286
|
} = projection;
|
|
11048
11287
|
const existingBatch = projection.existing_batch;
|
|
11049
|
-
const
|
|
11050
|
-
const branchBefore = spawnSync5("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
|
|
11288
|
+
const branchBefore = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
|
|
11051
11289
|
if (branchBefore.status !== 0)
|
|
11052
11290
|
return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
|
|
11291
|
+
const ownsUnpersistedHead = isResuming && existingBatch !== null && existingBatch.plan_digest === planDigest && expectedBatchHead(existingBatch) !== baseHead && ownUnpersistedBatchHead(root, existingBatch, baseHead);
|
|
11292
|
+
const fastForwardsRecordedHead = isResuming && existingBatch !== null && existingBatch.branch === batchBranch && expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead && classifyBatchLineage({
|
|
11293
|
+
root,
|
|
11294
|
+
branch: batchBranch,
|
|
11295
|
+
expectedHead: expectedBatchHead(existingBatch),
|
|
11296
|
+
childCommits: existingBatch.commits,
|
|
11297
|
+
batchId: existingBatch.batch_id
|
|
11298
|
+
}).kind === "fast_forward";
|
|
11299
|
+
if (isResuming && existingBatch && !existingBatch.commits.length && existingBatch.children[0]?.state === "settled" && expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead && !fastForwardsRecordedHead)
|
|
11300
|
+
return { outcome: "rejected", rejection: batchRejection("head_moved", "first unpersisted commit provenance is invalid") };
|
|
11053
11301
|
const reuseBlockers = [];
|
|
11054
11302
|
if (isResuming && existingBatch) {
|
|
11055
|
-
if (isExistingExpired)
|
|
11056
|
-
reuseBlockers.push("batch_authorization_expired");
|
|
11057
|
-
if (Date.parse(existingBatch.budget.deadline_at) <= Date.now())
|
|
11058
|
-
reuseBlockers.push("batch_budget_expired");
|
|
11059
11303
|
if (existingBatch.batch_state !== "running")
|
|
11060
11304
|
reuseBlockers.push("batch_not_running");
|
|
11061
11305
|
if (existingBatch.plan_digest !== planDigest)
|
|
11062
11306
|
reuseBlockers.push("batch_plan_digest_changed");
|
|
11063
11307
|
if (existingBatch.branch !== batchBranch)
|
|
11064
11308
|
reuseBlockers.push("batch_branch_changed");
|
|
11065
|
-
if (expectedBatchHead(existingBatch) !== baseHead)
|
|
11309
|
+
if (expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead && !fastForwardsRecordedHead)
|
|
11066
11310
|
reuseBlockers.push("batch_head_lineage_moved");
|
|
11067
11311
|
}
|
|
11068
11312
|
const reuseAuthorization = isResuming && reuseBlockers.length === 0;
|
|
11069
|
-
const expiresAt = reuseAuthorization ? existingBatch.authorization_expires_at : budget.deadline_at;
|
|
11070
11313
|
const batchId = isResuming && existingBatch ? existingBatch.batch_id : `batch-${initiativeSlug}-${randomUUID7()}`;
|
|
11071
11314
|
const facts = {
|
|
11072
11315
|
initiative_slug: initiativeSlug,
|
|
11073
11316
|
batch_branch: batchBranch,
|
|
11074
11317
|
plan_digest: planDigest,
|
|
11075
11318
|
budget,
|
|
11076
|
-
expires_at: expiresAt,
|
|
11077
11319
|
reuse_blockers: reuseBlockers,
|
|
11078
11320
|
children: projection.recovery_children.map((child) => ({
|
|
11079
11321
|
task_id: child.task_id,
|
|
@@ -11104,7 +11346,7 @@ async function authorizeBatch(options) {
|
|
|
11104
11346
|
return { outcome: "rejected", rejection: batchRejection("plan_changed", drift.plan_digest) };
|
|
11105
11347
|
if (drift.base_head === null)
|
|
11106
11348
|
return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
|
|
11107
|
-
if (drift.base_head !== baseHead)
|
|
11349
|
+
if (drift.base_head !== baseHead || ownsUnpersistedHead && !ownUnpersistedBatchHead(root, existingBatch, baseHead))
|
|
11108
11350
|
return { outcome: "rejected", rejection: batchRejection("head_moved", drift.base_head) };
|
|
11109
11351
|
const finalClaimTaskId = readActiveClaimTaskId(root);
|
|
11110
11352
|
const finalOwnClaim = isResuming && finalClaimTaskId !== null && isOwnBatchClaim(root, existingBatch, finalClaimTaskId, batchBranch);
|
|
@@ -11115,11 +11357,16 @@ async function authorizeBatch(options) {
|
|
|
11115
11357
|
if (!current || current.corrupt || JSON.stringify(current.record) !== JSON.stringify(existingBatch))
|
|
11116
11358
|
return { outcome: "rejected", rejection: batchRejection("plan_changed") };
|
|
11117
11359
|
}
|
|
11118
|
-
const branchAfter =
|
|
11360
|
+
const branchAfter = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
|
|
11119
11361
|
if (branchAfter.status !== 0 || branchAfter.stdout !== branchBefore.stdout)
|
|
11120
11362
|
return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "Git branch moved after native confirmation") };
|
|
11121
|
-
if (
|
|
11122
|
-
|
|
11363
|
+
if (projection.reconfirmation) {
|
|
11364
|
+
try {
|
|
11365
|
+
projection.reconfirmation.assertUnchanged();
|
|
11366
|
+
} catch {
|
|
11367
|
+
return { outcome: "rejected", rejection: batchRejection("plan_changed") };
|
|
11368
|
+
}
|
|
11369
|
+
}
|
|
11123
11370
|
const binding = {
|
|
11124
11371
|
batch_id: batchId,
|
|
11125
11372
|
initiative_slug: initiativeSlug,
|
|
@@ -11129,26 +11376,26 @@ async function authorizeBatch(options) {
|
|
|
11129
11376
|
budget,
|
|
11130
11377
|
actor_id: LITERAL_USER_ACTOR_ID,
|
|
11131
11378
|
confirmation_ref: options.confirmationRef({ batch_id: batchId, request_id: requestId }),
|
|
11132
|
-
expires_at: expiresAt,
|
|
11133
11379
|
nonce: options.nonce
|
|
11134
11380
|
};
|
|
11381
|
+
if (projection.reconfirmation)
|
|
11382
|
+
retainReconfirmation(binding.nonce, projection.reconfirmation);
|
|
11135
11383
|
return {
|
|
11136
11384
|
outcome: "authorized",
|
|
11137
11385
|
batch_id: batchId,
|
|
11138
11386
|
reuse_authorization: reuseAuthorization,
|
|
11139
11387
|
reuse_blockers: reuseBlockers,
|
|
11140
|
-
expires_at: expiresAt,
|
|
11141
11388
|
binding
|
|
11142
11389
|
};
|
|
11143
11390
|
}
|
|
11144
11391
|
|
|
11145
11392
|
// plugins/immune-brain/runtime/unattended/batch_runner.ts
|
|
11146
|
-
import { spawnSync as
|
|
11393
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
11147
11394
|
import { existsSync as existsSync9 } from "node:fs";
|
|
11148
11395
|
import { join as join14 } from "node:path";
|
|
11149
11396
|
|
|
11150
11397
|
// plugins/immune-brain/runtime/unattended/batch_git.ts
|
|
11151
|
-
import { spawnSync as
|
|
11398
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
11152
11399
|
import { createHash as createHash20, randomUUID as randomUUID8 } from "node:crypto";
|
|
11153
11400
|
import {
|
|
11154
11401
|
constants as constants6,
|
|
@@ -11173,7 +11420,7 @@ var DEFAULT_GIT_ENV = {
|
|
|
11173
11420
|
function runBatchGitPreflight(input) {
|
|
11174
11421
|
const { root, initiative_slug: initiativeSlug, base_head: baseHead } = input;
|
|
11175
11422
|
const branch = `imm/${initiativeSlug}`;
|
|
11176
|
-
const toplevelResult =
|
|
11423
|
+
const toplevelResult = spawnSync8("git", ["-C", root, "rev-parse", "--show-toplevel"], {
|
|
11177
11424
|
encoding: "utf8",
|
|
11178
11425
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11179
11426
|
});
|
|
@@ -11211,7 +11458,7 @@ function runBatchGitPreflight(input) {
|
|
|
11211
11458
|
message: `unsupported index flags (assume-unchanged/skip-worktree) detected: ${flaggedPreflight.join(", ")}`
|
|
11212
11459
|
};
|
|
11213
11460
|
}
|
|
11214
|
-
const statusResult =
|
|
11461
|
+
const statusResult = spawnSync8("git", ["-C", root, "status", "--porcelain", "--untracked-files=all", "--ignore-submodules=none"], {
|
|
11215
11462
|
encoding: "utf8",
|
|
11216
11463
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11217
11464
|
});
|
|
@@ -11229,7 +11476,7 @@ function runBatchGitPreflight(input) {
|
|
|
11229
11476
|
message: "working tree is dirty before batch preflight"
|
|
11230
11477
|
};
|
|
11231
11478
|
}
|
|
11232
|
-
const headResult =
|
|
11479
|
+
const headResult = spawnSync8("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
|
|
11233
11480
|
encoding: "utf8",
|
|
11234
11481
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11235
11482
|
});
|
|
@@ -11240,11 +11487,11 @@ function runBatchGitPreflight(input) {
|
|
|
11240
11487
|
message: "HEAD is uncommitted or not a valid commit"
|
|
11241
11488
|
};
|
|
11242
11489
|
}
|
|
11243
|
-
const branchCheck =
|
|
11490
|
+
const branchCheck = spawnSync8("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { stdio: ["ignore", "ignore", "ignore"] });
|
|
11244
11491
|
if (branchCheck.status === 0) {
|
|
11245
11492
|
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 &&
|
|
11493
|
+
const currentBranch = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], { encoding: "utf8" });
|
|
11494
|
+
if (findExistingActiveBatch(root, initiativeSlug) === null && settled?.branch === branch && currentBranch.status === 0 && currentBranch.stdout.trim() === branch && headResult.stdout.trim() === baseHead && spawnSync8("git", ["-C", root, "merge-base", "--is-ancestor", expectedBatchHead(settled), baseHead]).status === 0)
|
|
11248
11495
|
return { ok: true, branch };
|
|
11249
11496
|
return {
|
|
11250
11497
|
ok: false,
|
|
@@ -11252,25 +11499,25 @@ function runBatchGitPreflight(input) {
|
|
|
11252
11499
|
message: `branch refs/heads/${branch} already exists`
|
|
11253
11500
|
};
|
|
11254
11501
|
}
|
|
11255
|
-
const originalBranchResult =
|
|
11502
|
+
const originalBranchResult = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
11256
11503
|
encoding: "utf8",
|
|
11257
11504
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11258
11505
|
});
|
|
11259
11506
|
const originalBranch = originalBranchResult.stdout.trim();
|
|
11260
|
-
const checkoutResult =
|
|
11507
|
+
const checkoutResult = spawnSync8("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", "-b", branch, baseHead], {
|
|
11261
11508
|
encoding: "utf8",
|
|
11262
11509
|
stdio: ["ignore", "pipe", "pipe"],
|
|
11263
11510
|
env: DEFAULT_GIT_ENV
|
|
11264
11511
|
});
|
|
11265
11512
|
if (checkoutResult.status !== 0) {
|
|
11266
11513
|
const stderr = checkoutResult.stderr?.trim() || "";
|
|
11267
|
-
const branchExists = stderr.includes("already exists") ||
|
|
11268
|
-
const currentBranchCheck =
|
|
11514
|
+
const branchExists = stderr.includes("already exists") || spawnSync8("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
|
|
11515
|
+
const currentBranchCheck = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
11269
11516
|
encoding: "utf8",
|
|
11270
11517
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11271
11518
|
}).stdout.trim();
|
|
11272
11519
|
if (currentBranchCheck === branch && originalBranch && originalBranch !== branch) {
|
|
11273
|
-
|
|
11520
|
+
spawnSync8("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", originalBranch], {
|
|
11274
11521
|
stdio: ["ignore", "ignore", "ignore"]
|
|
11275
11522
|
});
|
|
11276
11523
|
}
|
|
@@ -11293,7 +11540,7 @@ function hasBoundaryWhitespace(path) {
|
|
|
11293
11540
|
return path.split("/").some((segment) => segment.trim() !== segment || segment.length === 0);
|
|
11294
11541
|
}
|
|
11295
11542
|
function getUnsupportedIndexFlags(root) {
|
|
11296
|
-
const result =
|
|
11543
|
+
const result = spawnSync8("git", ["-C", root, "ls-files", "-v", "-z", "--"], {
|
|
11297
11544
|
encoding: "utf8",
|
|
11298
11545
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11299
11546
|
});
|
|
@@ -11327,13 +11574,13 @@ function isPathAllowedForChild(path, taskId, scopeHint) {
|
|
|
11327
11574
|
});
|
|
11328
11575
|
}
|
|
11329
11576
|
function getChangedProjectPaths(root) {
|
|
11330
|
-
const tracked =
|
|
11577
|
+
const tracked = spawnSync8("git", ["-C", root, "diff-index", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
|
|
11331
11578
|
encoding: "utf8",
|
|
11332
11579
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11333
11580
|
});
|
|
11334
11581
|
if (tracked.status !== 0)
|
|
11335
11582
|
throw new Error("failed to inspect tracked diff vs HEAD");
|
|
11336
|
-
const untracked =
|
|
11583
|
+
const untracked = spawnSync8("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
|
|
11337
11584
|
encoding: "utf8",
|
|
11338
11585
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11339
11586
|
});
|
|
@@ -11343,7 +11590,7 @@ function getChangedProjectPaths(root) {
|
|
|
11343
11590
|
return [...new Set([...splitZ(tracked.stdout), ...splitZ(untracked.stdout)])];
|
|
11344
11591
|
}
|
|
11345
11592
|
function getStagedProjectPaths(root) {
|
|
11346
|
-
const staged =
|
|
11593
|
+
const staged = spawnSync8("git", ["-C", root, "diff-index", "--cached", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
|
|
11347
11594
|
encoding: "utf8",
|
|
11348
11595
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11349
11596
|
});
|
|
@@ -11352,13 +11599,13 @@ function getStagedProjectPaths(root) {
|
|
|
11352
11599
|
return staged.stdout.split("\x00").filter((p) => p.length > 0);
|
|
11353
11600
|
}
|
|
11354
11601
|
function getUnstagedProjectPaths(root) {
|
|
11355
|
-
const diffFiles =
|
|
11602
|
+
const diffFiles = spawnSync8("git", ["-C", root, "diff-files", "--name-only", "-z", "--ignore-submodules=none", "--"], {
|
|
11356
11603
|
encoding: "utf8",
|
|
11357
11604
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11358
11605
|
});
|
|
11359
11606
|
if (diffFiles.status !== 0)
|
|
11360
11607
|
throw new Error("failed to inspect unstaged tracked changes");
|
|
11361
|
-
const untracked =
|
|
11608
|
+
const untracked = spawnSync8("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
|
|
11362
11609
|
encoding: "utf8",
|
|
11363
11610
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11364
11611
|
});
|
|
@@ -11368,7 +11615,7 @@ function getUnstagedProjectPaths(root) {
|
|
|
11368
11615
|
return [...new Set([...splitZ(diffFiles.stdout), ...splitZ(untracked.stdout)])];
|
|
11369
11616
|
}
|
|
11370
11617
|
function getCommittedDeltaPaths(root) {
|
|
11371
|
-
const delta =
|
|
11618
|
+
const delta = spawnSync8("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", "HEAD~1", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
11372
11619
|
if (delta.status !== 0)
|
|
11373
11620
|
throw new Error("failed to inspect committed tree delta");
|
|
11374
11621
|
return delta.stdout.split("\x00").filter((p) => p.length > 0);
|
|
@@ -11474,7 +11721,7 @@ async function commitBatchChild(input) {
|
|
|
11474
11721
|
const goal = typeof intentSnapshot.goal === "string" ? intentSnapshot.goal : "";
|
|
11475
11722
|
const scopeHint = Array.isArray(intentSnapshot.scope_hint) ? intentSnapshot.scope_hint.filter((s) => typeof s === "string") : [];
|
|
11476
11723
|
if (expectedBranch !== undefined) {
|
|
11477
|
-
const currentBranchResult =
|
|
11724
|
+
const currentBranchResult = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
11478
11725
|
encoding: "utf8",
|
|
11479
11726
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11480
11727
|
});
|
|
@@ -11483,7 +11730,7 @@ async function commitBatchChild(input) {
|
|
|
11483
11730
|
throw new Error(`batch_head_lineage_broken: current branch ${currentBranch} does not match expected branch ${expectedBranch}`);
|
|
11484
11731
|
}
|
|
11485
11732
|
}
|
|
11486
|
-
const currentHeadResult =
|
|
11733
|
+
const currentHeadResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
11487
11734
|
encoding: "utf8",
|
|
11488
11735
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11489
11736
|
});
|
|
@@ -11502,7 +11749,7 @@ async function commitBatchChild(input) {
|
|
|
11502
11749
|
}
|
|
11503
11750
|
const pathsToStage = getUnstagedProjectPaths(root);
|
|
11504
11751
|
if (pathsToStage.length > 0) {
|
|
11505
|
-
const addResult =
|
|
11752
|
+
const addResult = spawnSync8("git", ["-C", root, "--literal-pathspecs", "add", "-A", "--", ...pathsToStage], {
|
|
11506
11753
|
encoding: "utf8",
|
|
11507
11754
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11508
11755
|
});
|
|
@@ -11517,7 +11764,7 @@ async function commitBatchChild(input) {
|
|
|
11517
11764
|
const staged = getStagedProjectPaths(root);
|
|
11518
11765
|
const stagedOutside = staged.filter((p) => !isPathAllowedForChild(p, taskId, scopeHint));
|
|
11519
11766
|
if (stagedOutside.length > 0) {
|
|
11520
|
-
|
|
11767
|
+
spawnSync8("git", ["-C", root, "reset", "--quiet"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
11521
11768
|
throw new Error("dirty_outside_scope");
|
|
11522
11769
|
}
|
|
11523
11770
|
const record = auditPair.record;
|
|
@@ -11535,7 +11782,7 @@ async function commitBatchChild(input) {
|
|
|
11535
11782
|
|
|
11536
11783
|
Immune-Brain-Batch: ${batchId}
|
|
11537
11784
|
`;
|
|
11538
|
-
const commitResult =
|
|
11785
|
+
const commitResult = spawnSync8("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "commit", "--no-verify", "-F", "-"], {
|
|
11539
11786
|
input: commitMessage,
|
|
11540
11787
|
encoding: "utf8",
|
|
11541
11788
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -11544,7 +11791,7 @@ Immune-Brain-Batch: ${batchId}
|
|
|
11544
11791
|
if (commitResult.status !== 0) {
|
|
11545
11792
|
throw new Error(`commit failed for child ${taskId}: ${commitResult.stderr?.trim() || "git commit failed"}`);
|
|
11546
11793
|
}
|
|
11547
|
-
const newHeadResult =
|
|
11794
|
+
const newHeadResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
11548
11795
|
encoding: "utf8",
|
|
11549
11796
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11550
11797
|
});
|
|
@@ -11552,7 +11799,7 @@ Immune-Brain-Batch: ${batchId}
|
|
|
11552
11799
|
if (newHeadResult.status !== 0 || !newHead || newHead === expectedHead) {
|
|
11553
11800
|
throw new Error("commit_failed");
|
|
11554
11801
|
}
|
|
11555
|
-
const parentsResult =
|
|
11802
|
+
const parentsResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD^@"], {
|
|
11556
11803
|
encoding: "utf8",
|
|
11557
11804
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11558
11805
|
});
|
|
@@ -11585,7 +11832,7 @@ async function lookupBatchCommit(input) {
|
|
|
11585
11832
|
let candidates = [];
|
|
11586
11833
|
let evidenceBacked = false;
|
|
11587
11834
|
if (evidence && evidence.commit) {
|
|
11588
|
-
const direct =
|
|
11835
|
+
const direct = spawnSync8("git", ["-C", root, "log", "-n", "1", `--format=${FORMAT}`, evidence.commit, "--"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
11589
11836
|
if (direct.status !== 0) {
|
|
11590
11837
|
throw new Error(`failed to inspect batch commit for ${taskId}: ${direct.stderr?.trim() || "git log failed"}`);
|
|
11591
11838
|
}
|
|
@@ -11598,7 +11845,7 @@ async function lookupBatchCommit(input) {
|
|
|
11598
11845
|
}
|
|
11599
11846
|
}
|
|
11600
11847
|
if (!candidates.length) {
|
|
11601
|
-
const result =
|
|
11848
|
+
const result = spawnSync8("git", [
|
|
11602
11849
|
"-C",
|
|
11603
11850
|
root,
|
|
11604
11851
|
"log",
|
|
@@ -11635,7 +11882,7 @@ async function lookupBatchCommit(input) {
|
|
|
11635
11882
|
throw new Error(`batch_head_lineage_broken: adopted commit author ${match.authorName} does not match batch authority ${DEFAULT_GIT_ENV.GIT_AUTHOR_NAME}`);
|
|
11636
11883
|
}
|
|
11637
11884
|
if (expectedBranch !== undefined) {
|
|
11638
|
-
const currentBranchResult =
|
|
11885
|
+
const currentBranchResult = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
11639
11886
|
encoding: "utf8",
|
|
11640
11887
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11641
11888
|
});
|
|
@@ -11645,15 +11892,17 @@ async function lookupBatchCommit(input) {
|
|
|
11645
11892
|
}
|
|
11646
11893
|
}
|
|
11647
11894
|
if (expectedHead !== undefined) {
|
|
11648
|
-
const currentHeadResult =
|
|
11895
|
+
const currentHeadResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
11649
11896
|
encoding: "utf8",
|
|
11650
11897
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11651
11898
|
});
|
|
11652
11899
|
const currentHead = currentHeadResult.stdout.trim();
|
|
11653
|
-
if (currentHead !== commit
|
|
11900
|
+
if (currentHead !== commit && spawnSync8("git", ["-C", root, "merge-base", "--is-ancestor", commit, currentHead], {
|
|
11901
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
11902
|
+
}).status !== 0) {
|
|
11654
11903
|
throw new Error(`batch_head_lineage_broken: current HEAD ${currentHead} diverged from adopted commit ${commit}`);
|
|
11655
11904
|
}
|
|
11656
|
-
const parentsResult =
|
|
11905
|
+
const parentsResult = spawnSync8("git", ["-C", root, "rev-parse", `${commit}^@`], {
|
|
11657
11906
|
encoding: "utf8",
|
|
11658
11907
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11659
11908
|
});
|
|
@@ -11671,7 +11920,7 @@ async function lookupBatchCommit(input) {
|
|
|
11671
11920
|
throw new Error(`batch_head_lineage_broken: adopted commit task lifecycle is not done: ${lifecycle}`);
|
|
11672
11921
|
}
|
|
11673
11922
|
const scopeHint = Array.isArray(auditPair.record.intent_snapshot.scope_hint) ? auditPair.record.intent_snapshot.scope_hint.filter((s) => typeof s === "string") : [];
|
|
11674
|
-
const deltaResult =
|
|
11923
|
+
const deltaResult = spawnSync8("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", expectedHead, commit], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
11675
11924
|
if (deltaResult.status !== 0) {
|
|
11676
11925
|
throw new Error("batch_head_lineage_broken: failed to inspect adopted commit delta");
|
|
11677
11926
|
}
|
|
@@ -11710,18 +11959,12 @@ function skipDependents(record, taskId, reason) {
|
|
|
11710
11959
|
}
|
|
11711
11960
|
record.children = record.children.map((child) => skip.has(child.task_id) && child.state === "pending" ? { ...child, state: "skipped_blocked", reason } : child);
|
|
11712
11961
|
}
|
|
11713
|
-
function budgetStopReason(record
|
|
11962
|
+
function budgetStopReason(record) {
|
|
11714
11963
|
const enrolledCount = record.children.filter((child) => child.state !== "pending" && child.state !== "skipped_blocked").length;
|
|
11715
11964
|
if (enrolledCount >= record.budget.max_children)
|
|
11716
11965
|
return `max_children budget exhausted (${record.budget.max_children})`;
|
|
11717
|
-
const deadline = Date.parse(record.budget.deadline_at);
|
|
11718
|
-
if (!Number.isNaN(deadline) && now >= deadline)
|
|
11719
|
-
return `deadline_at reached (${record.budget.deadline_at})`;
|
|
11720
11966
|
return null;
|
|
11721
11967
|
}
|
|
11722
|
-
function isAuthorizationExpiryError(error) {
|
|
11723
|
-
return error instanceof BatchAuthorizationExpiryError;
|
|
11724
|
-
}
|
|
11725
11968
|
function requireFreshProjection(result, taskId) {
|
|
11726
11969
|
if (result.error !== null)
|
|
11727
11970
|
throw new Error(`cannot reconcile Kernel projection for ${taskId}: ${result.error}`);
|
|
@@ -11732,7 +11975,7 @@ function nextEnrollableChild(record) {
|
|
|
11732
11975
|
}
|
|
11733
11976
|
var TERMINAL_NEXT_ACTIONS = {
|
|
11734
11977
|
completed: "The batch settled every enrollable child; review the commits and the tracker.",
|
|
11735
|
-
budget_stopped: "
|
|
11978
|
+
budget_stopped: "The child budget stopped new enrollments; re-confirm to continue under a new authorization.",
|
|
11736
11979
|
failed: "A commit or lineage failure stopped the batch; inspect the failing child and the branch state.",
|
|
11737
11980
|
rejected: "The batch was rejected before any enrollment; correct the stated reason and re-confirm.",
|
|
11738
11981
|
needs_human: "A parked child needs a human decision; resolve it, then re-confirm to continue.",
|
|
@@ -11787,28 +12030,44 @@ function failPersistedLineage(root, existing, message) {
|
|
|
11787
12030
|
}
|
|
11788
12031
|
return writeBatchRunState(root, record);
|
|
11789
12032
|
}
|
|
11790
|
-
function
|
|
12033
|
+
function reconcileLineage(root, record) {
|
|
11791
12034
|
if (!existsSync9(join14(root, ".git")))
|
|
11792
|
-
return null;
|
|
11793
|
-
const
|
|
11794
|
-
const
|
|
11795
|
-
|
|
11796
|
-
|
|
11797
|
-
|
|
11798
|
-
|
|
11799
|
-
|
|
11800
|
-
}
|
|
11801
|
-
const branchCheck = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
11802
|
-
encoding: "utf8",
|
|
11803
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
12035
|
+
return { record, failure: null };
|
|
12036
|
+
const expected = expectedBatchHead(record);
|
|
12037
|
+
const lineage = classifyBatchLineage({
|
|
12038
|
+
root,
|
|
12039
|
+
branch: record.branch ?? "",
|
|
12040
|
+
expectedHead: expected,
|
|
12041
|
+
childCommits: record.commits,
|
|
12042
|
+
batchId: record.batch_id
|
|
11804
12043
|
});
|
|
11805
|
-
|
|
11806
|
-
|
|
11807
|
-
|
|
11808
|
-
|
|
11809
|
-
return
|
|
12044
|
+
if (lineage.kind === "broken")
|
|
12045
|
+
return { record, failure: lineage.message };
|
|
12046
|
+
if (lineage.kind === "equal")
|
|
12047
|
+
return { record, failure: null };
|
|
12048
|
+
return {
|
|
12049
|
+
record: writeBatchRunState(root, {
|
|
12050
|
+
...record,
|
|
12051
|
+
adopted_heads: [...record.adopted_heads ?? [], { from: expected, to: lineage.head }]
|
|
12052
|
+
}),
|
|
12053
|
+
failure: null
|
|
12054
|
+
};
|
|
11810
12055
|
}
|
|
11811
12056
|
async function validatePersistedRun(input, record) {
|
|
12057
|
+
if (!record.commits.length && record.children[0]?.state === "settled" && existsSync9(join14(input.root, ".git"))) {
|
|
12058
|
+
const head = spawnSync9("git", ["-C", input.root, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
12059
|
+
const live = head.stdout.trim();
|
|
12060
|
+
const expected = expectedBatchHead(record);
|
|
12061
|
+
const adoptable = () => classifyBatchLineage({
|
|
12062
|
+
root: input.root,
|
|
12063
|
+
branch: record.branch ?? "",
|
|
12064
|
+
expectedHead: expected,
|
|
12065
|
+
childCommits: record.commits,
|
|
12066
|
+
batchId: record.batch_id
|
|
12067
|
+
}).kind === "fast_forward";
|
|
12068
|
+
if (head.status !== 0 || live !== expected && !ownUnpersistedBatchHead(input.root, record, live) && !adoptable())
|
|
12069
|
+
throw new Error("first unpersisted batch commit provenance is invalid");
|
|
12070
|
+
}
|
|
11812
12071
|
const plan = input.registry.children(input.capability);
|
|
11813
12072
|
if (record.plan_digest !== computeBatchPlanDigest(plan) || record.children.length !== plan.length || record.children.some((child, index) => {
|
|
11814
12073
|
const expected = plan[index];
|
|
@@ -11830,7 +12089,7 @@ async function validatePersistedRun(input, record) {
|
|
|
11830
12089
|
});
|
|
11831
12090
|
if (!evidence || evidence.commit !== child.commit) {
|
|
11832
12091
|
if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync9(join14(input.root, ".git"))) {
|
|
11833
|
-
const reach =
|
|
12092
|
+
const reach = spawnSync9("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
11834
12093
|
if (reach.status !== 0) {
|
|
11835
12094
|
throw new Error(`batch_head_lineage_broken: recorded commit ${child.commit} for ${child.task_id} is no longer reachable from HEAD`);
|
|
11836
12095
|
}
|
|
@@ -11854,8 +12113,7 @@ function validateRunAuthorization(input, existing) {
|
|
|
11854
12113
|
plan_digest: existing?.plan_digest ?? input.plan_digest,
|
|
11855
12114
|
base_head: existing?.base_head ?? input.base_head,
|
|
11856
12115
|
initiative_slug: existing?.initiative_slug ?? input.initiative_slug,
|
|
11857
|
-
budget: input.budget
|
|
11858
|
-
expires_at: input.authorization_expires_at
|
|
12116
|
+
budget: input.budget
|
|
11859
12117
|
}
|
|
11860
12118
|
});
|
|
11861
12119
|
if (authorized.issued_at !== input.confirmation_time || existing && Date.parse(authorized.issued_at) <= Date.parse(existing.confirmation_time))
|
|
@@ -11868,6 +12126,40 @@ function validateRunAuthorization(input, existing) {
|
|
|
11868
12126
|
});
|
|
11869
12127
|
if (computeBatchPlanDigest(children) !== authorized.plan_digest || input.plan_digest !== authorized.plan_digest || input.base_head !== authorized.base_head)
|
|
11870
12128
|
throw new Error("batch run input does not match the authorized plan or base_head");
|
|
12129
|
+
return authorized;
|
|
12130
|
+
}
|
|
12131
|
+
function applyPlanReconfirmation(input, existing) {
|
|
12132
|
+
if (existing.plan_digest === input.plan_digest)
|
|
12133
|
+
return existing;
|
|
12134
|
+
const next = {
|
|
12135
|
+
...existing,
|
|
12136
|
+
plan_digest: input.plan_digest,
|
|
12137
|
+
confirmation_time: input.confirmation_time,
|
|
12138
|
+
budget: input.budget
|
|
12139
|
+
};
|
|
12140
|
+
if (!existing.branch || input.batch_id !== existing.batch_id || input.initiative_slug !== existing.initiative_slug || input.base_head !== existing.base_head || input.budget.max_children !== existing.budget.max_children || input.budget.qa_failure_limit !== existing.budget.qa_failure_limit)
|
|
12141
|
+
throw new Error("plan_digest mismatch: reconfirmation binding changed");
|
|
12142
|
+
const validate = () => {
|
|
12143
|
+
const plan = input.registry.children(input.capability);
|
|
12144
|
+
if (existing.children.length !== plan.length || plan.some((child, index) => {
|
|
12145
|
+
const old = existing.children[index], nextChild = input.children[index];
|
|
12146
|
+
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);
|
|
12147
|
+
}))
|
|
12148
|
+
throw new Error("plan_digest mismatch: reconfirmation topology changed");
|
|
12149
|
+
const { issued_at: _issuedAt, ...binding } = validateRunAuthorization(input, { ...existing, plan_digest: input.plan_digest });
|
|
12150
|
+
if (binding.branch !== existing.branch)
|
|
12151
|
+
throw new Error("plan_digest mismatch: reconfirmation branch changed");
|
|
12152
|
+
prepareBatchRunState(input);
|
|
12153
|
+
return input.registry.inspect(input.capability, binding);
|
|
12154
|
+
};
|
|
12155
|
+
const authority = validate();
|
|
12156
|
+
const captured = takeReconfirmation(authority.nonce);
|
|
12157
|
+
if (captured.root !== input.root || captured.plan_digest !== input.plan_digest)
|
|
12158
|
+
throw new Error("plan_digest mismatch: captured reconfirmation does not match run input");
|
|
12159
|
+
return replaceBatchRunState(input.root, captured.stateBytes, next, () => {
|
|
12160
|
+
captured.assertUnchanged();
|
|
12161
|
+
validate();
|
|
12162
|
+
});
|
|
11871
12163
|
}
|
|
11872
12164
|
async function startBatch(input) {
|
|
11873
12165
|
try {
|
|
@@ -11898,7 +12190,9 @@ async function startBatchLocked(input) {
|
|
|
11898
12190
|
return reportFor({ ...rejected, batch_state: "rejected" }, "batch plan is empty", "Provide a non-empty enrollable child plan.");
|
|
11899
12191
|
}
|
|
11900
12192
|
let existing = readBatchRunState(input.root, input.batch_id);
|
|
12193
|
+
const reconfirmed = existing !== null && existing.plan_digest !== input.plan_digest;
|
|
11901
12194
|
if (existing) {
|
|
12195
|
+
existing = applyPlanReconfirmation(input, existing);
|
|
11902
12196
|
try {
|
|
11903
12197
|
await validatePersistedRun(input, existing);
|
|
11904
12198
|
} catch (error) {
|
|
@@ -11918,12 +12212,12 @@ async function startBatchLocked(input) {
|
|
|
11918
12212
|
if (existing?.batch_state === "needs_human") {
|
|
11919
12213
|
const priorConfirmation = Date.parse(existing.confirmation_time);
|
|
11920
12214
|
const nextConfirmation = Date.parse(input.confirmation_time);
|
|
11921
|
-
const
|
|
11922
|
-
|
|
11923
|
-
if (!freshAuthorization) {
|
|
12215
|
+
const freshAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation;
|
|
12216
|
+
if (!freshAuthorization && !reconfirmed) {
|
|
11924
12217
|
return finalize(input.root, existing, "the parked batch requires a fresh literal-user confirmation", "Resolve the parked child, then re-confirm the batch to continue.");
|
|
11925
12218
|
}
|
|
11926
|
-
|
|
12219
|
+
if (!reconfirmed)
|
|
12220
|
+
validateRunAuthorization(input, existing);
|
|
11927
12221
|
prepareBatchRunState({ ...input, now: input.now });
|
|
11928
12222
|
const remapped = await Promise.all(existing.children.map(async (child) => {
|
|
11929
12223
|
if (child.state !== "needs_human")
|
|
@@ -11945,7 +12239,6 @@ async function startBatchLocked(input) {
|
|
|
11945
12239
|
existing = writeBatchRunState(input.root, {
|
|
11946
12240
|
...reparked,
|
|
11947
12241
|
confirmation_time: input.confirmation_time,
|
|
11948
|
-
authorization_expires_at: input.authorization_expires_at,
|
|
11949
12242
|
budget: input.budget,
|
|
11950
12243
|
batch_state: "needs_human"
|
|
11951
12244
|
});
|
|
@@ -11955,28 +12248,11 @@ async function startBatchLocked(input) {
|
|
|
11955
12248
|
existing = writeBatchRunState(input.root, {
|
|
11956
12249
|
...existing,
|
|
11957
12250
|
confirmation_time: input.confirmation_time,
|
|
11958
|
-
authorization_expires_at: input.authorization_expires_at,
|
|
11959
12251
|
budget: input.budget,
|
|
11960
12252
|
batch_state: "running",
|
|
11961
12253
|
children: resumedChildren
|
|
11962
12254
|
});
|
|
11963
12255
|
}
|
|
11964
|
-
if (existing?.batch_state === "running") {
|
|
11965
|
-
const nextConfirmation = Date.parse(input.confirmation_time);
|
|
11966
|
-
const nextExpiry = Date.parse(input.authorization_expires_at);
|
|
11967
|
-
const priorConfirmation = Date.parse(existing.confirmation_time);
|
|
11968
|
-
const persistedExpiry = Date.parse(existing.authorization_expires_at);
|
|
11969
|
-
const renewedAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation && Number.isFinite(nextExpiry) && nextExpiry > Date.now() && nextExpiry > persistedExpiry;
|
|
11970
|
-
if (renewedAuthorization) {
|
|
11971
|
-
validateRunAuthorization(input, existing);
|
|
11972
|
-
existing = writeBatchRunState(input.root, {
|
|
11973
|
-
...existing,
|
|
11974
|
-
confirmation_time: input.confirmation_time,
|
|
11975
|
-
authorization_expires_at: input.authorization_expires_at,
|
|
11976
|
-
budget: input.budget
|
|
11977
|
-
});
|
|
11978
|
-
}
|
|
11979
|
-
}
|
|
11980
12256
|
if (existing) {
|
|
11981
12257
|
const interruptedChild = existing.children.find((child) => child.state === "enrolled" || child.state === "settled");
|
|
11982
12258
|
if (interruptedChild) {
|
|
@@ -12014,7 +12290,6 @@ async function startBatchLocked(input) {
|
|
|
12014
12290
|
plan_digest: input.plan_digest,
|
|
12015
12291
|
base_head: input.base_head,
|
|
12016
12292
|
confirmation_time: input.confirmation_time,
|
|
12017
|
-
authorization_expires_at: input.authorization_expires_at,
|
|
12018
12293
|
budget: input.budget,
|
|
12019
12294
|
now: input.now
|
|
12020
12295
|
});
|
|
@@ -12025,13 +12300,14 @@ async function startBatchLocked(input) {
|
|
|
12025
12300
|
record.batch_state = "running";
|
|
12026
12301
|
persist();
|
|
12027
12302
|
}
|
|
12028
|
-
|
|
12029
|
-
|
|
12030
|
-
if (
|
|
12303
|
+
const lineage = reconcileLineage(input.root, record);
|
|
12304
|
+
record = lineage.record;
|
|
12305
|
+
if (lineage.failure) {
|
|
12031
12306
|
record.batch_state = "failed";
|
|
12032
12307
|
persist();
|
|
12033
|
-
return finalize(input.root, record,
|
|
12308
|
+
return finalize(input.root, record, lineage.failure, "");
|
|
12034
12309
|
}
|
|
12310
|
+
let head = expectedBatchHead(record);
|
|
12035
12311
|
while (record.batch_state === "running") {
|
|
12036
12312
|
const child = nextEnrollableChild(record);
|
|
12037
12313
|
if (!child) {
|
|
@@ -12058,8 +12334,7 @@ async function startBatchLocked(input) {
|
|
|
12058
12334
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "enrolled", reason: "adopted existing batch claim after interruption" } : c);
|
|
12059
12335
|
persist();
|
|
12060
12336
|
} else {
|
|
12061
|
-
|
|
12062
|
-
if (now >= Date.parse(record.authorization_expires_at) || budgetStopReason(record, now)) {
|
|
12337
|
+
if (budgetStopReason(record)) {
|
|
12063
12338
|
record.batch_state = record.children.some((c) => c.state === "needs_human" || c.state === "skipped_blocked") ? "needs_human" : "budget_stopped";
|
|
12064
12339
|
persist();
|
|
12065
12340
|
break;
|
|
@@ -12074,8 +12349,7 @@ async function startBatchLocked(input) {
|
|
|
12074
12349
|
plan_digest: record.plan_digest,
|
|
12075
12350
|
base_head: record.base_head,
|
|
12076
12351
|
initiative_slug: record.initiative_slug,
|
|
12077
|
-
budget: record.budget
|
|
12078
|
-
expires_at: record.authorization_expires_at
|
|
12352
|
+
budget: record.budget
|
|
12079
12353
|
}
|
|
12080
12354
|
});
|
|
12081
12355
|
for (const committed of record.children) {
|
|
@@ -12095,12 +12369,6 @@ async function startBatchLocked(input) {
|
|
|
12095
12369
|
persist();
|
|
12096
12370
|
} catch (error) {
|
|
12097
12371
|
const message = error instanceof Error ? error.message : String(error);
|
|
12098
|
-
if (isAuthorizationExpiryError(error)) {
|
|
12099
|
-
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "pending", reason: message } : c);
|
|
12100
|
-
record.batch_state = "budget_stopped";
|
|
12101
|
-
persist();
|
|
12102
|
-
break;
|
|
12103
|
-
}
|
|
12104
12372
|
if (isLineageBreakError(message)) {
|
|
12105
12373
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
|
|
12106
12374
|
skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
@@ -12123,7 +12391,7 @@ async function startBatchLocked(input) {
|
|
|
12123
12391
|
function stopReasonFor(record) {
|
|
12124
12392
|
switch (record.batch_state) {
|
|
12125
12393
|
case "budget_stopped":
|
|
12126
|
-
return budgetStopReason(record
|
|
12394
|
+
return budgetStopReason(record) ?? "the child budget stopped new enrollments";
|
|
12127
12395
|
case "failed": {
|
|
12128
12396
|
const failedChild = record.children.find((c) => c.state === "needs_human" && c.reason);
|
|
12129
12397
|
return failedChild?.reason ?? "a commit or lineage failure stopped the batch";
|
|
@@ -12138,8 +12406,9 @@ function stopReasonFor(record) {
|
|
|
12138
12406
|
}
|
|
12139
12407
|
async function resumeBatch(input, projection) {
|
|
12140
12408
|
let existing = readBatchRunState(input.root, input.batch_id);
|
|
12141
|
-
if (!existing)
|
|
12409
|
+
if (!existing || existing.batch_state === "needs_human")
|
|
12142
12410
|
return startBatch(input);
|
|
12411
|
+
existing = applyPlanReconfirmation(input, existing);
|
|
12143
12412
|
try {
|
|
12144
12413
|
await validatePersistedRun(input, existing);
|
|
12145
12414
|
} catch (error) {
|
|
@@ -12154,15 +12423,14 @@ async function resumeBatch(input, projection) {
|
|
|
12154
12423
|
}
|
|
12155
12424
|
if (isTerminalBatchState(existing.batch_state))
|
|
12156
12425
|
return replayTerminal(input.root, existing);
|
|
12157
|
-
if (existing.batch_state === "needs_human")
|
|
12158
|
-
return startBatch(input);
|
|
12159
12426
|
const driven = existing.children.find((child) => child.state === "enrolled" || child.state === "settled");
|
|
12160
12427
|
if (driven) {
|
|
12161
12428
|
if (driven.state === "enrolled") {
|
|
12162
|
-
const
|
|
12163
|
-
|
|
12164
|
-
|
|
12165
|
-
|
|
12429
|
+
const drivenLineage = reconcileLineage(input.root, existing);
|
|
12430
|
+
existing = drivenLineage.record;
|
|
12431
|
+
if (drivenLineage.failure) {
|
|
12432
|
+
const failed = failPersistedLineage(input.root, existing, drivenLineage.failure);
|
|
12433
|
+
return finalize(input.root, failed, drivenLineage.failure, "");
|
|
12166
12434
|
}
|
|
12167
12435
|
const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, driven.task_id), driven.task_id);
|
|
12168
12436
|
const holdsClaim = fresh.claim !== null && fresh.claim.task_id === driven.task_id && input.kernel.ownsTaskClaim(driven.task_id);
|
|
@@ -12241,7 +12509,7 @@ async function driveInterruptedChild(input, child) {
|
|
|
12241
12509
|
record.batch_state = "running";
|
|
12242
12510
|
persist();
|
|
12243
12511
|
}
|
|
12244
|
-
let head = record
|
|
12512
|
+
let head = expectedBatchHead(record);
|
|
12245
12513
|
while (child.state === "enrolled" || child.state === "settled") {
|
|
12246
12514
|
if (child.state === "settled") {
|
|
12247
12515
|
let existing = null;
|
|
@@ -12262,6 +12530,10 @@ async function driveInterruptedChild(input, child) {
|
|
|
12262
12530
|
persist();
|
|
12263
12531
|
throw new BatchCommitAbortError(`commit lookup failed: ${message}`);
|
|
12264
12532
|
}
|
|
12533
|
+
if (!existing) {
|
|
12534
|
+
record = reconcileLineage(input.root, record).record;
|
|
12535
|
+
head = expectedBatchHead(record);
|
|
12536
|
+
}
|
|
12265
12537
|
const planChild = input.children.find((c) => c.task_id === child.task_id);
|
|
12266
12538
|
const intentPath = planChild?.intent_path ?? undefined;
|
|
12267
12539
|
const doCommit = async () => {
|
|
@@ -12280,6 +12552,8 @@ async function driveInterruptedChild(input, child) {
|
|
|
12280
12552
|
intentPath
|
|
12281
12553
|
});
|
|
12282
12554
|
};
|
|
12555
|
+
if (existing && !record.commits.length && existsSync9(join14(input.root, ".git")) && !ownUnpersistedBatchHead(input.root, record, existing.commit))
|
|
12556
|
+
throw new Error("first unpersisted batch commit provenance is invalid");
|
|
12283
12557
|
const adopted = existing ?? await doCommit().catch((error) => {
|
|
12284
12558
|
const message = error instanceof Error ? error.message : String(error);
|
|
12285
12559
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
|
|
@@ -12555,7 +12829,6 @@ async function mintCapability(registry, input) {
|
|
|
12555
12829
|
diff_hash: input.diff_hash,
|
|
12556
12830
|
actor_id: input.actor_id,
|
|
12557
12831
|
confirmation_ref: input.confirmation_ref,
|
|
12558
|
-
expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
|
12559
12832
|
findings_digest: input.action_kind === "request_rework" ? findingsDigestV2(input.findings) : null
|
|
12560
12833
|
};
|
|
12561
12834
|
return registry.issue(binding);
|
|
@@ -12692,7 +12965,6 @@ class ClaudeRuntime {
|
|
|
12692
12965
|
preparation_digest: preparation.digest,
|
|
12693
12966
|
actor_id: LITERAL_USER_ACTOR_ID,
|
|
12694
12967
|
confirmation_ref: gate.confirmation_ref,
|
|
12695
|
-
expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
|
12696
12968
|
nonce
|
|
12697
12969
|
};
|
|
12698
12970
|
const capability = this.enrollmentRegistry.issue(binding);
|
|
@@ -12716,11 +12988,41 @@ class ClaudeRuntime {
|
|
|
12716
12988
|
throw error;
|
|
12717
12989
|
}
|
|
12718
12990
|
}
|
|
12719
|
-
async advance(taskId, signal) {
|
|
12720
|
-
return this.withTerminalTracker(taskId, await this.coordinator.advance(taskId, { cwd: this.cwd }, signal));
|
|
12991
|
+
async advance(taskId, signal, meta) {
|
|
12992
|
+
return this.closeOutBatchChild(taskId, await this.withTerminalTracker(taskId, await this.coordinator.advance(taskId, { cwd: this.cwd }, signal)), meta);
|
|
12993
|
+
}
|
|
12994
|
+
async submitReview(taskId, verdictInput, meta) {
|
|
12995
|
+
return this.closeOutBatchChild(taskId, await this.withTerminalTracker(taskId, await submitClaudeReview(this.host, this.coordinator, { cwd: this.cwd }, taskId, verdictInput)), meta);
|
|
12721
12996
|
}
|
|
12722
|
-
async
|
|
12723
|
-
|
|
12997
|
+
async closeOutBatchChild(taskId, result, meta) {
|
|
12998
|
+
if (!meta || result === null || typeof result !== "object")
|
|
12999
|
+
return result;
|
|
13000
|
+
const lifecycle = result.record?.lifecycle;
|
|
13001
|
+
if (result.state !== "completed" && lifecycle !== "done")
|
|
13002
|
+
return result;
|
|
13003
|
+
let slug;
|
|
13004
|
+
try {
|
|
13005
|
+
slug = findResumableBatchSlugForTask(this.cwd, taskId);
|
|
13006
|
+
} catch {
|
|
13007
|
+
return result;
|
|
13008
|
+
}
|
|
13009
|
+
if (!slug)
|
|
13010
|
+
return result;
|
|
13011
|
+
try {
|
|
13012
|
+
if (existsSync10(join15(this.cwd, ".imm", "audit", taskId)))
|
|
13013
|
+
execFileSync7("git", ["-C", this.cwd, "add", "--", `.imm/audit/${taskId}`], { stdio: "ignore" });
|
|
13014
|
+
const batch = await this.startUnattendedBatch(slug, meta, { reuseOnly: true });
|
|
13015
|
+
return { ...result, batch };
|
|
13016
|
+
} catch (error) {
|
|
13017
|
+
return {
|
|
13018
|
+
...result,
|
|
13019
|
+
batch: {
|
|
13020
|
+
state: "rejected",
|
|
13021
|
+
reason: `batch continuation failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
13022
|
+
recovery_action: "call start_unattended_batch with the same Initiative to continue the batch"
|
|
13023
|
+
}
|
|
13024
|
+
};
|
|
13025
|
+
}
|
|
12724
13026
|
}
|
|
12725
13027
|
async withTerminalTracker(taskId, result) {
|
|
12726
13028
|
if (result === null || typeof result !== "object")
|
|
@@ -13022,15 +13324,16 @@ class ClaudeRuntime {
|
|
|
13022
13324
|
throw error;
|
|
13023
13325
|
}
|
|
13024
13326
|
}
|
|
13025
|
-
async startUnattendedBatch(initiativeSlug, meta) {
|
|
13327
|
+
async startUnattendedBatch(initiativeSlug, meta, options = {}) {
|
|
13026
13328
|
throwIfCancelled(meta.signal);
|
|
13329
|
+
const reuseOnly = options.reuseOnly === true;
|
|
13027
13330
|
const probe = probeHost(this.env, process.platform, this.hostVersion);
|
|
13028
13331
|
if (!probe.ok)
|
|
13029
13332
|
throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
13030
13333
|
const interactive = meta.interactive ?? this.interactive;
|
|
13031
|
-
if (!interactive)
|
|
13334
|
+
if (!reuseOnly && !interactive)
|
|
13032
13335
|
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
13033
|
-
if (!this.requestConfirmation)
|
|
13336
|
+
if (!reuseOnly && !this.requestConfirmation)
|
|
13034
13337
|
throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
|
|
13035
13338
|
let now = new Date().toISOString();
|
|
13036
13339
|
const preflight = await projectBatchPreflight({
|
|
@@ -13060,7 +13363,15 @@ class ClaudeRuntime {
|
|
|
13060
13363
|
readInitiative: this.readInitiative ?? observeGithubInitiative,
|
|
13061
13364
|
nonce: enrollmentNonce(),
|
|
13062
13365
|
gate: async (facts) => {
|
|
13063
|
-
|
|
13366
|
+
if (reuseOnly)
|
|
13367
|
+
return {
|
|
13368
|
+
kind: "host_rejection",
|
|
13369
|
+
value: {
|
|
13370
|
+
state: "blocked",
|
|
13371
|
+
reason: `batch authorization cannot be reused (${facts.reuse_blockers.join(", ")})`,
|
|
13372
|
+
recovery_action: "call start_unattended_batch with the same Initiative to confirm a fresh authorization"
|
|
13373
|
+
}
|
|
13374
|
+
};
|
|
13064
13375
|
let confirmationResult;
|
|
13065
13376
|
try {
|
|
13066
13377
|
confirmationResult = await this.requestConfirmation({
|
|
@@ -13082,17 +13393,14 @@ class ClaudeRuntime {
|
|
|
13082
13393
|
reason: child.reason
|
|
13083
13394
|
})),
|
|
13084
13395
|
budget: facts.budget,
|
|
13085
|
-
expires_at: facts.expires_at,
|
|
13086
13396
|
...facts.reuse_blockers.length > 0 ? {
|
|
13087
13397
|
re_confirmation_required: facts.reuse_blockers,
|
|
13088
13398
|
recovery: "confirm to issue a fresh authorization bound to the current plan and HEAD"
|
|
13089
13399
|
} : {}
|
|
13090
13400
|
},
|
|
13091
|
-
signal:
|
|
13401
|
+
signal: meta.signal
|
|
13092
13402
|
});
|
|
13093
13403
|
} catch (err) {
|
|
13094
|
-
if (deadline.timedOut())
|
|
13095
|
-
return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
|
|
13096
13404
|
if (meta.signal?.aborted)
|
|
13097
13405
|
return { kind: "host_rejection", value: batchReason("cancelled_before_execution") };
|
|
13098
13406
|
if (err instanceof NativeAuthorityError) {
|
|
@@ -13113,11 +13421,7 @@ class ClaudeRuntime {
|
|
|
13113
13421
|
kind: "host_rejection",
|
|
13114
13422
|
value: batchReason("confirmation_failed", err instanceof Error ? err.message : String(err))
|
|
13115
13423
|
};
|
|
13116
|
-
} finally {
|
|
13117
|
-
deadline.clear();
|
|
13118
13424
|
}
|
|
13119
|
-
if (deadline.timedOut())
|
|
13120
|
-
return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
|
|
13121
13425
|
if (confirmationResult.decision === "cancel" && meta.signal?.aborted)
|
|
13122
13426
|
return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
|
|
13123
13427
|
if (meta.signal?.aborted)
|
|
@@ -13143,12 +13447,10 @@ class ClaudeRuntime {
|
|
|
13143
13447
|
return authorization.value;
|
|
13144
13448
|
if (authorization.outcome === "rejected")
|
|
13145
13449
|
return authorization.rejection;
|
|
13146
|
-
const { binding, batch_id: batchId
|
|
13450
|
+
const { binding, batch_id: batchId } = authorization;
|
|
13147
13451
|
if (meta.signal?.aborted)
|
|
13148
13452
|
return batchReason("cancelled_before_execution");
|
|
13149
13453
|
now = new Date().toISOString();
|
|
13150
|
-
if (Date.parse(expiresAt) <= Date.parse(now))
|
|
13151
|
-
return batchReason("confirmation_failed", "confirmed batch deadline expired before authorization issuance");
|
|
13152
13454
|
const capability = this.batchRegistry.issue(binding, recoveryChildren, now);
|
|
13153
13455
|
const basePort = this.createBatchKernelPort(this.batchRegistry, capability, binding);
|
|
13154
13456
|
const kernelPort = {
|
|
@@ -13170,7 +13472,6 @@ class ClaudeRuntime {
|
|
|
13170
13472
|
plan_digest: planDigest,
|
|
13171
13473
|
base_head: binding.base_head,
|
|
13172
13474
|
confirmation_time: now,
|
|
13173
|
-
authorization_expires_at: expiresAt,
|
|
13174
13475
|
budget,
|
|
13175
13476
|
now,
|
|
13176
13477
|
kernel: kernelPort,
|
|
@@ -13381,11 +13682,11 @@ function createMcpRuntime(options = {}) {
|
|
|
13381
13682
|
if (name === "enroll")
|
|
13382
13683
|
return runtime.enroll(taskId, toolMeta);
|
|
13383
13684
|
if (name === "advance_assurance")
|
|
13384
|
-
return runtime.advance(taskId, toolMeta.signal);
|
|
13685
|
+
return runtime.advance(taskId, toolMeta.signal, toolMeta);
|
|
13385
13686
|
if (name === "submit_review") {
|
|
13386
13687
|
if (!Object.hasOwn(args, "verdict"))
|
|
13387
13688
|
throw new Error("verdict is required");
|
|
13388
|
-
return runtime.submitReview(taskId, args.verdict);
|
|
13689
|
+
return runtime.submitReview(taskId, args.verdict, toolMeta);
|
|
13389
13690
|
}
|
|
13390
13691
|
if (name === "revise_intent") {
|
|
13391
13692
|
if (!Object.hasOwn(args, "next_intent"))
|
|
@@ -13581,8 +13882,7 @@ ${b.children.map((c) => ` - ${c.task_id} (${c.slice_id}) [risk: ${c.risk ?? "un
|
|
|
13581
13882
|
b?.excluded && b.excluded.length > 0 ? `Excluded children (${b.excluded.length}):
|
|
13582
13883
|
${b.excluded.map((e) => ` - ${e.task_id} (${e.slice_id}): ${e.reason}`).join(`
|
|
13583
13884
|
`)}` : null,
|
|
13584
|
-
b?.budget ? `Budget: max_children=${b.budget.max_children},
|
|
13585
|
-
b?.expires_at ? `Expires at: ${b.expires_at}` : null
|
|
13885
|
+
b?.budget ? `Budget: max_children=${b.budget.max_children}, qa_failure_limit=${b.budget.qa_failure_limit}` : null
|
|
13586
13886
|
].filter(Boolean);
|
|
13587
13887
|
return {
|
|
13588
13888
|
mode: "form",
|