immune-brain 4.5.0 → 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/claude/mcp-server.mjs +679 -762
- package/plugins/immune-brain/dist/imm-loop.md +11 -4
- package/plugins/immune-brain/dist/imm-planner.md +37 -12
- 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 +738 -677
- 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/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/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 +120 -31
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +0 -6
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +6 -7
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +72 -119
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +36 -14
- package/plugins/immune-brain/runtime/unattended/types.ts +0 -2
- package/plugins/immune-brain/runtime/workspace_scope.ts +44 -8
- 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(() => {
|
|
@@ -2409,7 +2460,7 @@ function fileFingerprint(root, relativePath) {
|
|
|
2409
2460
|
return `file:${stat.mode}:${createHash7("sha256").update(readFileSync3(absolutePath)).digest("hex")}`;
|
|
2410
2461
|
}
|
|
2411
2462
|
function dirtyPaths(root) {
|
|
2412
|
-
const tracked = git(root, ["diff", "--name-only", "-z", "HEAD", "--"]);
|
|
2463
|
+
const tracked = git(root, ["diff", "--name-only", "--no-renames", "-z", "HEAD", "--"]);
|
|
2413
2464
|
const untracked = git(root, [
|
|
2414
2465
|
"ls-files",
|
|
2415
2466
|
"--others",
|
|
@@ -2584,6 +2635,25 @@ function headEntry(root, head, path) {
|
|
|
2584
2635
|
function taskPathMatchesScope(path, scope) {
|
|
2585
2636
|
return scope.some((scopePath) => pathMatchesScope(path, scopePath));
|
|
2586
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
|
+
}
|
|
2587
2657
|
function taskSnapshotOnce(root, scope, taskId) {
|
|
2588
2658
|
const repositoryRoot = git(root, ["rev-parse", "--show-toplevel"])?.trim();
|
|
2589
2659
|
const head = git(root, ["rev-parse", "--verify", "HEAD^{commit}"])?.trim();
|
|
@@ -2606,9 +2676,8 @@ function taskSnapshotOnce(root, scope, taskId) {
|
|
|
2606
2676
|
const uncommittedInScope = [...new Set([...unstagedPaths, ...untrackedPaths])].filter((path) => taskPathMatchesScope(path, scope)).sort(comparePaths);
|
|
2607
2677
|
if (uncommittedInScope.length > 0)
|
|
2608
2678
|
throw new Error(`task scope contains unstaged or untracked changes: ${uncommittedInScope.join(", ")}`);
|
|
2609
|
-
const
|
|
2610
|
-
|
|
2611
|
-
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);
|
|
2612
2681
|
const stagedFiles = {};
|
|
2613
2682
|
for (const path of taskPaths) {
|
|
2614
2683
|
const current = indexEntry(root, path);
|
|
@@ -2686,9 +2755,8 @@ function taskRevisionSnapshotOnce(root, scope, baseHead, taskId) {
|
|
|
2686
2755
|
const unstagedPaths = decodeNullPaths(gitBytes(root, ["diff", "--no-renames", "--name-only", "-z", "--"]), "unstaged task revision paths");
|
|
2687
2756
|
const untrackedPaths = decodeNullPaths(gitBytes(root, ["ls-files", "--others", "--exclude-standard", "-z", "--"]), "untracked task revision paths");
|
|
2688
2757
|
assertNoEnvelopeEscape(root, stagedPaths, scope, taskId);
|
|
2689
|
-
const
|
|
2690
|
-
|
|
2691
|
-
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);
|
|
2692
2760
|
const scopedUnstagedPaths = unstagedPaths.filter((path) => taskPathMatchesScope(path, scope));
|
|
2693
2761
|
const scopedUntrackedPaths = untrackedPaths.filter((path) => taskPathMatchesScope(path, scope));
|
|
2694
2762
|
assertNoCaseFoldCollisions([...scopedStagedPaths, ...scopedUnstagedPaths, ...scopedUntrackedPaths], "Git task revision paths");
|
|
@@ -4196,7 +4264,7 @@ function parseHistoryV3(value, index, violations) {
|
|
|
4196
4264
|
actor_id: stringAt(auth.actor_id, `${path}.authority.actor_id`, violations),
|
|
4197
4265
|
confirmation_ref: stringAt(auth.confirmation_ref, `${path}.authority.confirmation_ref`, violations),
|
|
4198
4266
|
issued_at: stringAt(auth.issued_at, `${path}.authority.issued_at`, violations),
|
|
4199
|
-
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) } : {}
|
|
4200
4268
|
};
|
|
4201
4269
|
}
|
|
4202
4270
|
return {
|
|
@@ -6718,6 +6786,8 @@ function publishInput(root, input) {
|
|
|
6718
6786
|
const recomputed = `sha256:${createHash13("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
|
|
6719
6787
|
if (recomputed !== input.expectedDiffHash)
|
|
6720
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");
|
|
6721
6791
|
return { snapshot, revision: publishReviewRevision(snapshot.repository_root, snapshot, recomputed, input.taskId) };
|
|
6722
6792
|
}
|
|
6723
6793
|
function captureReviewManifest(root, input) {
|
|
@@ -7007,7 +7077,7 @@ async function projectAssurance(root, taskId, diffProvider) {
|
|
|
7007
7077
|
|
|
7008
7078
|
// plugins/immune-brain/runtime/kernel/application.ts
|
|
7009
7079
|
function applyTaskAction(input) {
|
|
7010
|
-
const { root, task_id, prior_intent_token, registry, capability, diffProvider
|
|
7080
|
+
const { root, task_id, prior_intent_token, registry, capability, diffProvider } = input;
|
|
7011
7081
|
return withKernelStoreLock(root, () => {
|
|
7012
7082
|
const current = readTaskRecordRaw(root, task_id);
|
|
7013
7083
|
if (!current.record)
|
|
@@ -7058,7 +7128,7 @@ function applyTaskAction(input) {
|
|
|
7058
7128
|
diff_hash: trustedDiff.diff_hash,
|
|
7059
7129
|
...action.type === "request_rework" ? { findings_digest: findingsDigestV2(action.findings) } : {}
|
|
7060
7130
|
} : null;
|
|
7061
|
-
const inspectedAudit = expectedAuthority ? registry.inspect(capability, expectedAuthority
|
|
7131
|
+
const inspectedAudit = expectedAuthority ? registry.inspect(capability, expectedAuthority) : null;
|
|
7062
7132
|
const authorityRunId = inspectedAudit?.run_id;
|
|
7063
7133
|
const mutation = reduceTask(current.record, action, inspectedAudit ? inspectedAudit.audit : null, trustedDiff.changed_paths);
|
|
7064
7134
|
if (!isReducedMutation(mutation))
|
|
@@ -7084,7 +7154,7 @@ function applyTaskAction(input) {
|
|
|
7084
7154
|
consumeIntentToken(prior_intent_token);
|
|
7085
7155
|
consumeIntentToken(freshRead.token);
|
|
7086
7156
|
if (expectedAuthority) {
|
|
7087
|
-
registry.consume(capability, expectedAuthority
|
|
7157
|
+
registry.consume(capability, expectedAuthority);
|
|
7088
7158
|
}
|
|
7089
7159
|
const nextWorking = mutation.next_workspace_working;
|
|
7090
7160
|
const nextRecord = input.artifact_transition ? {
|
|
@@ -7410,7 +7480,6 @@ function createCanaryApplication(registry) {
|
|
|
7410
7480
|
registry,
|
|
7411
7481
|
capability,
|
|
7412
7482
|
diffProvider: input.diffProvider,
|
|
7413
|
-
now: Date.parse(now),
|
|
7414
7483
|
...operation.op === "complete" || operation.op === "stop" ? { terminal: { terminalized_at: now } } : {},
|
|
7415
7484
|
...artifactTransition ? { artifact_transition: artifactTransition } : {}
|
|
7416
7485
|
});
|
|
@@ -7458,7 +7527,7 @@ function createCanaryApplication(registry) {
|
|
|
7458
7527
|
intent_revision: current.record.intent_snapshot.revision,
|
|
7459
7528
|
intent_content_hash: current.record.intent_ref.content_hash,
|
|
7460
7529
|
diff_hash: "sha256:" + "0".repeat(64)
|
|
7461
|
-
}
|
|
7530
|
+
});
|
|
7462
7531
|
const nextClaim = {
|
|
7463
7532
|
...claim,
|
|
7464
7533
|
lifecycle_status: "draining",
|
|
@@ -7499,7 +7568,7 @@ function createCapabilityRegistry(capabilityBrand, hooks, domainLabel) {
|
|
|
7499
7568
|
return {
|
|
7500
7569
|
brand,
|
|
7501
7570
|
issue(binding, issuedAt = new Date().toISOString()) {
|
|
7502
|
-
hooks.validateBinding(binding
|
|
7571
|
+
hooks.validateBinding(binding);
|
|
7503
7572
|
const capability = Object.freeze(Object.defineProperties({}, {
|
|
7504
7573
|
[capabilityBrand]: { value: true, enumerable: false, writable: false, configurable: false },
|
|
7505
7574
|
[brand]: { value: true, enumerable: false, writable: false, configurable: false }
|
|
@@ -7507,16 +7576,16 @@ function createCapabilityRegistry(capabilityBrand, hooks, domainLabel) {
|
|
|
7507
7576
|
states.set(capability, { binding: { ...binding }, issued_at: issuedAt, consumed: false });
|
|
7508
7577
|
return capability;
|
|
7509
7578
|
},
|
|
7510
|
-
inspect(capability, expected
|
|
7579
|
+
inspect(capability, expected) {
|
|
7511
7580
|
if (!isCapability(capability))
|
|
7512
7581
|
throw new Error(`${domainLabel} capability is not recognized by this registry`);
|
|
7513
7582
|
const state = stateOf(capability);
|
|
7514
7583
|
if (state.consumed)
|
|
7515
7584
|
throw new Error(`${domainLabel} capability already consumed`);
|
|
7516
|
-
return hooks.validateAndProject({ ...state.binding, issued_at: state.issued_at }, expected
|
|
7585
|
+
return hooks.validateAndProject({ ...state.binding, issued_at: state.issued_at }, expected);
|
|
7517
7586
|
},
|
|
7518
|
-
consume(capability, expected
|
|
7519
|
-
const validated = this.inspect(capability, expected
|
|
7587
|
+
consume(capability, expected) {
|
|
7588
|
+
const validated = this.inspect(capability, expected);
|
|
7520
7589
|
stateOf(capability).consumed = true;
|
|
7521
7590
|
return validated;
|
|
7522
7591
|
},
|
|
@@ -7533,7 +7602,7 @@ function digestOfAction(action) {
|
|
|
7533
7602
|
}
|
|
7534
7603
|
function createMutationAuthorityRegistry() {
|
|
7535
7604
|
const inner = createCapabilityRegistry(MUTATION_AUTHORITY_CAPABILITY_BRAND, {
|
|
7536
|
-
validateBinding(binding
|
|
7605
|
+
validateBinding(binding) {
|
|
7537
7606
|
const missing = [];
|
|
7538
7607
|
if (binding.run_id !== undefined && binding.run_id.length === 0)
|
|
7539
7608
|
throw new Error("authority capability run_id must not be empty");
|
|
@@ -7547,12 +7616,8 @@ function createMutationAuthorityRegistry() {
|
|
|
7547
7616
|
throw new Error(`authority capability binding is incomplete: ${missing.join(", ")}`);
|
|
7548
7617
|
if (binding.findings_digest !== null && !/^sha256:[a-f0-9]{64}$/.test(binding.findings_digest))
|
|
7549
7618
|
throw new Error("authority capability findings_digest must be a canonical sha256 hash");
|
|
7550
|
-
if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
|
|
7551
|
-
throw new Error("authority capability must have a future expiry");
|
|
7552
7619
|
},
|
|
7553
|
-
validateAndProject(state, expected
|
|
7554
|
-
if (Date.parse(state.expires_at) <= now)
|
|
7555
|
-
throw new Error("authority capability has expired");
|
|
7620
|
+
validateAndProject(state, expected) {
|
|
7556
7621
|
const actionDigest = digestOfAction(expected.action);
|
|
7557
7622
|
if (state.action_digest !== actionDigest)
|
|
7558
7623
|
throw new Error("authority capability action digest mismatch");
|
|
@@ -7579,8 +7644,7 @@ function createMutationAuthorityRegistry() {
|
|
|
7579
7644
|
authority_kind: state.authority_kind,
|
|
7580
7645
|
actor_id: canonicalActorId(state.actor_id),
|
|
7581
7646
|
confirmation_ref: state.confirmation_ref,
|
|
7582
|
-
issued_at: state.issued_at
|
|
7583
|
-
expires_at: state.expires_at
|
|
7647
|
+
issued_at: state.issued_at
|
|
7584
7648
|
},
|
|
7585
7649
|
action_digest: actionDigest,
|
|
7586
7650
|
...state.run_id !== undefined ? { run_id: state.run_id } : {},
|
|
@@ -7595,20 +7659,20 @@ function createMutationAuthorityRegistry() {
|
|
|
7595
7659
|
return {
|
|
7596
7660
|
brand: inner.brand,
|
|
7597
7661
|
issue: inner.issue.bind(inner),
|
|
7598
|
-
inspect(capability, expected
|
|
7662
|
+
inspect(capability, expected) {
|
|
7599
7663
|
if (!capability)
|
|
7600
7664
|
throw new Error("privileged action requires an opaque authority capability");
|
|
7601
7665
|
try {
|
|
7602
|
-
return inner.inspect(capability, expected
|
|
7666
|
+
return inner.inspect(capability, expected);
|
|
7603
7667
|
} catch (err) {
|
|
7604
7668
|
if (err instanceof Error && err.message.includes("not recognized by this registry"))
|
|
7605
7669
|
throw new Error("privileged action requires an opaque authority capability");
|
|
7606
7670
|
throw err;
|
|
7607
7671
|
}
|
|
7608
7672
|
},
|
|
7609
|
-
consume(capability, expected
|
|
7673
|
+
consume(capability, expected) {
|
|
7610
7674
|
try {
|
|
7611
|
-
return inner.consume(capability, expected
|
|
7675
|
+
return inner.consume(capability, expected);
|
|
7612
7676
|
} catch (err) {
|
|
7613
7677
|
if (err instanceof Error && err.message.includes("not recognized by this registry"))
|
|
7614
7678
|
throw new Error("privileged action requires an opaque authority capability");
|
|
@@ -7623,7 +7687,7 @@ function createMutationAuthorityRegistry() {
|
|
|
7623
7687
|
var ENROLLMENT_CAPABILITY_BRAND = Symbol.for("assurance-kernel.enrollment-capability-brand");
|
|
7624
7688
|
function createEnrollmentAuthorityRegistry() {
|
|
7625
7689
|
return createCapabilityRegistry(ENROLLMENT_CAPABILITY_BRAND, {
|
|
7626
|
-
validateBinding(binding
|
|
7690
|
+
validateBinding(binding) {
|
|
7627
7691
|
const missing = [];
|
|
7628
7692
|
for (const [key, value] of Object.entries(binding)) {
|
|
7629
7693
|
if (value === undefined || value === null || value === "")
|
|
@@ -7631,12 +7695,8 @@ function createEnrollmentAuthorityRegistry() {
|
|
|
7631
7695
|
}
|
|
7632
7696
|
if (missing.length > 0)
|
|
7633
7697
|
throw new Error(`enrollment capability binding is incomplete: ${missing.join(", ")}`);
|
|
7634
|
-
if (Number.isNaN(Date.parse(binding.expires_at)) || Date.parse(binding.expires_at) <= Date.parse(issuedAt))
|
|
7635
|
-
throw new Error("enrollment capability must have a future expiry");
|
|
7636
7698
|
},
|
|
7637
|
-
validateAndProject(state, expected
|
|
7638
|
-
if (Date.parse(state.expires_at) <= now)
|
|
7639
|
-
throw new Error("enrollment capability has expired");
|
|
7699
|
+
validateAndProject(state, expected) {
|
|
7640
7700
|
for (const key of Object.keys(expected)) {
|
|
7641
7701
|
if (state[key] !== expected[key])
|
|
7642
7702
|
throw new Error(`enrollment capability ${key} mismatch`);
|
|
@@ -7650,13 +7710,16 @@ function createEnrollmentAuthorityRegistry() {
|
|
|
7650
7710
|
actor_id: state.actor_id,
|
|
7651
7711
|
confirmation_ref: state.confirmation_ref,
|
|
7652
7712
|
issued_at: state.issued_at,
|
|
7653
|
-
expires_at: state.expires_at,
|
|
7654
7713
|
nonce: state.nonce
|
|
7655
7714
|
};
|
|
7656
7715
|
}
|
|
7657
7716
|
}, "enrollment");
|
|
7658
7717
|
}
|
|
7659
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
|
+
|
|
7660
7723
|
// plugins/immune-brain/runtime/kernel/pi_canary_prepare.ts
|
|
7661
7724
|
import { createHash as createHash15 } from "node:crypto";
|
|
7662
7725
|
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
@@ -7758,6 +7821,238 @@ function revalidatePiCanary(root, input, previous) {
|
|
|
7758
7821
|
return { unchanged: current.digest === previous.digest, current };
|
|
7759
7822
|
}
|
|
7760
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
|
+
|
|
7761
8056
|
// plugins/immune-brain/runtime/kernel/enrollment.ts
|
|
7762
8057
|
function runEnrollmentPreconditionChecks(root, input, capability, registry, mode, beforeLock, onReady) {
|
|
7763
8058
|
const blockers = [];
|
|
@@ -7809,6 +8104,17 @@ function runEnrollmentPreconditionChecks(root, input, capability, registry, mode
|
|
|
7809
8104
|
} catch (error) {
|
|
7810
8105
|
fail(`git base: ${error instanceof Error ? error.message : String(error)}`, error);
|
|
7811
8106
|
}
|
|
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
|
+
}
|
|
7812
8118
|
const state = {
|
|
7813
8119
|
validated,
|
|
7814
8120
|
current,
|
|
@@ -7905,16 +8211,16 @@ function enrollCanaryTask(root, input, registry) {
|
|
|
7905
8211
|
if (checks.gitBaseHead !== gitBaseHead)
|
|
7906
8212
|
throw new Error("Git HEAD moved after the enrollment confirmation");
|
|
7907
8213
|
if (input.batch) {
|
|
7908
|
-
const batch = input.batch.registry.inspect(input.batch.capability, input.batch.binding
|
|
7909
|
-
if (input.batch.registry.consumedChildren(input.batch.capability).length === 0
|
|
7910
|
-
|
|
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);
|
|
7911
8217
|
if (checks.gitBaseHead !== input.batch.expected_head)
|
|
7912
8218
|
throw new Error(`batch_head_lineage_broken: expected ${input.batch.expected_head}, found ${checks.gitBaseHead}`);
|
|
7913
8219
|
}
|
|
7914
8220
|
registry.consume(input.capability, input.capability_binding);
|
|
7915
8221
|
consumed = true;
|
|
7916
8222
|
if (input.batch)
|
|
7917
|
-
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);
|
|
7918
8224
|
if (!gitBaseHead)
|
|
7919
8225
|
throw new Error("enrollment requires a committed Git HEAD");
|
|
7920
8226
|
const record = buildTaskRecordV4(input, checks.intent, gitBaseHead);
|
|
@@ -7968,7 +8274,7 @@ function enrollCanaryTask(root, input, registry) {
|
|
|
7968
8274
|
}
|
|
7969
8275
|
|
|
7970
8276
|
// plugins/immune-brain/runtime/assurance/enrollment_git_base.ts
|
|
7971
|
-
import { execFileSync as execFileSync4, spawnSync as
|
|
8277
|
+
import { execFileSync as execFileSync4, spawnSync as spawnSync5 } from "node:child_process";
|
|
7972
8278
|
function git2(root, args, input, env = process.env) {
|
|
7973
8279
|
return execFileSync4("git", args, { cwd: root, encoding: "utf8", input, env, stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
7974
8280
|
}
|
|
@@ -7981,8 +8287,8 @@ function inspectEnrollmentGitBase(root) {
|
|
|
7981
8287
|
const branch = git2(root, ["symbolic-ref", "--no-recurse", "HEAD"]);
|
|
7982
8288
|
if (!branch.startsWith("refs/heads/"))
|
|
7983
8289
|
throw new Error("Enrollment HEAD must name a local branch");
|
|
7984
|
-
const ref =
|
|
7985
|
-
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 });
|
|
7986
8292
|
if (ref.status !== 1 || symbolic.status !== 1)
|
|
7987
8293
|
throw new Error("Enrollment Git HEAD is invalid; only an absent branch can be initialized");
|
|
7988
8294
|
return { state: "unborn", branch };
|
|
@@ -7992,7 +8298,7 @@ function enrollmentGitBaseNotice(base) {
|
|
|
7992
8298
|
}
|
|
7993
8299
|
function identityEnv(root) {
|
|
7994
8300
|
const configured = (key) => {
|
|
7995
|
-
const result =
|
|
8301
|
+
const result = spawnSync5("git", ["config", "--get", key], { cwd: root, encoding: "utf8" });
|
|
7996
8302
|
if (result.status !== 0 && result.status !== 1)
|
|
7997
8303
|
throw new Error(`Cannot read Git identity: ${key}`);
|
|
7998
8304
|
return result.stdout?.trim() ?? "";
|
|
@@ -8041,10 +8347,10 @@ function initializeEnrollmentGitBase(root, input, previous, base, signal) {
|
|
|
8041
8347
|
}
|
|
8042
8348
|
|
|
8043
8349
|
// plugins/immune-brain/runtime/assurance/qa.ts
|
|
8044
|
-
import { createHash as
|
|
8350
|
+
import { createHash as createHash18 } from "node:crypto";
|
|
8045
8351
|
// plugins/immune-brain/runtime/assurance/delivery_workspace.ts
|
|
8046
8352
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
8047
|
-
import { createHash as
|
|
8353
|
+
import { createHash as createHash17 } from "node:crypto";
|
|
8048
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";
|
|
8049
8355
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
8050
8356
|
import { dirname as dirname6, isAbsolute as isAbsolute5, join as join8, parse, relative as relative4, resolve as resolve9, sep as sep5 } from "node:path";
|
|
@@ -8194,7 +8500,7 @@ function workspaceFiles(root, dir = root, result = Object.create(null)) {
|
|
|
8194
8500
|
result[rel] = `${stat.mode & 4095}:directory`;
|
|
8195
8501
|
workspaceFiles(root, path, result);
|
|
8196
8502
|
} else if (stat.isFile())
|
|
8197
|
-
result[rel] = `${stat.mode & 4095}:${
|
|
8503
|
+
result[rel] = `${stat.mode & 4095}:${createHash17("sha256").update(readFileSync10(path)).digest("hex")}`;
|
|
8198
8504
|
else
|
|
8199
8505
|
throw new DeliveryWorkspaceError("delivery contains an unsupported filesystem entry");
|
|
8200
8506
|
}
|
|
@@ -8296,7 +8602,7 @@ function deliveryTreeForSnapshot(snapshot, writeTree) {
|
|
|
8296
8602
|
if (!record || record.contract !== "assurance_kernel/task_record/v4" || !record.git_base_head)
|
|
8297
8603
|
throw new Error("QA delivery requires a TaskRecord v4 git_base_head");
|
|
8298
8604
|
const captured = captureGitTaskRevisionSnapshot(snapshot.root, record.intent_snapshot.scope_hint, record.git_base_head, snapshot.task_id);
|
|
8299
|
-
const digest = `sha256:${
|
|
8605
|
+
const digest = `sha256:${createHash18("sha256").update(JSON.stringify(captured)).digest("hex")}`;
|
|
8300
8606
|
if (digest !== snapshot.diff_hash)
|
|
8301
8607
|
throw new Error("QA delivery identity does not match the frozen snapshot");
|
|
8302
8608
|
return writeDeliveryTree(snapshot.root, captured);
|
|
@@ -8331,7 +8637,7 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
|
8331
8637
|
const diagnostics = (stage, outcome, affected = group, commandStage = "check", elapsed = 0, result) => affected.map((item) => ({
|
|
8332
8638
|
acceptance_id: item.id,
|
|
8333
8639
|
descriptor_ref: `acceptance/${item.index}/verification/${commandStage === "prepare" ? "environment/prepare" : "command"}`,
|
|
8334
|
-
descriptor_digest: `sha256:${
|
|
8640
|
+
descriptor_digest: `sha256:${createHash18("sha256").update(JSON.stringify(item.descriptor)).digest("hex")}`,
|
|
8335
8641
|
stage,
|
|
8336
8642
|
outcome,
|
|
8337
8643
|
elapsed_ms: elapsed,
|
|
@@ -8405,8 +8711,8 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
|
8405
8711
|
}
|
|
8406
8712
|
evidence.push({
|
|
8407
8713
|
stage,
|
|
8408
|
-
environment:
|
|
8409
|
-
command:
|
|
8714
|
+
environment: createHash18("sha256").update(environmentKey).digest("hex"),
|
|
8715
|
+
command: createHash18("sha256").update(JSON.stringify(command)).digest("hex"),
|
|
8410
8716
|
entry: frozen?.entry?.content_hash ?? null,
|
|
8411
8717
|
interpreter: frozen?.interpreter?.content_hash ?? null,
|
|
8412
8718
|
exit_code: result.exit_code,
|
|
@@ -8484,7 +8790,7 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
|
8484
8790
|
};
|
|
8485
8791
|
if (findings.length)
|
|
8486
8792
|
return { ...base, decision: "rework", findings };
|
|
8487
|
-
const executionDigest =
|
|
8793
|
+
const executionDigest = createHash18("sha256").update(JSON.stringify({
|
|
8488
8794
|
tree,
|
|
8489
8795
|
platform: process.platform,
|
|
8490
8796
|
arch: process.arch,
|
|
@@ -8569,11 +8875,6 @@ var BATCH_REASONS = Object.freeze({
|
|
|
8569
8875
|
reason: "native confirmation port is unavailable",
|
|
8570
8876
|
recovery_action: "retry through a fresh native gate in the current Host"
|
|
8571
8877
|
},
|
|
8572
|
-
confirmation_timed_out: {
|
|
8573
|
-
state: "rejected",
|
|
8574
|
-
reason: "native confirmation timed out waiting for user interaction",
|
|
8575
|
-
recovery_action: "retry through a fresh native gate in the current Host"
|
|
8576
|
-
},
|
|
8577
8878
|
confirmation_cancelled: {
|
|
8578
8879
|
state: "cancelled",
|
|
8579
8880
|
reason: "native interaction cancelled",
|
|
@@ -8639,23 +8940,6 @@ function batchReason(key, detail = "") {
|
|
|
8639
8940
|
};
|
|
8640
8941
|
}
|
|
8641
8942
|
|
|
8642
|
-
// plugins/immune-brain/runtime/unattended/confirmation_deadline.ts
|
|
8643
|
-
var CONFIRMATION_TIMEOUT_DEFAULT_MS = 60000;
|
|
8644
|
-
var CONFIRMATION_TIMEOUT_ENV = "IMMUNE_BRAIN_BATCH_TIMEOUT_MS";
|
|
8645
|
-
function startConfirmationDeadline(input) {
|
|
8646
|
-
const configured = Number(input.env?.[CONFIRMATION_TIMEOUT_ENV]);
|
|
8647
|
-
const timeoutMs = Number.isFinite(configured) && configured > 0 ? configured : CONFIRMATION_TIMEOUT_DEFAULT_MS;
|
|
8648
|
-
const controller = new AbortController;
|
|
8649
|
-
const timer = setTimeout(() => {
|
|
8650
|
-
controller.abort(new Error("native confirmation timed out waiting for user interaction"));
|
|
8651
|
-
}, timeoutMs);
|
|
8652
|
-
return {
|
|
8653
|
-
signal: input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal,
|
|
8654
|
-
timedOut: () => controller.signal.aborted && !input.signal?.aborted,
|
|
8655
|
-
clear: () => clearTimeout(timer)
|
|
8656
|
-
};
|
|
8657
|
-
}
|
|
8658
|
-
|
|
8659
8943
|
// plugins/immune-brain/runtime/authorization_operation.ts
|
|
8660
8944
|
function deriveAuthorizationOperation(input) {
|
|
8661
8945
|
if (input.readiness.state === "resolve_user_decision")
|
|
@@ -8885,17 +9169,17 @@ function createGhTransport(binary = "gh") {
|
|
|
8885
9169
|
return new Promise((complete) => {
|
|
8886
9170
|
let stdout = Buffer.alloc(0);
|
|
8887
9171
|
let stderr = Buffer.alloc(0);
|
|
8888
|
-
|
|
8889
|
-
if (options.budget && (options.budget.signal?.aborted || remaining <= 0)) {
|
|
9172
|
+
if (options.signal?.aborted) {
|
|
8890
9173
|
complete({
|
|
8891
9174
|
exit_code: 1,
|
|
8892
9175
|
stdout: "",
|
|
8893
|
-
stderr:
|
|
9176
|
+
stderr: "operation cancelled by the caller",
|
|
8894
9177
|
timed_out: true,
|
|
8895
9178
|
output_exceeded: false
|
|
8896
9179
|
});
|
|
8897
9180
|
return;
|
|
8898
9181
|
}
|
|
9182
|
+
const remaining = GH_TIMEOUT_MS;
|
|
8899
9183
|
let timedOut = false;
|
|
8900
9184
|
let outputExceeded = false;
|
|
8901
9185
|
let timer;
|
|
@@ -9283,7 +9567,7 @@ ${provenance}## How to use this Issue
|
|
|
9283
9567
|
|
|
9284
9568
|
- Edit planning prose and Slice ordering directly after creation.
|
|
9285
9569
|
- Keep each Slice marker attached to exactly one stable Slice entry.
|
|
9286
|
-
- 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.
|
|
9287
9571
|
|
|
9288
9572
|
## Problem
|
|
9289
9573
|
|
|
@@ -9432,7 +9716,7 @@ ${publicText(projection.agent_handoff ?? "Implement only the bounded TaskIntent
|
|
|
9432
9716
|
${ISSUE_FOOTER}
|
|
9433
9717
|
`;
|
|
9434
9718
|
}
|
|
9435
|
-
async function upsertTask(root, gh, operation, source
|
|
9719
|
+
async function upsertTask(root, gh, operation, source) {
|
|
9436
9720
|
const labelFailure = await labelAvailabilityFailure(root, gh, source.repository, desiredTaskLabels(operation));
|
|
9437
9721
|
if (labelFailure)
|
|
9438
9722
|
return labelFailure;
|
|
@@ -9447,8 +9731,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
|
|
|
9447
9731
|
const found = lookup(source.issues);
|
|
9448
9732
|
if (found.kind === "ambiguous")
|
|
9449
9733
|
return result(operation.op, "ambiguous_remote_state", found.message);
|
|
9450
|
-
if (found.kind === "missing" && pendingBinding !== null && pendingBinding !== undefined)
|
|
9451
|
-
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);
|
|
9452
9734
|
const blockerIds = operation.projection?.blocked_by ?? [];
|
|
9453
9735
|
const blockers = [];
|
|
9454
9736
|
for (const blockerId of blockerIds) {
|
|
@@ -9473,32 +9755,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
|
|
|
9473
9755
|
let createdChild = false;
|
|
9474
9756
|
let labelsConverged = false;
|
|
9475
9757
|
if (found.kind === "missing") {
|
|
9476
|
-
if (amendmentContext !== undefined) {
|
|
9477
|
-
const reRead = await snapshot(root, gh, operation.op);
|
|
9478
|
-
if ("contract" in reRead)
|
|
9479
|
-
return reRead;
|
|
9480
|
-
const reReadParent = initiativeLookup(reRead.issues, reRead.repository.id, operation.initiative_id);
|
|
9481
|
-
if (reReadParent.kind !== "found")
|
|
9482
|
-
return result(operation.op, "ambiguous_remote_state", reReadParent.kind === "ambiguous" ? reReadParent.message : "amendment Parent is not observable before creating a new Child", parent.issue);
|
|
9483
|
-
if (reReadParent.issue.number !== amendmentContext.parentIssueNumber)
|
|
9484
|
-
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);
|
|
9485
|
-
if (reReadParent.issue.state !== "open")
|
|
9486
|
-
return result(operation.op, "ambiguous_remote_state", "amendment Parent is no longer open before creating a new Child", reReadParent.issue);
|
|
9487
|
-
if (amendmentContext.parent.title !== reReadParent.issue.title || amendmentContext.parent.body !== reReadParent.issue.body)
|
|
9488
|
-
return result(operation.op, "ambiguous_remote_state", "amendment Parent content changed before creating a new Child", reReadParent.issue);
|
|
9489
|
-
if (sliceCount(reReadParent.issue.body, operation.slice_id) !== 1)
|
|
9490
|
-
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);
|
|
9491
|
-
const raced = lookup(reRead.issues);
|
|
9492
|
-
if (raced.kind === "ambiguous")
|
|
9493
|
-
return result(operation.op, "ambiguous_remote_state", raced.message);
|
|
9494
|
-
if (raced.kind === "found") {
|
|
9495
|
-
const approved = amendmentContext.pendingContent.get(operation.task_id);
|
|
9496
|
-
const resumable = approved !== undefined && raced.issue.state === "open" && carriesApprovedContent(raced.issue.title, raced.issue.body, approved);
|
|
9497
|
-
if (!resumable)
|
|
9498
|
-
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);
|
|
9499
|
-
return updatePendingChild(root, gh, reRead, operation, raced.issue, undefined, blockers, approved, amendmentContext);
|
|
9500
|
-
}
|
|
9501
|
-
}
|
|
9502
9758
|
const mutation = await gh.run([
|
|
9503
9759
|
"issue",
|
|
9504
9760
|
"create",
|
|
@@ -9521,11 +9777,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
|
|
|
9521
9777
|
}
|
|
9522
9778
|
if (created.issue.body !== body || created.issue.title !== title)
|
|
9523
9779
|
return result(operation.op, "retryable_failure", "Task Issue did not converge to the requested title and body", created.issue);
|
|
9524
|
-
if (amendmentContext !== undefined) {
|
|
9525
|
-
if (created.issue.state !== "open")
|
|
9526
|
-
return result(operation.op, "ambiguous_remote_state", `new pending Task ${operation.task_id} (Issue #${created.issue.number}) is not open after creation`, created.issue);
|
|
9527
|
-
return updatePendingChild(root, gh, source, operation, created.issue, undefined, blockers, amendmentContext.pendingContent.get(operation.task_id), amendmentContext);
|
|
9528
|
-
}
|
|
9529
9780
|
child = created.issue;
|
|
9530
9781
|
createdChild = true;
|
|
9531
9782
|
} else {
|
|
@@ -9533,27 +9784,6 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
|
|
|
9533
9784
|
if (owned.kind !== "found")
|
|
9534
9785
|
return result(operation.op, "ambiguous_remote_state", owned.kind === "ambiguous" ? owned.message : "Task Issue ownership changed during publication", found.issue);
|
|
9535
9786
|
child = owned.issue;
|
|
9536
|
-
if (pendingBinding !== null) {
|
|
9537
|
-
if (child.state !== "open")
|
|
9538
|
-
return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} is closed and cannot be amended as pending work`, found.issue);
|
|
9539
|
-
if (pendingBinding !== undefined && pendingBinding.issue_number !== child.number)
|
|
9540
|
-
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);
|
|
9541
|
-
const approvedFinal = amendmentContext?.pendingContent.get(operation.task_id);
|
|
9542
|
-
const isFinal = approvedFinal !== undefined && carriesApprovedContent(found.issue.title, found.issue.body, approvedFinal);
|
|
9543
|
-
if (!isFinal) {
|
|
9544
|
-
const baselineMatches = pendingBinding !== undefined && pendingBinding.title === found.issue.title && carriesApprovedContent(found.issue.title, found.issue.body, pendingBinding);
|
|
9545
|
-
if (!baselineMatches)
|
|
9546
|
-
return result(operation.op, "ambiguous_remote_state", `Task ${operation.task_id} changed since the approved amendment baseline`, found.issue);
|
|
9547
|
-
}
|
|
9548
|
-
return updatePendingChild(root, gh, source, operation, child, pendingBinding?.issue_number, blockers, approvedFinal, amendmentContext);
|
|
9549
|
-
}
|
|
9550
|
-
if (amendmentContext !== undefined) {
|
|
9551
|
-
const approvedFinal = amendmentContext.pendingContent.get(operation.task_id);
|
|
9552
|
-
const resumable = approvedFinal !== undefined && child.state === "open" && carriesApprovedContent(found.issue.title, found.issue.body, approvedFinal);
|
|
9553
|
-
if (!resumable)
|
|
9554
|
-
return result(operation.op, "ambiguous_remote_state", `new pending Task ${operation.task_id} is unbound but Issue #${child.number} already exists with divergent content; bind it to amend`, found.issue);
|
|
9555
|
-
return updatePendingChild(root, gh, source, operation, child, undefined, blockers, approvedFinal, amendmentContext);
|
|
9556
|
-
}
|
|
9557
9787
|
if (!sameTrackedBody(found.issue.body, body) || found.issue.title !== title)
|
|
9558
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);
|
|
9559
9789
|
const intentMismatch = publishedIntentMismatch(found.issue.body, operation.task_id, operation.intent_hash);
|
|
@@ -9660,7 +9890,7 @@ async function closeTerminalIssue(root, gh, operation, source, issue, lookup) {
|
|
|
9660
9890
|
return result(operation.op, "updated", "terminal Task Issue closure confirmed", found.issue);
|
|
9661
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);
|
|
9662
9892
|
}
|
|
9663
|
-
async function
|
|
9893
|
+
async function markChildTerminal(root, gh, operation, source) {
|
|
9664
9894
|
const lookup = (issues) => taskLookup(issues, source.repository.id, operation.task_id);
|
|
9665
9895
|
const found = lookup(source.issues);
|
|
9666
9896
|
if (found.kind === "missing")
|
|
@@ -9710,6 +9940,69 @@ async function markTerminal(root, gh, operation, source) {
|
|
|
9710
9940
|
return refreshedOwnership;
|
|
9711
9941
|
return closeTerminalIssue(root, gh, operation, refreshed, reread.issue, lookup);
|
|
9712
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
|
+
}
|
|
9713
10006
|
function normalizedList(value, name, max = 2000) {
|
|
9714
10007
|
if (value === undefined)
|
|
9715
10008
|
return;
|
|
@@ -9850,457 +10143,15 @@ async function runGithubTrackerOperation(root, input, gh = createGhTransport())
|
|
|
9850
10143
|
return markTerminal(absoluteRoot, gh, operation, source);
|
|
9851
10144
|
}
|
|
9852
10145
|
}
|
|
9853
|
-
function issueTerminalEventId(body) {
|
|
9854
|
-
const suffixMatch = [...body.matchAll(/<!-- immune-brain:terminal-event=([A-Za-z0-9._:-]+) -->/g)];
|
|
9855
|
-
if (suffixMatch.length === 0)
|
|
9856
|
-
return null;
|
|
9857
|
-
if (suffixMatch.length > 1)
|
|
9858
|
-
return "multiple";
|
|
9859
|
-
const eventId = suffixMatch[0][1];
|
|
9860
|
-
if (eventId.length > MAX_TERMINAL_EVENT_ID || !body.endsWith(terminalSuffix(eventId)))
|
|
9861
|
-
return "malformed";
|
|
9862
|
-
return eventId;
|
|
9863
|
-
}
|
|
9864
|
-
function stripTerminalSuffixFromBytes(body) {
|
|
9865
|
-
if (body === undefined)
|
|
9866
|
-
return null;
|
|
9867
|
-
const eventId = issueTerminalEventId(body);
|
|
9868
|
-
if (eventId === null || eventId === "multiple" || eventId === "malformed")
|
|
9869
|
-
return eventId;
|
|
9870
|
-
const suffix = terminalSuffix(eventId);
|
|
9871
|
-
if (!body.endsWith(suffix))
|
|
9872
|
-
return null;
|
|
9873
|
-
return body.slice(0, body.length - suffix.length);
|
|
9874
|
-
}
|
|
9875
|
-
function carriesApprovedContent(title, body, approved) {
|
|
9876
|
-
if (title !== approved.title)
|
|
9877
|
-
return false;
|
|
9878
|
-
if (body === approved.body)
|
|
9879
|
-
return true;
|
|
9880
|
-
const stripped = stripTerminalSuffixFromBytes(body);
|
|
9881
|
-
return typeof stripped === "string" && stripped.trimEnd() === approved.body.trimEnd();
|
|
9882
|
-
}
|
|
9883
|
-
async function updatePendingChild(root, gh, source, op, child, boundNumber, desiredBlockers, approvedFinal, amendmentContext = undefined) {
|
|
9884
|
-
const parent = initiativeLookup(source.issues, source.repository.id, op.initiative_id);
|
|
9885
|
-
const body = childBody(source.repository, op, parent.kind === "found" ? parent.issue : child);
|
|
9886
|
-
const oversized = bodyLimitFailure(op.op, body, MAX_TERMINAL_SUFFIX_BYTES);
|
|
9887
|
-
if (oversized)
|
|
9888
|
-
return oversized;
|
|
9889
|
-
const title = taskIssueTitle(op, sliceOrdinalFromChecklist(parent.kind === "found" ? parent.issue.body : child.body, op.slice_id, op.projection?.slice_ordinal ?? 1));
|
|
9890
|
-
const parentApprovedContent = amendmentContext?.parent;
|
|
9891
|
-
const parentBoundNumber = amendmentContext?.parentIssueNumber;
|
|
9892
|
-
const baseBody = child.body;
|
|
9893
|
-
const suffixEvent = issueTerminalEventId(baseBody);
|
|
9894
|
-
if (suffixEvent === "multiple")
|
|
9895
|
-
return result(op.op, "ambiguous_remote_state", "pending Task has multiple terminal markers", child);
|
|
9896
|
-
if (suffixEvent === "malformed")
|
|
9897
|
-
return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a malformed terminal marker (marker without its exact canonical suffix)`, child);
|
|
9898
|
-
let finalBody = suffixEvent !== null ? `${body.trimEnd()}${terminalSuffix(suffixEvent)}` : body;
|
|
9899
|
-
const approvedMatches = approvedFinal !== undefined && carriesApprovedContent(title, body, approvedFinal);
|
|
9900
|
-
if (!approvedMatches)
|
|
9901
|
-
return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a terminal suffix that diverges from the approved amendment content`, child);
|
|
9902
|
-
const approvedNow = approvedFinal !== undefined && carriesApprovedContent(child.title, child.body, approvedFinal);
|
|
9903
|
-
if (approvedNow) {
|
|
9904
|
-
const currentParent = initiativeLookup(source.issues, source.repository.id, op.initiative_id);
|
|
9905
|
-
if (currentParent.kind !== "found")
|
|
9906
|
-
return result(op.op, "ambiguous_remote_state", "pending Task Parent is not observable before attachment convergence", child);
|
|
9907
|
-
const revalidated = await revalidatePendingChildBeforeWrite(root, gh, child.number, op.task_id, approvedFinal, undefined, true, parentApprovedContent, parentBoundNumber);
|
|
9908
|
-
if ("contract" in revalidated)
|
|
9909
|
-
return revalidated;
|
|
9910
|
-
const targetParentNumber = parentBoundNumber ?? currentParent.issue.number;
|
|
9911
|
-
const attachment = await confirmAttachment(root, gh, op.op, source.repository, targetParentNumber, child.number);
|
|
9912
|
-
if (!("attached" in attachment))
|
|
9913
|
-
return attachment;
|
|
9914
|
-
if (!attachment.attached) {
|
|
9915
|
-
const attached = await attachSubIssue(root, gh, op.op, source.repository, targetParentNumber, child);
|
|
9916
|
-
if (!("attached" in attached))
|
|
9917
|
-
return attached;
|
|
9918
|
-
}
|
|
9919
|
-
}
|
|
9920
|
-
let observed = child;
|
|
9921
|
-
const desiredLabels = desiredTaskLabels(op);
|
|
9922
|
-
if (child.title !== title || child.body !== finalBody || labelMutationArgs(child.labels, desiredLabels).length) {
|
|
9923
|
-
const revalidated = await revalidatePendingChildBeforeWrite(root, gh, child.number, op.task_id, approvedFinal, { title: child.title, body: child.body }, false, parentApprovedContent, parentBoundNumber);
|
|
9924
|
-
if ("contract" in revalidated)
|
|
9925
|
-
return revalidated;
|
|
9926
|
-
observed = revalidated;
|
|
9927
|
-
const observedEvent = issueTerminalEventId(observed.body);
|
|
9928
|
-
if (observedEvent === "malformed")
|
|
9929
|
-
return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} carries a malformed terminal marker (marker without its exact canonical suffix)`, child);
|
|
9930
|
-
const writeBody = typeof observedEvent === "string" ? `${body.trimEnd()}${terminalSuffix(observedEvent)}` : finalBody;
|
|
9931
|
-
const labelArgs = labelMutationArgs(observed.labels, desiredLabels);
|
|
9932
|
-
if (observed.title !== title || observed.body !== writeBody || labelArgs.length) {
|
|
9933
|
-
const edited = await gh.run([
|
|
9934
|
-
"issue",
|
|
9935
|
-
"edit",
|
|
9936
|
-
String(child.number),
|
|
9937
|
-
"--repo",
|
|
9938
|
-
source.repository.name_with_owner,
|
|
9939
|
-
"--title",
|
|
9940
|
-
title,
|
|
9941
|
-
"--body-file",
|
|
9942
|
-
"-",
|
|
9943
|
-
...labelArgs
|
|
9944
|
-
], { cwd: root, stdin: writeBody });
|
|
9945
|
-
if (edited.exit_code !== 0 || edited.output_exceeded)
|
|
9946
|
-
return ghFailure(op.op, edited, `pending Task Issue #${child.number} update failed`);
|
|
9947
|
-
finalBody = writeBody;
|
|
9948
|
-
}
|
|
9949
|
-
}
|
|
9950
|
-
const dependencies = await convergePendingDependencies(root, gh, source, child.number, op.task_id, desiredBlockers, approvedFinal, parentApprovedContent, parentBoundNumber);
|
|
9951
|
-
if (!("complete" in dependencies))
|
|
9952
|
-
return dependencies;
|
|
9953
|
-
const refreshed = await snapshot(root, gh, op.op);
|
|
9954
|
-
if ("contract" in refreshed)
|
|
9955
|
-
return refreshed;
|
|
9956
|
-
const reread = ownedTaskLookup(refreshed.issues, refreshed.repository.id, op.task_id, op.initiative_id, op.slice_id);
|
|
9957
|
-
if (reread.kind !== "found" || reread.issue.number !== child.number)
|
|
9958
|
-
return result(op.op, "ambiguous_remote_state", `pending Task ${op.task_id} changed identity during amendment`, child);
|
|
9959
|
-
if (reread.issue.title !== title || reread.issue.body !== finalBody)
|
|
9960
|
-
return result(op.op, "retryable_failure", `pending Task ${op.task_id} update did not converge`, reread.issue);
|
|
9961
|
-
const labelsCurrent = !desiredLabels.some((label) => !reread.issue.labels.includes(label));
|
|
9962
|
-
if (!labelsCurrent)
|
|
9963
|
-
return result(op.op, "retryable_failure", `pending Task ${op.task_id} labels did not converge`, reread.issue);
|
|
9964
|
-
const currentDependencies = await confirmBlockedBy(root, gh, op.op, refreshed.repository, reread.issue.number, desiredBlockers);
|
|
9965
|
-
if (!("complete" in currentDependencies))
|
|
9966
|
-
return currentDependencies;
|
|
9967
|
-
if (!currentDependencies.complete)
|
|
9968
|
-
return result(op.op, "retryable_failure", `pending Task ${op.task_id} dependencies did not converge`, reread.issue);
|
|
9969
|
-
const contentCurrent = child.title === title && child.body === finalBody && labelMutationArgs(child.labels, desiredLabels).length === 0;
|
|
9970
|
-
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);
|
|
9971
|
-
}
|
|
9972
|
-
async function convergePendingDependencies(root, gh, source, childNumber, childTaskId, requestedBlockers, approvedFinal, parentApprovedContent = undefined, parentBoundNumber = undefined) {
|
|
9973
|
-
const expected = requestedBlockers.map((blocker) => blocker.id);
|
|
9974
|
-
const existing = await readBlockedByIds(root, gh, "upsert-task", source.repository, childNumber);
|
|
9975
|
-
if (!Array.isArray(existing))
|
|
9976
|
-
return existing;
|
|
9977
|
-
const removed = existing.filter((id) => !expected.includes(id));
|
|
9978
|
-
for (const id of removed) {
|
|
9979
|
-
const revalidated = await revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, undefined, false, parentApprovedContent, parentBoundNumber);
|
|
9980
|
-
if ("contract" in revalidated)
|
|
9981
|
-
return revalidated;
|
|
9982
|
-
const mutation = await gh.run([
|
|
9983
|
-
"api",
|
|
9984
|
-
"--method",
|
|
9985
|
-
"DELETE",
|
|
9986
|
-
`repos/${source.repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by/${id}`
|
|
9987
|
-
], { cwd: root });
|
|
9988
|
-
if (mutation.exit_code !== 0 || mutation.output_exceeded)
|
|
9989
|
-
return ghFailure("upsert-task", mutation, `native blocked_by removal failed for Issue #${childNumber}`);
|
|
9990
|
-
}
|
|
9991
|
-
const additions = requestedBlockers.filter((blocker) => !existing.includes(blocker.id));
|
|
9992
|
-
for (const blocker of additions) {
|
|
9993
|
-
const revalidated = await revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, undefined, false, parentApprovedContent, parentBoundNumber);
|
|
9994
|
-
if ("contract" in revalidated)
|
|
9995
|
-
return revalidated;
|
|
9996
|
-
const mutation = await gh.run([
|
|
9997
|
-
"api",
|
|
9998
|
-
"-F",
|
|
9999
|
-
`issue_id=${blocker.id}`,
|
|
10000
|
-
`repos/${source.repository.name_with_owner}/issues/${childNumber}/dependencies/blocked_by`
|
|
10001
|
-
], { cwd: root });
|
|
10002
|
-
if (mutation.exit_code !== 0 || mutation.output_exceeded)
|
|
10003
|
-
return ghFailure("upsert-task", mutation, `native blocked_by attachment failed for Issue #${blocker.number}`);
|
|
10004
|
-
}
|
|
10005
|
-
const confirm = await confirmBlockedBy(root, gh, "upsert-task", source.repository, childNumber, requestedBlockers);
|
|
10006
|
-
if (!("complete" in confirm))
|
|
10007
|
-
return confirm;
|
|
10008
|
-
return confirm.complete ? { complete: true } : { complete: true };
|
|
10009
|
-
}
|
|
10010
|
-
async function revalidatePendingChildBeforeWrite(root, gh, childNumber, childTaskId, approvedFinal, allowedBaseline = undefined, skipAttachmentCheck = false, parentApproved = undefined, parentExpectedNumber = undefined) {
|
|
10011
|
-
const refreshed = await snapshot(root, gh, "upsert-task");
|
|
10012
|
-
if ("contract" in refreshed)
|
|
10013
|
-
return refreshed;
|
|
10014
|
-
const resolved = taskLookup(refreshed.issues, refreshed.repository.id, childTaskId);
|
|
10015
|
-
if (resolved.kind !== "found")
|
|
10016
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} is not uniquely owned before a write: ${resolved.kind}`);
|
|
10017
|
-
const child = resolved.issue;
|
|
10018
|
-
if (child.number !== childNumber)
|
|
10019
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} resolves to Issue #${child.number}, not the bound Issue #${childNumber}`, child);
|
|
10020
|
-
if (child.state !== "open")
|
|
10021
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) is no longer open before a dependency write`, child);
|
|
10022
|
-
const contentFinal = approvedFinal !== undefined && carriesApprovedContent(child.title, child.body, approvedFinal);
|
|
10023
|
-
const contentBaseline = allowedBaseline !== undefined && child.title === allowedBaseline.title && child.body === allowedBaseline.body;
|
|
10024
|
-
if (!contentFinal && !contentBaseline)
|
|
10025
|
-
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);
|
|
10026
|
-
const initiativeId = [...child.body.matchAll(/<!-- immune-brain:initiative-id=([A-Za-z0-9._:-]+) -->/g)].map((match) => match[1])[0];
|
|
10027
|
-
if (!initiativeId)
|
|
10028
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its Initiative marker before a dependency write`, child);
|
|
10029
|
-
const parent = initiativeLookup(refreshed.issues, refreshed.repository.id, initiativeId);
|
|
10030
|
-
if (parent.kind !== "found" || parent.issue.state !== "open")
|
|
10031
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its open Parent before a dependency write`, child);
|
|
10032
|
-
if (parentExpectedNumber !== undefined && parent.issue.number !== parentExpectedNumber)
|
|
10033
|
-
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);
|
|
10034
|
-
if (parentApproved !== undefined && (parent.issue.title !== parentApproved.title || parent.issue.body !== parentApproved.body))
|
|
10035
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) Parent changed since the approved amendment content before a dependency write`, child);
|
|
10036
|
-
if (!skipAttachmentCheck) {
|
|
10037
|
-
const attachedNow = await readSubIssueNumbers(root, gh, "upsert-task", refreshed.repository, parent.issue.number);
|
|
10038
|
-
if (!Array.isArray(attachedNow))
|
|
10039
|
-
return attachedNow;
|
|
10040
|
-
if (!attachedNow.includes(childNumber))
|
|
10041
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) is no longer attached to the Parent before a dependency write`, child);
|
|
10042
|
-
}
|
|
10043
|
-
const sliceId = ownershipMarkerValue(child.body, "slice-id");
|
|
10044
|
-
if (sliceId && sliceCount(parent.issue.body, sliceId) !== 1)
|
|
10045
|
-
return result("upsert-task", "ambiguous_remote_state", `pending Task ${childTaskId} (Issue #${childNumber}) lost its exact Slice in the Parent before a dependency write`, child);
|
|
10046
|
-
return child;
|
|
10047
|
-
}
|
|
10048
10146
|
|
|
10049
10147
|
// plugins/immune-brain/runtime/unattended/batch_preflight.ts
|
|
10050
10148
|
import { existsSync as existsSync7, readdirSync as readdirSync6, readFileSync as readFileSync13 } from "node:fs";
|
|
10051
10149
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
10052
10150
|
import { join as join12 } from "node:path";
|
|
10053
|
-
import { spawnSync as
|
|
10054
|
-
|
|
10055
|
-
// plugins/immune-brain/runtime/kernel/batch_authority.ts
|
|
10056
|
-
import { createHash as createHash18 } from "node:crypto";
|
|
10057
|
-
var BATCH_AUTHORITY_CAPABILITY_BRAND = Symbol.for("assurance-kernel.batch-authority-capability-brand");
|
|
10058
|
-
var GIT_COMMIT_ID2 = /^[a-f0-9]{40}$/;
|
|
10059
|
-
|
|
10060
|
-
class BatchAuthorizationExpiryError extends Error {
|
|
10061
|
-
constructor(message) {
|
|
10062
|
-
super(message);
|
|
10063
|
-
this.name = "BatchAuthorizationExpiryError";
|
|
10064
|
-
}
|
|
10065
|
-
}
|
|
10066
|
-
function sha256Hex3(bytes) {
|
|
10067
|
-
return createHash18("sha256").update(bytes).digest("hex");
|
|
10068
|
-
}
|
|
10069
|
-
function stableStringify3(value) {
|
|
10070
|
-
if (value === null || typeof value !== "object")
|
|
10071
|
-
return JSON.stringify(value);
|
|
10072
|
-
if (Array.isArray(value))
|
|
10073
|
-
return `[${value.map((entry) => stableStringify3(entry)).join(",")}]`;
|
|
10074
|
-
const record = value;
|
|
10075
|
-
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableStringify3(record[key])}`).join(",")}}`;
|
|
10076
|
-
}
|
|
10077
|
-
function computeBatchPlanDigest(children) {
|
|
10078
|
-
const canonical = children.map((child) => ({
|
|
10079
|
-
blocked_by: [...child.blocked_by],
|
|
10080
|
-
intent_content_hash: child.intent_content_hash,
|
|
10081
|
-
intent_path: child.intent_path,
|
|
10082
|
-
intent_revision: child.intent_revision,
|
|
10083
|
-
task_id: child.task_id
|
|
10084
|
-
}));
|
|
10085
|
-
return `sha256:${sha256Hex3(stableStringify3(canonical))}`;
|
|
10086
|
-
}
|
|
10087
|
-
function requireNonEmpty(binding) {
|
|
10088
|
-
const missing = [];
|
|
10089
|
-
for (const key of [
|
|
10090
|
-
"batch_id",
|
|
10091
|
-
"initiative_slug",
|
|
10092
|
-
"plan_digest",
|
|
10093
|
-
"branch",
|
|
10094
|
-
"base_head",
|
|
10095
|
-
"actor_id",
|
|
10096
|
-
"confirmation_ref",
|
|
10097
|
-
"expires_at",
|
|
10098
|
-
"nonce"
|
|
10099
|
-
]) {
|
|
10100
|
-
const value = binding[key];
|
|
10101
|
-
if (value === undefined || value === null || value === "")
|
|
10102
|
-
missing.push(key);
|
|
10103
|
-
}
|
|
10104
|
-
if (!binding.budget || typeof binding.budget !== "object")
|
|
10105
|
-
missing.push("budget");
|
|
10106
|
-
if (missing.length > 0)
|
|
10107
|
-
throw new Error(`batch authorization binding is incomplete: ${missing.join(", ")}`);
|
|
10108
|
-
}
|
|
10109
|
-
function validateBudget(budget, issuedAt) {
|
|
10110
|
-
if (!Number.isInteger(budget.max_children) || budget.max_children <= 0)
|
|
10111
|
-
throw new Error("batch budget max_children must be a positive integer");
|
|
10112
|
-
if (!Number.isInteger(budget.qa_failure_limit) || budget.qa_failure_limit <= 0)
|
|
10113
|
-
throw new Error("batch budget qa_failure_limit must be a positive integer");
|
|
10114
|
-
const deadline = Date.parse(budget.deadline_at);
|
|
10115
|
-
if (Number.isNaN(deadline) || deadline <= Date.parse(issuedAt))
|
|
10116
|
-
throw new Error("batch budget must have a future deadline_at");
|
|
10117
|
-
}
|
|
10118
|
-
function validateChildren(children, planDigest) {
|
|
10119
|
-
if (!Array.isArray(children) || children.length === 0)
|
|
10120
|
-
throw new Error("batch authorization requires a non-empty child plan");
|
|
10121
|
-
const seen = new Set;
|
|
10122
|
-
for (const child of children) {
|
|
10123
|
-
if (!child || typeof child !== "object")
|
|
10124
|
-
throw new Error("batch plan child must be an object");
|
|
10125
|
-
for (const key of ["task_id", "intent_path", "intent_content_hash"]) {
|
|
10126
|
-
if (typeof child[key] !== "string" || child[key] === "")
|
|
10127
|
-
throw new Error(`batch plan child ${key} must be a non-empty string`);
|
|
10128
|
-
}
|
|
10129
|
-
if (!Number.isInteger(child.intent_revision) || child.intent_revision <= 0)
|
|
10130
|
-
throw new Error("batch plan child intent_revision must be a positive integer");
|
|
10131
|
-
if (!Array.isArray(child.blocked_by) || child.blocked_by.some((id) => typeof id !== "string" || id === ""))
|
|
10132
|
-
throw new Error("batch plan child blocked_by must be an array of task ids");
|
|
10133
|
-
if (seen.has(child.task_id))
|
|
10134
|
-
throw new Error(`batch plan child ${child.task_id} appears more than once`);
|
|
10135
|
-
seen.add(child.task_id);
|
|
10136
|
-
}
|
|
10137
|
-
for (const child of children) {
|
|
10138
|
-
for (const blocker of child.blocked_by) {
|
|
10139
|
-
if (!seen.has(blocker))
|
|
10140
|
-
throw new Error(`batch plan child ${child.task_id} is blocked by ${blocker}, which is not in the confirmed plan`);
|
|
10141
|
-
}
|
|
10142
|
-
}
|
|
10143
|
-
const dependencies = new Map(children.map((child) => [child.task_id, child.blocked_by]));
|
|
10144
|
-
const visiting = new Set;
|
|
10145
|
-
const visited = new Set;
|
|
10146
|
-
function visit(taskId) {
|
|
10147
|
-
if (visiting.has(taskId))
|
|
10148
|
-
throw new Error(`batch plan dependency cycle at ${taskId}`);
|
|
10149
|
-
if (visited.has(taskId))
|
|
10150
|
-
return;
|
|
10151
|
-
visiting.add(taskId);
|
|
10152
|
-
for (const blocker of dependencies.get(taskId))
|
|
10153
|
-
visit(blocker);
|
|
10154
|
-
visiting.delete(taskId);
|
|
10155
|
-
visited.add(taskId);
|
|
10156
|
-
}
|
|
10157
|
-
for (const child of children)
|
|
10158
|
-
visit(child.task_id);
|
|
10159
|
-
if (computeBatchPlanDigest(children) !== planDigest)
|
|
10160
|
-
throw new Error("batch plan digest does not match the confirmed child plan");
|
|
10161
|
-
}
|
|
10162
|
-
function createBatchAuthorityRegistry() {
|
|
10163
|
-
const inner = createCapabilityRegistry(BATCH_AUTHORITY_CAPABILITY_BRAND, {
|
|
10164
|
-
validateBinding(binding, issuedAt) {
|
|
10165
|
-
requireNonEmpty(binding);
|
|
10166
|
-
if (!isLiteralUserActor(binding.actor_id))
|
|
10167
|
-
throw new Error("batch authorization requires a literal-user actor_id");
|
|
10168
|
-
if (!GIT_COMMIT_ID2.test(binding.base_head))
|
|
10169
|
-
throw new Error("batch authorization base_head must be a committed 40-hex commit id");
|
|
10170
|
-
const expires = Date.parse(binding.expires_at);
|
|
10171
|
-
if (Number.isNaN(expires) || expires <= Date.parse(issuedAt))
|
|
10172
|
-
throw new Error("batch authorization must have a future expiry");
|
|
10173
|
-
validateBudget(binding.budget, issuedAt);
|
|
10174
|
-
},
|
|
10175
|
-
validateAndProject(state, expected, now) {
|
|
10176
|
-
if (!Number.isFinite(now))
|
|
10177
|
-
throw new Error("batch authorization requires a valid clock");
|
|
10178
|
-
const expires = Date.parse(state.expires_at);
|
|
10179
|
-
if (Number.isNaN(expires) || expires <= now)
|
|
10180
|
-
throw new BatchAuthorizationExpiryError("batch authorization has expired");
|
|
10181
|
-
if (Date.parse(state.budget.deadline_at) <= now)
|
|
10182
|
-
throw new BatchAuthorizationExpiryError("batch authorization deadline has expired");
|
|
10183
|
-
for (const key of Object.keys(expected)) {
|
|
10184
|
-
if (key === "budget") {
|
|
10185
|
-
const a = state.budget ?? {};
|
|
10186
|
-
const b = expected.budget ?? {};
|
|
10187
|
-
if (a.max_children !== b.max_children || a.deadline_at !== b.deadline_at || a.qa_failure_limit !== b.qa_failure_limit)
|
|
10188
|
-
throw new Error("batch authorization budget mismatch");
|
|
10189
|
-
continue;
|
|
10190
|
-
}
|
|
10191
|
-
if (state[key] !== expected[key])
|
|
10192
|
-
throw new Error(`batch authorization ${key} mismatch`);
|
|
10193
|
-
}
|
|
10194
|
-
return {
|
|
10195
|
-
batch_id: state.batch_id,
|
|
10196
|
-
initiative_slug: state.initiative_slug,
|
|
10197
|
-
plan_digest: state.plan_digest,
|
|
10198
|
-
branch: state.branch,
|
|
10199
|
-
base_head: state.base_head,
|
|
10200
|
-
budget: { ...state.budget },
|
|
10201
|
-
actor_id: state.actor_id,
|
|
10202
|
-
confirmation_ref: state.confirmation_ref,
|
|
10203
|
-
issued_at: state.issued_at,
|
|
10204
|
-
expires_at: state.expires_at,
|
|
10205
|
-
nonce: state.nonce
|
|
10206
|
-
};
|
|
10207
|
-
}
|
|
10208
|
-
}, "batch authorization");
|
|
10209
|
-
const plans = new WeakMap;
|
|
10210
|
-
const consumed = new WeakMap;
|
|
10211
|
-
function planOf(capability) {
|
|
10212
|
-
const plan = plans.get(capability);
|
|
10213
|
-
if (!plan)
|
|
10214
|
-
throw new Error("batch authorization capability is not recognized by this registry");
|
|
10215
|
-
return plan;
|
|
10216
|
-
}
|
|
10217
|
-
function slotsOf(capability) {
|
|
10218
|
-
const slots = consumed.get(capability);
|
|
10219
|
-
if (!slots)
|
|
10220
|
-
throw new Error("batch authorization capability is not recognized by this registry");
|
|
10221
|
-
return slots;
|
|
10222
|
-
}
|
|
10223
|
-
return {
|
|
10224
|
-
brand: inner.brand,
|
|
10225
|
-
issue(binding, children, issuedAt = new Date().toISOString()) {
|
|
10226
|
-
requireNonEmpty(binding);
|
|
10227
|
-
validateChildren(children, binding.plan_digest);
|
|
10228
|
-
const capability = inner.issue(binding, issuedAt);
|
|
10229
|
-
plans.set(capability, children.map((child) => ({ ...child, blocked_by: [...child.blocked_by] })));
|
|
10230
|
-
consumed.set(capability, new Set);
|
|
10231
|
-
return capability;
|
|
10232
|
-
},
|
|
10233
|
-
inspect(capability, expected, now = Date.now()) {
|
|
10234
|
-
planOf(capability);
|
|
10235
|
-
return inner.inspect(capability, expected, now);
|
|
10236
|
-
},
|
|
10237
|
-
children(capability) {
|
|
10238
|
-
return planOf(capability).map((child) => ({ ...child, blocked_by: [...child.blocked_by] }));
|
|
10239
|
-
},
|
|
10240
|
-
consumedChildren(capability) {
|
|
10241
|
-
return [...slotsOf(capability)];
|
|
10242
|
-
},
|
|
10243
|
-
isChildConsumed(capability, taskId) {
|
|
10244
|
-
return slotsOf(capability).has(taskId);
|
|
10245
|
-
},
|
|
10246
|
-
consumeChild(capability, expected, taskId, now = Date.now()) {
|
|
10247
|
-
const validated = this.inspect(capability, expected, now);
|
|
10248
|
-
const plan = planOf(capability);
|
|
10249
|
-
if (!plan.some((child) => child.task_id === taskId))
|
|
10250
|
-
throw new Error(`batch_child_not_in_plan: ${taskId}`);
|
|
10251
|
-
const slots = slotsOf(capability);
|
|
10252
|
-
if (slots.has(taskId))
|
|
10253
|
-
throw new Error(`batch_child_slot_consumed: ${taskId}`);
|
|
10254
|
-
slots.add(taskId);
|
|
10255
|
-
return validated;
|
|
10256
|
-
},
|
|
10257
|
-
releaseChild(capability, taskId) {
|
|
10258
|
-
slotsOf(capability).delete(taskId);
|
|
10259
|
-
},
|
|
10260
|
-
isExhausted(capability) {
|
|
10261
|
-
return slotsOf(capability).size >= planOf(capability).length;
|
|
10262
|
-
}
|
|
10263
|
-
};
|
|
10264
|
-
}
|
|
10265
|
-
function deriveChildEnrollment(root, registry, input) {
|
|
10266
|
-
const validated = registry.inspect(input.capability, input.binding, Date.parse(input.now));
|
|
10267
|
-
const child = registry.children(input.capability).find((entry) => entry.task_id === input.task_id);
|
|
10268
|
-
if (!child)
|
|
10269
|
-
throw new Error(`batch_child_not_in_plan: ${input.task_id}`);
|
|
10270
|
-
if (registry.isChildConsumed(input.capability, input.task_id))
|
|
10271
|
-
throw new Error(`batch_child_slot_consumed: ${input.task_id}`);
|
|
10272
|
-
if (!GIT_COMMIT_ID2.test(input.expected_head))
|
|
10273
|
-
throw new Error("batch_head_lineage_broken: expected_head is not a commit id");
|
|
10274
|
-
const preparation = preparePiCanary(root, { task_id: input.task_id, now: input.now });
|
|
10275
|
-
if (!preparation.git_base_head)
|
|
10276
|
-
throw new Error(preparation.git_error ?? "enrollment requires a committed Git HEAD");
|
|
10277
|
-
if (preparation.git_base_head !== input.expected_head)
|
|
10278
|
-
throw new Error(`batch_head_lineage_broken: expected ${input.expected_head}, found ${preparation.git_base_head}`);
|
|
10279
|
-
if (!preparation.intent)
|
|
10280
|
-
throw new Error(`batch_child_intent_changed: ${input.task_id} intent sidecar is unreadable`);
|
|
10281
|
-
if (preparation.intent.path !== child.intent_path || preparation.intent.revision !== child.intent_revision || preparation.intent.content_hash !== child.intent_content_hash)
|
|
10282
|
-
throw new Error(`batch_child_intent_changed: ${input.task_id}`);
|
|
10283
|
-
if (registry.consumedChildren(input.capability).length === 0 && input.expected_head !== validated.base_head)
|
|
10284
|
-
throw new Error(`batch_head_lineage_broken: the first child must enroll on the confirmed base_head ${validated.base_head}, not ${input.expected_head}`);
|
|
10285
|
-
return {
|
|
10286
|
-
child,
|
|
10287
|
-
preparation,
|
|
10288
|
-
binding: {
|
|
10289
|
-
task_id: child.task_id,
|
|
10290
|
-
intent_path: child.intent_path,
|
|
10291
|
-
intent_revision: child.intent_revision,
|
|
10292
|
-
intent_content_hash: child.intent_content_hash,
|
|
10293
|
-
preparation_digest: preparation.digest,
|
|
10294
|
-
actor_id: validated.actor_id,
|
|
10295
|
-
confirmation_ref: validated.confirmation_ref,
|
|
10296
|
-
expires_at: validated.expires_at,
|
|
10297
|
-
nonce: `${validated.nonce}:${child.task_id}`
|
|
10298
|
-
}
|
|
10299
|
-
};
|
|
10300
|
-
}
|
|
10151
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
10301
10152
|
|
|
10302
10153
|
// plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts
|
|
10303
|
-
import { spawnSync as
|
|
10154
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
10304
10155
|
|
|
10305
10156
|
// plugins/immune-brain/runtime/unattended/batch_state.ts
|
|
10306
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";
|
|
@@ -10339,8 +10190,13 @@ function statePath(batchId) {
|
|
|
10339
10190
|
validateBatchId(batchId);
|
|
10340
10191
|
return join11(".imm", "state", "batches", `${batchId}.json`);
|
|
10341
10192
|
}
|
|
10193
|
+
function withoutRetiredClock(record) {
|
|
10194
|
+
const { authorization_expires_at: _expiry, ...rest } = record;
|
|
10195
|
+
const { deadline_at: _deadline, ...budget } = rest.budget;
|
|
10196
|
+
return { ...rest, budget };
|
|
10197
|
+
}
|
|
10342
10198
|
function canonicalBytes(record) {
|
|
10343
|
-
return `${JSON.stringify(record, null, 2)}
|
|
10199
|
+
return `${JSON.stringify(withoutRetiredClock(record), null, 2)}
|
|
10344
10200
|
`;
|
|
10345
10201
|
}
|
|
10346
10202
|
function validateRecordShape(value, batchId) {
|
|
@@ -10359,8 +10215,6 @@ function validateRecordShape(value, batchId) {
|
|
|
10359
10215
|
throw new Error(`batch run state ${batchId} has an invalid branch`);
|
|
10360
10216
|
if (!isCanonicalTimestamp(record.confirmation_time))
|
|
10361
10217
|
throw new Error(`batch run state ${batchId} has an invalid confirmation_time`);
|
|
10362
|
-
if (!isCanonicalTimestamp(record.authorization_expires_at))
|
|
10363
|
-
throw new Error(`batch run state ${batchId} has an invalid authorization_expires_at`);
|
|
10364
10218
|
if (!isCanonicalTimestamp(record.created_at) || !isCanonicalTimestamp(record.updated_at))
|
|
10365
10219
|
throw new Error(`batch run state ${batchId} has invalid state timestamps`);
|
|
10366
10220
|
if (!Array.isArray(record.children) || record.children.length === 0)
|
|
@@ -10370,10 +10224,12 @@ function validateRecordShape(value, batchId) {
|
|
|
10370
10224
|
if (typeof record.consecutive_qa_failures !== "number" || !Number.isInteger(record.consecutive_qa_failures) || record.consecutive_qa_failures < 0)
|
|
10371
10225
|
throw new Error(`batch run state ${batchId} has an invalid consecutive_qa_failures`);
|
|
10372
10226
|
const budget = record.budget;
|
|
10373
|
-
if (typeof record.budget !== "object" || record.budget === null || typeof budget.max_children !== "number" || !Number.isInteger(budget.max_children) || budget.max_children <= 0 ||
|
|
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)
|
|
10374
10228
|
throw new Error(`batch run state ${batchId} has an invalid budget`);
|
|
10375
10229
|
if (!Array.isArray(record.commits) || record.commits.some((c) => typeof c !== "string"))
|
|
10376
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`);
|
|
10377
10233
|
const seenTaskIds = new Set;
|
|
10378
10234
|
for (const child of record.children) {
|
|
10379
10235
|
if (typeof child !== "object" || child === null || typeof child.task_id !== "string" || !child.task_id || typeof child.slice_id !== "string" || !child.slice_id)
|
|
@@ -10413,7 +10269,6 @@ function prepareBatchRunState(input) {
|
|
|
10413
10269
|
base_head: input.base_head,
|
|
10414
10270
|
branch: input.branch ?? `imm/${input.initiative_slug}`,
|
|
10415
10271
|
confirmation_time: input.confirmation_time,
|
|
10416
|
-
authorization_expires_at: input.authorization_expires_at,
|
|
10417
10272
|
budget: input.budget,
|
|
10418
10273
|
batch_state: "prepared",
|
|
10419
10274
|
children: input.children.map((child) => ({
|
|
@@ -10435,7 +10290,7 @@ function parseBatchRunState(raw, batchId) {
|
|
|
10435
10290
|
validateBatchId(batchId);
|
|
10436
10291
|
const parsed = JSON.parse(raw);
|
|
10437
10292
|
validateRecordShape(parsed, batchId);
|
|
10438
|
-
return parsed;
|
|
10293
|
+
return withoutRetiredClock(parsed);
|
|
10439
10294
|
}
|
|
10440
10295
|
function readBatchRunState(root, batchId) {
|
|
10441
10296
|
const path = statePath(batchId);
|
|
@@ -10488,7 +10343,7 @@ function replaceBatchRunState(root, expected, next, validate) {
|
|
|
10488
10343
|
if (!readSecureProjectBytes(root, path).equals(expected))
|
|
10489
10344
|
throw new Error("batch state CAS mismatch");
|
|
10490
10345
|
validate();
|
|
10491
|
-
const stored = { ...next, updated_at: new Date().toISOString() };
|
|
10346
|
+
const stored = withoutRetiredClock({ ...next, updated_at: new Date().toISOString() });
|
|
10492
10347
|
writeFileAtomically(root, path, canonicalBytes(stored));
|
|
10493
10348
|
return stored;
|
|
10494
10349
|
});
|
|
@@ -10500,10 +10355,10 @@ function writeBatchRunState(root, record) {
|
|
|
10500
10355
|
const existing = existsSync6(join11(root, path)) ? readSecureProjectFile(root, path) : null;
|
|
10501
10356
|
if (existing !== null && existing === canonicalBytes(record))
|
|
10502
10357
|
return record;
|
|
10503
|
-
const stored = {
|
|
10358
|
+
const stored = withoutRetiredClock({
|
|
10504
10359
|
...record,
|
|
10505
10360
|
updated_at: new Date().toISOString()
|
|
10506
|
-
};
|
|
10361
|
+
});
|
|
10507
10362
|
ensureSecureDirectory2(root, join11(".imm", "state", "batches"));
|
|
10508
10363
|
writeFileAtomically(root, path, canonicalBytes(stored));
|
|
10509
10364
|
return stored;
|
|
@@ -10552,7 +10407,7 @@ function matchesLocalProof(localJson, exported) {
|
|
|
10552
10407
|
return localJson !== null && JSON.stringify(Object.entries(JSON.parse(localJson)).sort()) === JSON.stringify(Object.entries(exported).sort());
|
|
10553
10408
|
}
|
|
10554
10409
|
function git4(root, args) {
|
|
10555
|
-
const r =
|
|
10410
|
+
const r = spawnSync6("git", ["-C", root, ...args], { encoding: "buffer", maxBuffer: 262144, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
|
|
10556
10411
|
if (r.status !== 0)
|
|
10557
10412
|
refuse();
|
|
10558
10413
|
return decode(r.stdout);
|
|
@@ -10601,20 +10456,17 @@ function ownUnpersistedBatchHead(root, record, head) {
|
|
|
10601
10456
|
}
|
|
10602
10457
|
}
|
|
10603
10458
|
var confirmations = new Map;
|
|
10604
|
-
function retainReconfirmation(nonce,
|
|
10605
|
-
for (const [key, value] of confirmations)
|
|
10606
|
-
if (value.expires <= Date.now())
|
|
10607
|
-
confirmations.delete(key);
|
|
10459
|
+
function retainReconfirmation(nonce, snapshot) {
|
|
10608
10460
|
if (confirmations.has(nonce))
|
|
10609
10461
|
refuse();
|
|
10610
|
-
confirmations.set(nonce,
|
|
10462
|
+
confirmations.set(nonce, snapshot);
|
|
10611
10463
|
}
|
|
10612
10464
|
function takeReconfirmation(nonce) {
|
|
10613
10465
|
const found = confirmations.get(nonce);
|
|
10614
10466
|
confirmations.delete(nonce);
|
|
10615
|
-
if (!found
|
|
10467
|
+
if (!found)
|
|
10616
10468
|
refuse();
|
|
10617
|
-
return found
|
|
10469
|
+
return found;
|
|
10618
10470
|
}
|
|
10619
10471
|
async function captureBatchReconfirmation(root, record, children) {
|
|
10620
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)
|
|
@@ -10759,7 +10611,6 @@ async function captureBatchReconfirmation(root, record, children) {
|
|
|
10759
10611
|
// plugins/immune-brain/runtime/unattended/batch_plan.ts
|
|
10760
10612
|
import { createHash as createHash19 } from "node:crypto";
|
|
10761
10613
|
var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10762
|
-
var DEFAULT_DEADLINE_MS = 8 * 60 * 60 * 1000;
|
|
10763
10614
|
var DEFAULT_QA_FAILURE_LIMIT = 2;
|
|
10764
10615
|
function specBindingReason(inspection) {
|
|
10765
10616
|
if (inspection.missing.length > 0)
|
|
@@ -10852,15 +10703,12 @@ function dependencyClosures(order) {
|
|
|
10852
10703
|
}
|
|
10853
10704
|
return closures;
|
|
10854
10705
|
}
|
|
10855
|
-
function budget(input, enrollableCount
|
|
10706
|
+
function budget(input, enrollableCount) {
|
|
10856
10707
|
const maxChildren = input.budget?.max_children === undefined ? positiveSafeInteger(enrollableCount, "budget.max_children") : positiveSafeInteger(input.budget.max_children, "budget.max_children");
|
|
10857
10708
|
if (maxChildren > enrollableCount)
|
|
10858
10709
|
throw new Error("budget.max_children exceeds the enrollable Child count");
|
|
10859
10710
|
const qaFailureLimit = input.budget?.qa_failure_limit === undefined ? DEFAULT_QA_FAILURE_LIMIT : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
|
|
10860
|
-
|
|
10861
|
-
if (Date.parse(deadline) <= confirmationTime.milliseconds)
|
|
10862
|
-
throw new Error("budget.deadline_at must be later than confirmation_time");
|
|
10863
|
-
return { max_children: maxChildren, deadline_at: deadline, qa_failure_limit: qaFailureLimit };
|
|
10711
|
+
return { max_children: maxChildren, qa_failure_limit: qaFailureLimit };
|
|
10864
10712
|
}
|
|
10865
10713
|
async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeGithubInitiative) {
|
|
10866
10714
|
if (!ID_PATTERN2.test(initiativeSlug))
|
|
@@ -10958,7 +10806,7 @@ async function projectBatchPlan(root, initiativeSlug, input, readInitiative = ob
|
|
|
10958
10806
|
children,
|
|
10959
10807
|
enrollable,
|
|
10960
10808
|
plan_digest: planDigest,
|
|
10961
|
-
budget: budget(input, enrollable.length
|
|
10809
|
+
budget: budget(input, enrollable.length)
|
|
10962
10810
|
};
|
|
10963
10811
|
}
|
|
10964
10812
|
|
|
@@ -10980,6 +10828,27 @@ function readActiveClaimTaskId(root) {
|
|
|
10980
10828
|
const claim = readBackendClaim(root);
|
|
10981
10829
|
return workspace.state.current_working || (claim?.lifecycle_status === "active" ? claim.task_id : null);
|
|
10982
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
|
+
}
|
|
10983
10852
|
function findExistingActiveBatch(root, initiativeSlug) {
|
|
10984
10853
|
const batchesDir = join12(root, ".imm", "state", "batches");
|
|
10985
10854
|
if (!existsSync7(batchesDir))
|
|
@@ -11051,9 +10920,51 @@ function findSettledBatchRecord(root, initiativeSlug) {
|
|
|
11051
10920
|
}
|
|
11052
10921
|
return newest;
|
|
11053
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
|
+
}
|
|
11054
10960
|
function expectedBatchHead(record) {
|
|
11055
10961
|
const commits = Array.isArray(record.commits) ? record.commits : [];
|
|
11056
|
-
|
|
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;
|
|
11057
10968
|
}
|
|
11058
10969
|
function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
|
|
11059
10970
|
let claim = null;
|
|
@@ -11067,7 +10978,7 @@ function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
|
|
|
11067
10978
|
const currentTaskId = workspaceOwner || (claim?.lifecycle_status === "active" ? claim?.task_id : null);
|
|
11068
10979
|
if (currentTaskId !== taskId || !claim)
|
|
11069
10980
|
return false;
|
|
11070
|
-
const branch =
|
|
10981
|
+
const branch = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" }).stdout.trim();
|
|
11071
10982
|
if (branch !== batchBranch)
|
|
11072
10983
|
return false;
|
|
11073
10984
|
const childInBatch = existingBatch.children.find((c) => c.task_id === taskId);
|
|
@@ -11083,7 +10994,7 @@ function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
|
|
|
11083
10994
|
}
|
|
11084
10995
|
if (!rec)
|
|
11085
10996
|
return false;
|
|
11086
|
-
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));
|
|
11087
10998
|
if (!lineageHeads.includes(rec.git_base_head))
|
|
11088
10999
|
return false;
|
|
11089
11000
|
if (claim.enrollment_event_id !== `enroll-${taskId}-${claim.created_at}`)
|
|
@@ -11124,7 +11035,7 @@ function authorizedScopeOf(root, taskId, state) {
|
|
|
11124
11035
|
return scope;
|
|
11125
11036
|
}
|
|
11126
11037
|
function porcelainEntries(root) {
|
|
11127
|
-
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" } });
|
|
11128
11039
|
if (statusProc.status !== 0)
|
|
11129
11040
|
return null;
|
|
11130
11041
|
const entries = [];
|
|
@@ -11143,9 +11054,10 @@ async function projectPlanSurface(input) {
|
|
|
11143
11054
|
const riskByTask = new Map;
|
|
11144
11055
|
let budget;
|
|
11145
11056
|
if (isResuming && existingBatch) {
|
|
11146
|
-
budget = {
|
|
11147
|
-
|
|
11148
|
-
|
|
11057
|
+
budget = {
|
|
11058
|
+
max_children: existingBatch.budget.max_children,
|
|
11059
|
+
qa_failure_limit: existingBatch.budget.qa_failure_limit
|
|
11060
|
+
};
|
|
11149
11061
|
try {
|
|
11150
11062
|
recoveryChildren = existingBatch.children.map((c) => {
|
|
11151
11063
|
const intentPath = `docs/plans/${c.task_id}.intent.json`;
|
|
@@ -11262,7 +11174,7 @@ async function projectBatchPreflight(options) {
|
|
|
11262
11174
|
} catch (err) {
|
|
11263
11175
|
return reject("git_head_unreadable", err instanceof Error ? err.message : String(err));
|
|
11264
11176
|
}
|
|
11265
|
-
const branchExists =
|
|
11177
|
+
const branchExists = spawnSync7("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${batchBranch}`]);
|
|
11266
11178
|
if (branchExists.status === 0 && !existingBatch)
|
|
11267
11179
|
return reject("branch_already_exists", batchBranch);
|
|
11268
11180
|
const statusEntries = porcelainEntries(root);
|
|
@@ -11373,37 +11285,37 @@ async function authorizeBatch(options) {
|
|
|
11373
11285
|
is_resuming: isResuming
|
|
11374
11286
|
} = projection;
|
|
11375
11287
|
const existingBatch = projection.existing_batch;
|
|
11376
|
-
const
|
|
11377
|
-
const branchBefore = spawnSync6("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
|
|
11288
|
+
const branchBefore = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
|
|
11378
11289
|
if (branchBefore.status !== 0)
|
|
11379
11290
|
return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
|
|
11380
11291
|
const ownsUnpersistedHead = isResuming && existingBatch !== null && existingBatch.plan_digest === planDigest && expectedBatchHead(existingBatch) !== baseHead && ownUnpersistedBatchHead(root, existingBatch, baseHead);
|
|
11381
|
-
|
|
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)
|
|
11382
11300
|
return { outcome: "rejected", rejection: batchRejection("head_moved", "first unpersisted commit provenance is invalid") };
|
|
11383
11301
|
const reuseBlockers = [];
|
|
11384
11302
|
if (isResuming && existingBatch) {
|
|
11385
|
-
if (isExistingExpired)
|
|
11386
|
-
reuseBlockers.push("batch_authorization_expired");
|
|
11387
|
-
if (Date.parse(existingBatch.budget.deadline_at) <= Date.now())
|
|
11388
|
-
reuseBlockers.push("batch_budget_expired");
|
|
11389
11303
|
if (existingBatch.batch_state !== "running")
|
|
11390
11304
|
reuseBlockers.push("batch_not_running");
|
|
11391
11305
|
if (existingBatch.plan_digest !== planDigest)
|
|
11392
11306
|
reuseBlockers.push("batch_plan_digest_changed");
|
|
11393
11307
|
if (existingBatch.branch !== batchBranch)
|
|
11394
11308
|
reuseBlockers.push("batch_branch_changed");
|
|
11395
|
-
if (expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead)
|
|
11309
|
+
if (expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead && !fastForwardsRecordedHead)
|
|
11396
11310
|
reuseBlockers.push("batch_head_lineage_moved");
|
|
11397
11311
|
}
|
|
11398
11312
|
const reuseAuthorization = isResuming && reuseBlockers.length === 0;
|
|
11399
|
-
const expiresAt = reuseAuthorization ? existingBatch.authorization_expires_at : budget.deadline_at;
|
|
11400
11313
|
const batchId = isResuming && existingBatch ? existingBatch.batch_id : `batch-${initiativeSlug}-${randomUUID7()}`;
|
|
11401
11314
|
const facts = {
|
|
11402
11315
|
initiative_slug: initiativeSlug,
|
|
11403
11316
|
batch_branch: batchBranch,
|
|
11404
11317
|
plan_digest: planDigest,
|
|
11405
11318
|
budget,
|
|
11406
|
-
expires_at: expiresAt,
|
|
11407
11319
|
reuse_blockers: reuseBlockers,
|
|
11408
11320
|
children: projection.recovery_children.map((child) => ({
|
|
11409
11321
|
task_id: child.task_id,
|
|
@@ -11445,11 +11357,9 @@ async function authorizeBatch(options) {
|
|
|
11445
11357
|
if (!current || current.corrupt || JSON.stringify(current.record) !== JSON.stringify(existingBatch))
|
|
11446
11358
|
return { outcome: "rejected", rejection: batchRejection("plan_changed") };
|
|
11447
11359
|
}
|
|
11448
|
-
const branchAfter =
|
|
11360
|
+
const branchAfter = spawnSync7("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
|
|
11449
11361
|
if (branchAfter.status !== 0 || branchAfter.stdout !== branchBefore.stdout)
|
|
11450
11362
|
return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "Git branch moved after native confirmation") };
|
|
11451
|
-
if (Date.parse(expiresAt) <= Date.now())
|
|
11452
|
-
return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "confirmed batch deadline expired before authorization issuance") };
|
|
11453
11363
|
if (projection.reconfirmation) {
|
|
11454
11364
|
try {
|
|
11455
11365
|
projection.reconfirmation.assertUnchanged();
|
|
@@ -11466,28 +11376,26 @@ async function authorizeBatch(options) {
|
|
|
11466
11376
|
budget,
|
|
11467
11377
|
actor_id: LITERAL_USER_ACTOR_ID,
|
|
11468
11378
|
confirmation_ref: options.confirmationRef({ batch_id: batchId, request_id: requestId }),
|
|
11469
|
-
expires_at: expiresAt,
|
|
11470
11379
|
nonce: options.nonce
|
|
11471
11380
|
};
|
|
11472
11381
|
if (projection.reconfirmation)
|
|
11473
|
-
retainReconfirmation(binding.nonce,
|
|
11382
|
+
retainReconfirmation(binding.nonce, projection.reconfirmation);
|
|
11474
11383
|
return {
|
|
11475
11384
|
outcome: "authorized",
|
|
11476
11385
|
batch_id: batchId,
|
|
11477
11386
|
reuse_authorization: reuseAuthorization,
|
|
11478
11387
|
reuse_blockers: reuseBlockers,
|
|
11479
|
-
expires_at: expiresAt,
|
|
11480
11388
|
binding
|
|
11481
11389
|
};
|
|
11482
11390
|
}
|
|
11483
11391
|
|
|
11484
11392
|
// plugins/immune-brain/runtime/unattended/batch_runner.ts
|
|
11485
|
-
import { spawnSync as
|
|
11393
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
11486
11394
|
import { existsSync as existsSync9 } from "node:fs";
|
|
11487
11395
|
import { join as join14 } from "node:path";
|
|
11488
11396
|
|
|
11489
11397
|
// plugins/immune-brain/runtime/unattended/batch_git.ts
|
|
11490
|
-
import { spawnSync as
|
|
11398
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
11491
11399
|
import { createHash as createHash20, randomUUID as randomUUID8 } from "node:crypto";
|
|
11492
11400
|
import {
|
|
11493
11401
|
constants as constants6,
|
|
@@ -11512,7 +11420,7 @@ var DEFAULT_GIT_ENV = {
|
|
|
11512
11420
|
function runBatchGitPreflight(input) {
|
|
11513
11421
|
const { root, initiative_slug: initiativeSlug, base_head: baseHead } = input;
|
|
11514
11422
|
const branch = `imm/${initiativeSlug}`;
|
|
11515
|
-
const toplevelResult =
|
|
11423
|
+
const toplevelResult = spawnSync8("git", ["-C", root, "rev-parse", "--show-toplevel"], {
|
|
11516
11424
|
encoding: "utf8",
|
|
11517
11425
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11518
11426
|
});
|
|
@@ -11550,7 +11458,7 @@ function runBatchGitPreflight(input) {
|
|
|
11550
11458
|
message: `unsupported index flags (assume-unchanged/skip-worktree) detected: ${flaggedPreflight.join(", ")}`
|
|
11551
11459
|
};
|
|
11552
11460
|
}
|
|
11553
|
-
const statusResult =
|
|
11461
|
+
const statusResult = spawnSync8("git", ["-C", root, "status", "--porcelain", "--untracked-files=all", "--ignore-submodules=none"], {
|
|
11554
11462
|
encoding: "utf8",
|
|
11555
11463
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11556
11464
|
});
|
|
@@ -11568,7 +11476,7 @@ function runBatchGitPreflight(input) {
|
|
|
11568
11476
|
message: "working tree is dirty before batch preflight"
|
|
11569
11477
|
};
|
|
11570
11478
|
}
|
|
11571
|
-
const headResult =
|
|
11479
|
+
const headResult = spawnSync8("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
|
|
11572
11480
|
encoding: "utf8",
|
|
11573
11481
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11574
11482
|
});
|
|
@@ -11579,11 +11487,11 @@ function runBatchGitPreflight(input) {
|
|
|
11579
11487
|
message: "HEAD is uncommitted or not a valid commit"
|
|
11580
11488
|
};
|
|
11581
11489
|
}
|
|
11582
|
-
const branchCheck =
|
|
11490
|
+
const branchCheck = spawnSync8("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { stdio: ["ignore", "ignore", "ignore"] });
|
|
11583
11491
|
if (branchCheck.status === 0) {
|
|
11584
11492
|
const settled = findSettledBatchRecord(root, initiativeSlug);
|
|
11585
|
-
const currentBranch =
|
|
11586
|
-
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)
|
|
11587
11495
|
return { ok: true, branch };
|
|
11588
11496
|
return {
|
|
11589
11497
|
ok: false,
|
|
@@ -11591,25 +11499,25 @@ function runBatchGitPreflight(input) {
|
|
|
11591
11499
|
message: `branch refs/heads/${branch} already exists`
|
|
11592
11500
|
};
|
|
11593
11501
|
}
|
|
11594
|
-
const originalBranchResult =
|
|
11502
|
+
const originalBranchResult = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
11595
11503
|
encoding: "utf8",
|
|
11596
11504
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11597
11505
|
});
|
|
11598
11506
|
const originalBranch = originalBranchResult.stdout.trim();
|
|
11599
|
-
const checkoutResult =
|
|
11507
|
+
const checkoutResult = spawnSync8("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", "-b", branch, baseHead], {
|
|
11600
11508
|
encoding: "utf8",
|
|
11601
11509
|
stdio: ["ignore", "pipe", "pipe"],
|
|
11602
11510
|
env: DEFAULT_GIT_ENV
|
|
11603
11511
|
});
|
|
11604
11512
|
if (checkoutResult.status !== 0) {
|
|
11605
11513
|
const stderr = checkoutResult.stderr?.trim() || "";
|
|
11606
|
-
const branchExists = stderr.includes("already exists") ||
|
|
11607
|
-
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"], {
|
|
11608
11516
|
encoding: "utf8",
|
|
11609
11517
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11610
11518
|
}).stdout.trim();
|
|
11611
11519
|
if (currentBranchCheck === branch && originalBranch && originalBranch !== branch) {
|
|
11612
|
-
|
|
11520
|
+
spawnSync8("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", originalBranch], {
|
|
11613
11521
|
stdio: ["ignore", "ignore", "ignore"]
|
|
11614
11522
|
});
|
|
11615
11523
|
}
|
|
@@ -11632,7 +11540,7 @@ function hasBoundaryWhitespace(path) {
|
|
|
11632
11540
|
return path.split("/").some((segment) => segment.trim() !== segment || segment.length === 0);
|
|
11633
11541
|
}
|
|
11634
11542
|
function getUnsupportedIndexFlags(root) {
|
|
11635
|
-
const result =
|
|
11543
|
+
const result = spawnSync8("git", ["-C", root, "ls-files", "-v", "-z", "--"], {
|
|
11636
11544
|
encoding: "utf8",
|
|
11637
11545
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11638
11546
|
});
|
|
@@ -11666,13 +11574,13 @@ function isPathAllowedForChild(path, taskId, scopeHint) {
|
|
|
11666
11574
|
});
|
|
11667
11575
|
}
|
|
11668
11576
|
function getChangedProjectPaths(root) {
|
|
11669
|
-
const tracked =
|
|
11577
|
+
const tracked = spawnSync8("git", ["-C", root, "diff-index", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
|
|
11670
11578
|
encoding: "utf8",
|
|
11671
11579
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11672
11580
|
});
|
|
11673
11581
|
if (tracked.status !== 0)
|
|
11674
11582
|
throw new Error("failed to inspect tracked diff vs HEAD");
|
|
11675
|
-
const untracked =
|
|
11583
|
+
const untracked = spawnSync8("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
|
|
11676
11584
|
encoding: "utf8",
|
|
11677
11585
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11678
11586
|
});
|
|
@@ -11682,7 +11590,7 @@ function getChangedProjectPaths(root) {
|
|
|
11682
11590
|
return [...new Set([...splitZ(tracked.stdout), ...splitZ(untracked.stdout)])];
|
|
11683
11591
|
}
|
|
11684
11592
|
function getStagedProjectPaths(root) {
|
|
11685
|
-
const staged =
|
|
11593
|
+
const staged = spawnSync8("git", ["-C", root, "diff-index", "--cached", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
|
|
11686
11594
|
encoding: "utf8",
|
|
11687
11595
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11688
11596
|
});
|
|
@@ -11691,13 +11599,13 @@ function getStagedProjectPaths(root) {
|
|
|
11691
11599
|
return staged.stdout.split("\x00").filter((p) => p.length > 0);
|
|
11692
11600
|
}
|
|
11693
11601
|
function getUnstagedProjectPaths(root) {
|
|
11694
|
-
const diffFiles =
|
|
11602
|
+
const diffFiles = spawnSync8("git", ["-C", root, "diff-files", "--name-only", "-z", "--ignore-submodules=none", "--"], {
|
|
11695
11603
|
encoding: "utf8",
|
|
11696
11604
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11697
11605
|
});
|
|
11698
11606
|
if (diffFiles.status !== 0)
|
|
11699
11607
|
throw new Error("failed to inspect unstaged tracked changes");
|
|
11700
|
-
const untracked =
|
|
11608
|
+
const untracked = spawnSync8("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
|
|
11701
11609
|
encoding: "utf8",
|
|
11702
11610
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11703
11611
|
});
|
|
@@ -11707,7 +11615,7 @@ function getUnstagedProjectPaths(root) {
|
|
|
11707
11615
|
return [...new Set([...splitZ(diffFiles.stdout), ...splitZ(untracked.stdout)])];
|
|
11708
11616
|
}
|
|
11709
11617
|
function getCommittedDeltaPaths(root) {
|
|
11710
|
-
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"] });
|
|
11711
11619
|
if (delta.status !== 0)
|
|
11712
11620
|
throw new Error("failed to inspect committed tree delta");
|
|
11713
11621
|
return delta.stdout.split("\x00").filter((p) => p.length > 0);
|
|
@@ -11813,7 +11721,7 @@ async function commitBatchChild(input) {
|
|
|
11813
11721
|
const goal = typeof intentSnapshot.goal === "string" ? intentSnapshot.goal : "";
|
|
11814
11722
|
const scopeHint = Array.isArray(intentSnapshot.scope_hint) ? intentSnapshot.scope_hint.filter((s) => typeof s === "string") : [];
|
|
11815
11723
|
if (expectedBranch !== undefined) {
|
|
11816
|
-
const currentBranchResult =
|
|
11724
|
+
const currentBranchResult = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
11817
11725
|
encoding: "utf8",
|
|
11818
11726
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11819
11727
|
});
|
|
@@ -11822,7 +11730,7 @@ async function commitBatchChild(input) {
|
|
|
11822
11730
|
throw new Error(`batch_head_lineage_broken: current branch ${currentBranch} does not match expected branch ${expectedBranch}`);
|
|
11823
11731
|
}
|
|
11824
11732
|
}
|
|
11825
|
-
const currentHeadResult =
|
|
11733
|
+
const currentHeadResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
11826
11734
|
encoding: "utf8",
|
|
11827
11735
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11828
11736
|
});
|
|
@@ -11841,7 +11749,7 @@ async function commitBatchChild(input) {
|
|
|
11841
11749
|
}
|
|
11842
11750
|
const pathsToStage = getUnstagedProjectPaths(root);
|
|
11843
11751
|
if (pathsToStage.length > 0) {
|
|
11844
|
-
const addResult =
|
|
11752
|
+
const addResult = spawnSync8("git", ["-C", root, "--literal-pathspecs", "add", "-A", "--", ...pathsToStage], {
|
|
11845
11753
|
encoding: "utf8",
|
|
11846
11754
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11847
11755
|
});
|
|
@@ -11856,7 +11764,7 @@ async function commitBatchChild(input) {
|
|
|
11856
11764
|
const staged = getStagedProjectPaths(root);
|
|
11857
11765
|
const stagedOutside = staged.filter((p) => !isPathAllowedForChild(p, taskId, scopeHint));
|
|
11858
11766
|
if (stagedOutside.length > 0) {
|
|
11859
|
-
|
|
11767
|
+
spawnSync8("git", ["-C", root, "reset", "--quiet"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
11860
11768
|
throw new Error("dirty_outside_scope");
|
|
11861
11769
|
}
|
|
11862
11770
|
const record = auditPair.record;
|
|
@@ -11874,7 +11782,7 @@ async function commitBatchChild(input) {
|
|
|
11874
11782
|
|
|
11875
11783
|
Immune-Brain-Batch: ${batchId}
|
|
11876
11784
|
`;
|
|
11877
|
-
const commitResult =
|
|
11785
|
+
const commitResult = spawnSync8("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "commit", "--no-verify", "-F", "-"], {
|
|
11878
11786
|
input: commitMessage,
|
|
11879
11787
|
encoding: "utf8",
|
|
11880
11788
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -11883,7 +11791,7 @@ Immune-Brain-Batch: ${batchId}
|
|
|
11883
11791
|
if (commitResult.status !== 0) {
|
|
11884
11792
|
throw new Error(`commit failed for child ${taskId}: ${commitResult.stderr?.trim() || "git commit failed"}`);
|
|
11885
11793
|
}
|
|
11886
|
-
const newHeadResult =
|
|
11794
|
+
const newHeadResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
11887
11795
|
encoding: "utf8",
|
|
11888
11796
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11889
11797
|
});
|
|
@@ -11891,7 +11799,7 @@ Immune-Brain-Batch: ${batchId}
|
|
|
11891
11799
|
if (newHeadResult.status !== 0 || !newHead || newHead === expectedHead) {
|
|
11892
11800
|
throw new Error("commit_failed");
|
|
11893
11801
|
}
|
|
11894
|
-
const parentsResult =
|
|
11802
|
+
const parentsResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD^@"], {
|
|
11895
11803
|
encoding: "utf8",
|
|
11896
11804
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11897
11805
|
});
|
|
@@ -11924,7 +11832,7 @@ async function lookupBatchCommit(input) {
|
|
|
11924
11832
|
let candidates = [];
|
|
11925
11833
|
let evidenceBacked = false;
|
|
11926
11834
|
if (evidence && evidence.commit) {
|
|
11927
|
-
const direct =
|
|
11835
|
+
const direct = spawnSync8("git", ["-C", root, "log", "-n", "1", `--format=${FORMAT}`, evidence.commit, "--"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
11928
11836
|
if (direct.status !== 0) {
|
|
11929
11837
|
throw new Error(`failed to inspect batch commit for ${taskId}: ${direct.stderr?.trim() || "git log failed"}`);
|
|
11930
11838
|
}
|
|
@@ -11937,7 +11845,7 @@ async function lookupBatchCommit(input) {
|
|
|
11937
11845
|
}
|
|
11938
11846
|
}
|
|
11939
11847
|
if (!candidates.length) {
|
|
11940
|
-
const result =
|
|
11848
|
+
const result = spawnSync8("git", [
|
|
11941
11849
|
"-C",
|
|
11942
11850
|
root,
|
|
11943
11851
|
"log",
|
|
@@ -11974,7 +11882,7 @@ async function lookupBatchCommit(input) {
|
|
|
11974
11882
|
throw new Error(`batch_head_lineage_broken: adopted commit author ${match.authorName} does not match batch authority ${DEFAULT_GIT_ENV.GIT_AUTHOR_NAME}`);
|
|
11975
11883
|
}
|
|
11976
11884
|
if (expectedBranch !== undefined) {
|
|
11977
|
-
const currentBranchResult =
|
|
11885
|
+
const currentBranchResult = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
11978
11886
|
encoding: "utf8",
|
|
11979
11887
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11980
11888
|
});
|
|
@@ -11984,15 +11892,17 @@ async function lookupBatchCommit(input) {
|
|
|
11984
11892
|
}
|
|
11985
11893
|
}
|
|
11986
11894
|
if (expectedHead !== undefined) {
|
|
11987
|
-
const currentHeadResult =
|
|
11895
|
+
const currentHeadResult = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
11988
11896
|
encoding: "utf8",
|
|
11989
11897
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11990
11898
|
});
|
|
11991
11899
|
const currentHead = currentHeadResult.stdout.trim();
|
|
11992
|
-
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) {
|
|
11993
11903
|
throw new Error(`batch_head_lineage_broken: current HEAD ${currentHead} diverged from adopted commit ${commit}`);
|
|
11994
11904
|
}
|
|
11995
|
-
const parentsResult =
|
|
11905
|
+
const parentsResult = spawnSync8("git", ["-C", root, "rev-parse", `${commit}^@`], {
|
|
11996
11906
|
encoding: "utf8",
|
|
11997
11907
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11998
11908
|
});
|
|
@@ -12010,7 +11920,7 @@ async function lookupBatchCommit(input) {
|
|
|
12010
11920
|
throw new Error(`batch_head_lineage_broken: adopted commit task lifecycle is not done: ${lifecycle}`);
|
|
12011
11921
|
}
|
|
12012
11922
|
const scopeHint = Array.isArray(auditPair.record.intent_snapshot.scope_hint) ? auditPair.record.intent_snapshot.scope_hint.filter((s) => typeof s === "string") : [];
|
|
12013
|
-
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"] });
|
|
12014
11924
|
if (deltaResult.status !== 0) {
|
|
12015
11925
|
throw new Error("batch_head_lineage_broken: failed to inspect adopted commit delta");
|
|
12016
11926
|
}
|
|
@@ -12049,18 +11959,12 @@ function skipDependents(record, taskId, reason) {
|
|
|
12049
11959
|
}
|
|
12050
11960
|
record.children = record.children.map((child) => skip.has(child.task_id) && child.state === "pending" ? { ...child, state: "skipped_blocked", reason } : child);
|
|
12051
11961
|
}
|
|
12052
|
-
function budgetStopReason(record
|
|
11962
|
+
function budgetStopReason(record) {
|
|
12053
11963
|
const enrolledCount = record.children.filter((child) => child.state !== "pending" && child.state !== "skipped_blocked").length;
|
|
12054
11964
|
if (enrolledCount >= record.budget.max_children)
|
|
12055
11965
|
return `max_children budget exhausted (${record.budget.max_children})`;
|
|
12056
|
-
const deadline = Date.parse(record.budget.deadline_at);
|
|
12057
|
-
if (!Number.isNaN(deadline) && now >= deadline)
|
|
12058
|
-
return `deadline_at reached (${record.budget.deadline_at})`;
|
|
12059
11966
|
return null;
|
|
12060
11967
|
}
|
|
12061
|
-
function isAuthorizationExpiryError(error) {
|
|
12062
|
-
return error instanceof BatchAuthorizationExpiryError;
|
|
12063
|
-
}
|
|
12064
11968
|
function requireFreshProjection(result, taskId) {
|
|
12065
11969
|
if (result.error !== null)
|
|
12066
11970
|
throw new Error(`cannot reconcile Kernel projection for ${taskId}: ${result.error}`);
|
|
@@ -12071,7 +11975,7 @@ function nextEnrollableChild(record) {
|
|
|
12071
11975
|
}
|
|
12072
11976
|
var TERMINAL_NEXT_ACTIONS = {
|
|
12073
11977
|
completed: "The batch settled every enrollable child; review the commits and the tracker.",
|
|
12074
|
-
budget_stopped: "
|
|
11978
|
+
budget_stopped: "The child budget stopped new enrollments; re-confirm to continue under a new authorization.",
|
|
12075
11979
|
failed: "A commit or lineage failure stopped the batch; inspect the failing child and the branch state.",
|
|
12076
11980
|
rejected: "The batch was rejected before any enrollment; correct the stated reason and re-confirm.",
|
|
12077
11981
|
needs_human: "A parked child needs a human decision; resolve it, then re-confirm to continue.",
|
|
@@ -12126,31 +12030,42 @@ function failPersistedLineage(root, existing, message) {
|
|
|
12126
12030
|
}
|
|
12127
12031
|
return writeBatchRunState(root, record);
|
|
12128
12032
|
}
|
|
12129
|
-
function
|
|
12033
|
+
function reconcileLineage(root, record) {
|
|
12130
12034
|
if (!existsSync9(join14(root, ".git")))
|
|
12131
|
-
return null;
|
|
12132
|
-
const
|
|
12133
|
-
const
|
|
12134
|
-
|
|
12135
|
-
|
|
12136
|
-
|
|
12137
|
-
|
|
12138
|
-
|
|
12139
|
-
}
|
|
12140
|
-
const branchCheck = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
12141
|
-
encoding: "utf8",
|
|
12142
|
-
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
|
|
12143
12043
|
});
|
|
12144
|
-
|
|
12145
|
-
|
|
12146
|
-
|
|
12147
|
-
|
|
12148
|
-
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
|
+
};
|
|
12149
12055
|
}
|
|
12150
12056
|
async function validatePersistedRun(input, record) {
|
|
12151
12057
|
if (!record.commits.length && record.children[0]?.state === "settled" && existsSync9(join14(input.root, ".git"))) {
|
|
12152
|
-
const head =
|
|
12153
|
-
|
|
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())
|
|
12154
12069
|
throw new Error("first unpersisted batch commit provenance is invalid");
|
|
12155
12070
|
}
|
|
12156
12071
|
const plan = input.registry.children(input.capability);
|
|
@@ -12174,7 +12089,7 @@ async function validatePersistedRun(input, record) {
|
|
|
12174
12089
|
});
|
|
12175
12090
|
if (!evidence || evidence.commit !== child.commit) {
|
|
12176
12091
|
if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync9(join14(input.root, ".git"))) {
|
|
12177
|
-
const reach =
|
|
12092
|
+
const reach = spawnSync9("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
12178
12093
|
if (reach.status !== 0) {
|
|
12179
12094
|
throw new Error(`batch_head_lineage_broken: recorded commit ${child.commit} for ${child.task_id} is no longer reachable from HEAD`);
|
|
12180
12095
|
}
|
|
@@ -12198,8 +12113,7 @@ function validateRunAuthorization(input, existing) {
|
|
|
12198
12113
|
plan_digest: existing?.plan_digest ?? input.plan_digest,
|
|
12199
12114
|
base_head: existing?.base_head ?? input.base_head,
|
|
12200
12115
|
initiative_slug: existing?.initiative_slug ?? input.initiative_slug,
|
|
12201
|
-
budget: input.budget
|
|
12202
|
-
expires_at: input.authorization_expires_at
|
|
12116
|
+
budget: input.budget
|
|
12203
12117
|
}
|
|
12204
12118
|
});
|
|
12205
12119
|
if (authorized.issued_at !== input.confirmation_time || existing && Date.parse(authorized.issued_at) <= Date.parse(existing.confirmation_time))
|
|
@@ -12221,7 +12135,6 @@ function applyPlanReconfirmation(input, existing) {
|
|
|
12221
12135
|
...existing,
|
|
12222
12136
|
plan_digest: input.plan_digest,
|
|
12223
12137
|
confirmation_time: input.confirmation_time,
|
|
12224
|
-
authorization_expires_at: input.authorization_expires_at,
|
|
12225
12138
|
budget: input.budget
|
|
12226
12139
|
};
|
|
12227
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)
|
|
@@ -12299,8 +12212,7 @@ async function startBatchLocked(input) {
|
|
|
12299
12212
|
if (existing?.batch_state === "needs_human") {
|
|
12300
12213
|
const priorConfirmation = Date.parse(existing.confirmation_time);
|
|
12301
12214
|
const nextConfirmation = Date.parse(input.confirmation_time);
|
|
12302
|
-
const
|
|
12303
|
-
const freshAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation && Number.isFinite(nextExpiry) && nextExpiry > Date.now();
|
|
12215
|
+
const freshAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation;
|
|
12304
12216
|
if (!freshAuthorization && !reconfirmed) {
|
|
12305
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.");
|
|
12306
12218
|
}
|
|
@@ -12327,7 +12239,6 @@ async function startBatchLocked(input) {
|
|
|
12327
12239
|
existing = writeBatchRunState(input.root, {
|
|
12328
12240
|
...reparked,
|
|
12329
12241
|
confirmation_time: input.confirmation_time,
|
|
12330
|
-
authorization_expires_at: input.authorization_expires_at,
|
|
12331
12242
|
budget: input.budget,
|
|
12332
12243
|
batch_state: "needs_human"
|
|
12333
12244
|
});
|
|
@@ -12337,28 +12248,11 @@ async function startBatchLocked(input) {
|
|
|
12337
12248
|
existing = writeBatchRunState(input.root, {
|
|
12338
12249
|
...existing,
|
|
12339
12250
|
confirmation_time: input.confirmation_time,
|
|
12340
|
-
authorization_expires_at: input.authorization_expires_at,
|
|
12341
12251
|
budget: input.budget,
|
|
12342
12252
|
batch_state: "running",
|
|
12343
12253
|
children: resumedChildren
|
|
12344
12254
|
});
|
|
12345
12255
|
}
|
|
12346
|
-
if (existing?.batch_state === "running") {
|
|
12347
|
-
const nextConfirmation = Date.parse(input.confirmation_time);
|
|
12348
|
-
const nextExpiry = Date.parse(input.authorization_expires_at);
|
|
12349
|
-
const priorConfirmation = Date.parse(existing.confirmation_time);
|
|
12350
|
-
const persistedExpiry = Date.parse(existing.authorization_expires_at);
|
|
12351
|
-
const renewedAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation && Number.isFinite(nextExpiry) && nextExpiry > Date.now() && nextExpiry > persistedExpiry;
|
|
12352
|
-
if (renewedAuthorization) {
|
|
12353
|
-
validateRunAuthorization(input, existing);
|
|
12354
|
-
existing = writeBatchRunState(input.root, {
|
|
12355
|
-
...existing,
|
|
12356
|
-
confirmation_time: input.confirmation_time,
|
|
12357
|
-
authorization_expires_at: input.authorization_expires_at,
|
|
12358
|
-
budget: input.budget
|
|
12359
|
-
});
|
|
12360
|
-
}
|
|
12361
|
-
}
|
|
12362
12256
|
if (existing) {
|
|
12363
12257
|
const interruptedChild = existing.children.find((child) => child.state === "enrolled" || child.state === "settled");
|
|
12364
12258
|
if (interruptedChild) {
|
|
@@ -12396,7 +12290,6 @@ async function startBatchLocked(input) {
|
|
|
12396
12290
|
plan_digest: input.plan_digest,
|
|
12397
12291
|
base_head: input.base_head,
|
|
12398
12292
|
confirmation_time: input.confirmation_time,
|
|
12399
|
-
authorization_expires_at: input.authorization_expires_at,
|
|
12400
12293
|
budget: input.budget,
|
|
12401
12294
|
now: input.now
|
|
12402
12295
|
});
|
|
@@ -12407,13 +12300,14 @@ async function startBatchLocked(input) {
|
|
|
12407
12300
|
record.batch_state = "running";
|
|
12408
12301
|
persist();
|
|
12409
12302
|
}
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
if (
|
|
12303
|
+
const lineage = reconcileLineage(input.root, record);
|
|
12304
|
+
record = lineage.record;
|
|
12305
|
+
if (lineage.failure) {
|
|
12413
12306
|
record.batch_state = "failed";
|
|
12414
12307
|
persist();
|
|
12415
|
-
return finalize(input.root, record,
|
|
12308
|
+
return finalize(input.root, record, lineage.failure, "");
|
|
12416
12309
|
}
|
|
12310
|
+
let head = expectedBatchHead(record);
|
|
12417
12311
|
while (record.batch_state === "running") {
|
|
12418
12312
|
const child = nextEnrollableChild(record);
|
|
12419
12313
|
if (!child) {
|
|
@@ -12440,8 +12334,7 @@ async function startBatchLocked(input) {
|
|
|
12440
12334
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "enrolled", reason: "adopted existing batch claim after interruption" } : c);
|
|
12441
12335
|
persist();
|
|
12442
12336
|
} else {
|
|
12443
|
-
|
|
12444
|
-
if (now >= Date.parse(record.authorization_expires_at) || budgetStopReason(record, now)) {
|
|
12337
|
+
if (budgetStopReason(record)) {
|
|
12445
12338
|
record.batch_state = record.children.some((c) => c.state === "needs_human" || c.state === "skipped_blocked") ? "needs_human" : "budget_stopped";
|
|
12446
12339
|
persist();
|
|
12447
12340
|
break;
|
|
@@ -12456,8 +12349,7 @@ async function startBatchLocked(input) {
|
|
|
12456
12349
|
plan_digest: record.plan_digest,
|
|
12457
12350
|
base_head: record.base_head,
|
|
12458
12351
|
initiative_slug: record.initiative_slug,
|
|
12459
|
-
budget: record.budget
|
|
12460
|
-
expires_at: record.authorization_expires_at
|
|
12352
|
+
budget: record.budget
|
|
12461
12353
|
}
|
|
12462
12354
|
});
|
|
12463
12355
|
for (const committed of record.children) {
|
|
@@ -12477,12 +12369,6 @@ async function startBatchLocked(input) {
|
|
|
12477
12369
|
persist();
|
|
12478
12370
|
} catch (error) {
|
|
12479
12371
|
const message = error instanceof Error ? error.message : String(error);
|
|
12480
|
-
if (isAuthorizationExpiryError(error)) {
|
|
12481
|
-
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "pending", reason: message } : c);
|
|
12482
|
-
record.batch_state = "budget_stopped";
|
|
12483
|
-
persist();
|
|
12484
|
-
break;
|
|
12485
|
-
}
|
|
12486
12372
|
if (isLineageBreakError(message)) {
|
|
12487
12373
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
|
|
12488
12374
|
skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
@@ -12505,7 +12391,7 @@ async function startBatchLocked(input) {
|
|
|
12505
12391
|
function stopReasonFor(record) {
|
|
12506
12392
|
switch (record.batch_state) {
|
|
12507
12393
|
case "budget_stopped":
|
|
12508
|
-
return budgetStopReason(record
|
|
12394
|
+
return budgetStopReason(record) ?? "the child budget stopped new enrollments";
|
|
12509
12395
|
case "failed": {
|
|
12510
12396
|
const failedChild = record.children.find((c) => c.state === "needs_human" && c.reason);
|
|
12511
12397
|
return failedChild?.reason ?? "a commit or lineage failure stopped the batch";
|
|
@@ -12540,10 +12426,11 @@ async function resumeBatch(input, projection) {
|
|
|
12540
12426
|
const driven = existing.children.find((child) => child.state === "enrolled" || child.state === "settled");
|
|
12541
12427
|
if (driven) {
|
|
12542
12428
|
if (driven.state === "enrolled") {
|
|
12543
|
-
const
|
|
12544
|
-
|
|
12545
|
-
|
|
12546
|
-
|
|
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, "");
|
|
12547
12434
|
}
|
|
12548
12435
|
const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, driven.task_id), driven.task_id);
|
|
12549
12436
|
const holdsClaim = fresh.claim !== null && fresh.claim.task_id === driven.task_id && input.kernel.ownsTaskClaim(driven.task_id);
|
|
@@ -12622,7 +12509,7 @@ async function driveInterruptedChild(input, child) {
|
|
|
12622
12509
|
record.batch_state = "running";
|
|
12623
12510
|
persist();
|
|
12624
12511
|
}
|
|
12625
|
-
let head = record
|
|
12512
|
+
let head = expectedBatchHead(record);
|
|
12626
12513
|
while (child.state === "enrolled" || child.state === "settled") {
|
|
12627
12514
|
if (child.state === "settled") {
|
|
12628
12515
|
let existing = null;
|
|
@@ -12643,6 +12530,10 @@ async function driveInterruptedChild(input, child) {
|
|
|
12643
12530
|
persist();
|
|
12644
12531
|
throw new BatchCommitAbortError(`commit lookup failed: ${message}`);
|
|
12645
12532
|
}
|
|
12533
|
+
if (!existing) {
|
|
12534
|
+
record = reconcileLineage(input.root, record).record;
|
|
12535
|
+
head = expectedBatchHead(record);
|
|
12536
|
+
}
|
|
12646
12537
|
const planChild = input.children.find((c) => c.task_id === child.task_id);
|
|
12647
12538
|
const intentPath = planChild?.intent_path ?? undefined;
|
|
12648
12539
|
const doCommit = async () => {
|
|
@@ -12938,7 +12829,6 @@ async function mintCapability(registry, input) {
|
|
|
12938
12829
|
diff_hash: input.diff_hash,
|
|
12939
12830
|
actor_id: input.actor_id,
|
|
12940
12831
|
confirmation_ref: input.confirmation_ref,
|
|
12941
|
-
expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
|
12942
12832
|
findings_digest: input.action_kind === "request_rework" ? findingsDigestV2(input.findings) : null
|
|
12943
12833
|
};
|
|
12944
12834
|
return registry.issue(binding);
|
|
@@ -13075,7 +12965,6 @@ class ClaudeRuntime {
|
|
|
13075
12965
|
preparation_digest: preparation.digest,
|
|
13076
12966
|
actor_id: LITERAL_USER_ACTOR_ID,
|
|
13077
12967
|
confirmation_ref: gate.confirmation_ref,
|
|
13078
|
-
expires_at: new Date(Date.now() + 10 * 60 * 1000).toISOString(),
|
|
13079
12968
|
nonce
|
|
13080
12969
|
};
|
|
13081
12970
|
const capability = this.enrollmentRegistry.issue(binding);
|
|
@@ -13099,11 +12988,41 @@ class ClaudeRuntime {
|
|
|
13099
12988
|
throw error;
|
|
13100
12989
|
}
|
|
13101
12990
|
}
|
|
13102
|
-
async advance(taskId, signal) {
|
|
13103
|
-
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);
|
|
13104
12996
|
}
|
|
13105
|
-
async
|
|
13106
|
-
|
|
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
|
+
}
|
|
13107
13026
|
}
|
|
13108
13027
|
async withTerminalTracker(taskId, result) {
|
|
13109
13028
|
if (result === null || typeof result !== "object")
|
|
@@ -13405,15 +13324,16 @@ class ClaudeRuntime {
|
|
|
13405
13324
|
throw error;
|
|
13406
13325
|
}
|
|
13407
13326
|
}
|
|
13408
|
-
async startUnattendedBatch(initiativeSlug, meta) {
|
|
13327
|
+
async startUnattendedBatch(initiativeSlug, meta, options = {}) {
|
|
13409
13328
|
throwIfCancelled(meta.signal);
|
|
13329
|
+
const reuseOnly = options.reuseOnly === true;
|
|
13410
13330
|
const probe = probeHost(this.env, process.platform, this.hostVersion);
|
|
13411
13331
|
if (!probe.ok)
|
|
13412
13332
|
throw new NativeAuthorityError("unsupported_host", probe.reason);
|
|
13413
13333
|
const interactive = meta.interactive ?? this.interactive;
|
|
13414
|
-
if (!interactive)
|
|
13334
|
+
if (!reuseOnly && !interactive)
|
|
13415
13335
|
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
13416
|
-
if (!this.requestConfirmation)
|
|
13336
|
+
if (!reuseOnly && !this.requestConfirmation)
|
|
13417
13337
|
throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
|
|
13418
13338
|
let now = new Date().toISOString();
|
|
13419
13339
|
const preflight = await projectBatchPreflight({
|
|
@@ -13443,7 +13363,15 @@ class ClaudeRuntime {
|
|
|
13443
13363
|
readInitiative: this.readInitiative ?? observeGithubInitiative,
|
|
13444
13364
|
nonce: enrollmentNonce(),
|
|
13445
13365
|
gate: async (facts) => {
|
|
13446
|
-
|
|
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
|
+
};
|
|
13447
13375
|
let confirmationResult;
|
|
13448
13376
|
try {
|
|
13449
13377
|
confirmationResult = await this.requestConfirmation({
|
|
@@ -13465,17 +13393,14 @@ class ClaudeRuntime {
|
|
|
13465
13393
|
reason: child.reason
|
|
13466
13394
|
})),
|
|
13467
13395
|
budget: facts.budget,
|
|
13468
|
-
expires_at: facts.expires_at,
|
|
13469
13396
|
...facts.reuse_blockers.length > 0 ? {
|
|
13470
13397
|
re_confirmation_required: facts.reuse_blockers,
|
|
13471
13398
|
recovery: "confirm to issue a fresh authorization bound to the current plan and HEAD"
|
|
13472
13399
|
} : {}
|
|
13473
13400
|
},
|
|
13474
|
-
signal:
|
|
13401
|
+
signal: meta.signal
|
|
13475
13402
|
});
|
|
13476
13403
|
} catch (err) {
|
|
13477
|
-
if (deadline.timedOut())
|
|
13478
|
-
return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
|
|
13479
13404
|
if (meta.signal?.aborted)
|
|
13480
13405
|
return { kind: "host_rejection", value: batchReason("cancelled_before_execution") };
|
|
13481
13406
|
if (err instanceof NativeAuthorityError) {
|
|
@@ -13496,11 +13421,7 @@ class ClaudeRuntime {
|
|
|
13496
13421
|
kind: "host_rejection",
|
|
13497
13422
|
value: batchReason("confirmation_failed", err instanceof Error ? err.message : String(err))
|
|
13498
13423
|
};
|
|
13499
|
-
} finally {
|
|
13500
|
-
deadline.clear();
|
|
13501
13424
|
}
|
|
13502
|
-
if (deadline.timedOut())
|
|
13503
|
-
return { kind: "host_rejection", value: batchReason("confirmation_timed_out") };
|
|
13504
13425
|
if (confirmationResult.decision === "cancel" && meta.signal?.aborted)
|
|
13505
13426
|
return { kind: "host_rejection", value: batchReason("confirmation_cancelled") };
|
|
13506
13427
|
if (meta.signal?.aborted)
|
|
@@ -13526,12 +13447,10 @@ class ClaudeRuntime {
|
|
|
13526
13447
|
return authorization.value;
|
|
13527
13448
|
if (authorization.outcome === "rejected")
|
|
13528
13449
|
return authorization.rejection;
|
|
13529
|
-
const { binding, batch_id: batchId
|
|
13450
|
+
const { binding, batch_id: batchId } = authorization;
|
|
13530
13451
|
if (meta.signal?.aborted)
|
|
13531
13452
|
return batchReason("cancelled_before_execution");
|
|
13532
13453
|
now = new Date().toISOString();
|
|
13533
|
-
if (Date.parse(expiresAt) <= Date.parse(now))
|
|
13534
|
-
return batchReason("confirmation_failed", "confirmed batch deadline expired before authorization issuance");
|
|
13535
13454
|
const capability = this.batchRegistry.issue(binding, recoveryChildren, now);
|
|
13536
13455
|
const basePort = this.createBatchKernelPort(this.batchRegistry, capability, binding);
|
|
13537
13456
|
const kernelPort = {
|
|
@@ -13553,7 +13472,6 @@ class ClaudeRuntime {
|
|
|
13553
13472
|
plan_digest: planDigest,
|
|
13554
13473
|
base_head: binding.base_head,
|
|
13555
13474
|
confirmation_time: now,
|
|
13556
|
-
authorization_expires_at: expiresAt,
|
|
13557
13475
|
budget,
|
|
13558
13476
|
now,
|
|
13559
13477
|
kernel: kernelPort,
|
|
@@ -13764,11 +13682,11 @@ function createMcpRuntime(options = {}) {
|
|
|
13764
13682
|
if (name === "enroll")
|
|
13765
13683
|
return runtime.enroll(taskId, toolMeta);
|
|
13766
13684
|
if (name === "advance_assurance")
|
|
13767
|
-
return runtime.advance(taskId, toolMeta.signal);
|
|
13685
|
+
return runtime.advance(taskId, toolMeta.signal, toolMeta);
|
|
13768
13686
|
if (name === "submit_review") {
|
|
13769
13687
|
if (!Object.hasOwn(args, "verdict"))
|
|
13770
13688
|
throw new Error("verdict is required");
|
|
13771
|
-
return runtime.submitReview(taskId, args.verdict);
|
|
13689
|
+
return runtime.submitReview(taskId, args.verdict, toolMeta);
|
|
13772
13690
|
}
|
|
13773
13691
|
if (name === "revise_intent") {
|
|
13774
13692
|
if (!Object.hasOwn(args, "next_intent"))
|
|
@@ -13964,8 +13882,7 @@ ${b.children.map((c) => ` - ${c.task_id} (${c.slice_id}) [risk: ${c.risk ?? "un
|
|
|
13964
13882
|
b?.excluded && b.excluded.length > 0 ? `Excluded children (${b.excluded.length}):
|
|
13965
13883
|
${b.excluded.map((e) => ` - ${e.task_id} (${e.slice_id}): ${e.reason}`).join(`
|
|
13966
13884
|
`)}` : null,
|
|
13967
|
-
b?.budget ? `Budget: max_children=${b.budget.max_children},
|
|
13968
|
-
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
|
|
13969
13886
|
].filter(Boolean);
|
|
13970
13887
|
return {
|
|
13971
13888
|
mode: "form",
|