immune-brain 4.4.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugins/immune-brain/.claude-plugin/plugin.json +1 -1
- package/plugins/immune-brain/.pi-extension/imm-canary-work.ts +19 -2
- package/plugins/immune-brain/.pi-extension/imm-unattended-batch.ts +10 -2
- package/plugins/immune-brain/.pi-extension/pi-canary-tool-failure.ts +6 -1
- package/plugins/immune-brain/.pi-extension/runtime-stub.ts +11 -6
- package/plugins/immune-brain/dist/BASELINE.md +18 -1
- package/plugins/immune-brain/dist/claude/mcp-server.mjs +1039 -465
- package/plugins/immune-brain/dist/docs/reference/subagent-dispatch-protocol.md +30 -0
- package/plugins/immune-brain/dist/imm-brainstorm.md +21 -0
- package/plugins/immune-brain/dist/imm-loop.md +76 -4
- package/plugins/immune-brain/dist/imm-planner.md +65 -1
- package/plugins/immune-brain/dist/role-prompts/code-review.md +19 -0
- package/plugins/immune-brain/dist/role-prompts/executor.md +6 -0
- package/plugins/immune-brain/dist/role-prompts/qa.md +13 -1
- package/plugins/immune-brain/runtime/assurance/coordinator.ts +160 -12
- package/plugins/immune-brain/runtime/assurance/qa.ts +57 -42
- package/plugins/immune-brain/runtime/claude/kernel_ports.ts +20 -8
- package/plugins/immune-brain/runtime/github_issue_tracker.ts +321 -60
- package/plugins/immune-brain/runtime/kernel/storage.ts +8 -3
- package/plugins/immune-brain/runtime/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/code-review.md +19 -0
- package/plugins/immune-brain/runtime/prompts/executor.md +6 -0
- package/plugins/immune-brain/runtime/prompts/qa.md +13 -1
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +1 -1
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +73 -26
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +6 -0
- package/plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts +194 -0
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +126 -151
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +34 -4
- package/plugins/immune-brain/runtime/v4_runtime.ts +54 -0
- package/plugins/immune-brain/runtime/workspace_scope.ts +31 -4
- package/plugins/immune-brain/skills/BASELINE.md +18 -1
- package/plugins/immune-brain/skills/imm-brainstorm/SKILL.md +5 -0
- package/plugins/immune-brain/skills/imm-loop/SKILL.md +4 -2
- package/plugins/immune-brain/skills/imm-planner/SKILL.md +10 -1
|
@@ -39,7 +39,7 @@ function probeHost(env = process.env, platform = process.platform, hostVersion)
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// plugins/immune-brain/runtime/plugin_version.ts
|
|
42
|
-
var PLUGIN_VERSION = "4.
|
|
42
|
+
var PLUGIN_VERSION = "4.5.0";
|
|
43
43
|
|
|
44
44
|
// plugins/immune-brain/runtime/claude/interaction.ts
|
|
45
45
|
import { createHash, randomUUID } from "node:crypto";
|
|
@@ -1362,6 +1362,88 @@ function reviewAdvisoryRecords(verdict) {
|
|
|
1362
1362
|
evidence: finding.evidence ?? null
|
|
1363
1363
|
}));
|
|
1364
1364
|
}
|
|
1365
|
+
var QA_STAGES = ["resolution", "prepare", "check", "integrity"];
|
|
1366
|
+
var QA_OUTCOMES = [
|
|
1367
|
+
"running",
|
|
1368
|
+
"passed",
|
|
1369
|
+
"executable_or_cwd_unavailable",
|
|
1370
|
+
"process_launch_failed",
|
|
1371
|
+
"process_cleanup_failed",
|
|
1372
|
+
"protected_input_or_output_drift",
|
|
1373
|
+
"execution_metadata_limit_exceeded",
|
|
1374
|
+
"timeout",
|
|
1375
|
+
"output_limit",
|
|
1376
|
+
"nonzero_exit",
|
|
1377
|
+
"execution_failed",
|
|
1378
|
+
"command_identity_changed",
|
|
1379
|
+
"delivery_cleanup_failed",
|
|
1380
|
+
"delivery_unavailable"
|
|
1381
|
+
];
|
|
1382
|
+
function qaDiagnosticMetadata(d) {
|
|
1383
|
+
if (typeof d.acceptance_id !== "string" || !d.acceptance_id || !/^acceptance\/\d{1,6}\/verification\/(command|environment\/prepare)$/.test(d.descriptor_ref) || !/^sha256:[a-f0-9]{64}$/.test(d.descriptor_digest) || !QA_STAGES.includes(d.stage) || !QA_OUTCOMES.includes(d.outcome) || !Number.isFinite(d.elapsed_ms) || d.elapsed_ms < 0 || [d.stdout_bytes, d.stderr_bytes].some((n) => n !== null && (!Number.isSafeInteger(n) || n < 0)) || d.exit_code !== null && !Number.isSafeInteger(d.exit_code))
|
|
1384
|
+
throw new Error("invalid QA diagnostic metadata");
|
|
1385
|
+
const safe = {
|
|
1386
|
+
acceptance_id: d.acceptance_id,
|
|
1387
|
+
descriptor_ref: d.descriptor_ref,
|
|
1388
|
+
descriptor_digest: d.descriptor_digest,
|
|
1389
|
+
stage: d.stage,
|
|
1390
|
+
outcome: d.outcome,
|
|
1391
|
+
elapsed_ms: d.elapsed_ms,
|
|
1392
|
+
exit_code: d.exit_code,
|
|
1393
|
+
stdout_bytes: d.stdout_bytes,
|
|
1394
|
+
stderr_bytes: d.stderr_bytes
|
|
1395
|
+
};
|
|
1396
|
+
if (Buffer.byteLength(JSON.stringify(safe)) > 16384)
|
|
1397
|
+
throw new QaPreparationError("prepare", [], "execution_metadata_limit_exceeded");
|
|
1398
|
+
return safe;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
class QaPreparationError extends Error {
|
|
1402
|
+
stage;
|
|
1403
|
+
acceptance_ids;
|
|
1404
|
+
reason;
|
|
1405
|
+
diagnostics;
|
|
1406
|
+
constructor(stage, acceptance_ids, reason, diagnostics = []) {
|
|
1407
|
+
if (!QA_STAGES.includes(stage) || !QA_OUTCOMES.includes(reason))
|
|
1408
|
+
throw new Error("invalid QA preparation classification");
|
|
1409
|
+
const safe = diagnostics.map(qaDiagnosticMetadata);
|
|
1410
|
+
if (Buffer.byteLength(JSON.stringify(safe)) > 16384)
|
|
1411
|
+
throw new QaPreparationError("prepare", [], "execution_metadata_limit_exceeded");
|
|
1412
|
+
super(`QA ${stage} failed (${reason}); affected checks=${acceptance_ids.length}`);
|
|
1413
|
+
this.stage = stage;
|
|
1414
|
+
this.acceptance_ids = acceptance_ids;
|
|
1415
|
+
this.reason = reason;
|
|
1416
|
+
this.diagnostics = diagnostics;
|
|
1417
|
+
this.diagnostics = safe;
|
|
1418
|
+
this.name = "QaPreparationError";
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function deriveAssuranceRecovery(taskId, fresh, result, findings = []) {
|
|
1422
|
+
if (fresh.error || fresh.claim?.task_id !== taskId || fresh.projection.lifecycle !== "active")
|
|
1423
|
+
return null;
|
|
1424
|
+
const p = fresh.projection;
|
|
1425
|
+
const findingIds = [...p.blocking_finding_ids, ...p.unresolved_user_decision_ids, ...p.replan_required_ids];
|
|
1426
|
+
const scope = result.reason?.includes("task delivery contains paths outside the authorization envelope");
|
|
1427
|
+
const migration = result.reason?.includes("verification_contract_migration_required");
|
|
1428
|
+
const unstaged = result.reason?.includes("task delivery has unstaged or untracked changes");
|
|
1429
|
+
const reviewPreparation = result.state === "review_preparation_failed" && p.next_obligation === "run_review";
|
|
1430
|
+
const authority = scope || migration || p.next_obligation === "resolve_user_decision" || p.next_obligation === "revise_intent" || p.unresolved_user_decision_ids.length > 0 || p.replan_required_ids.length > 0 || p.authorization.state !== "none";
|
|
1431
|
+
const repair = unstaged || p.next_obligation === "resolve_findings" || p.blocking_finding_ids.length > 0;
|
|
1432
|
+
if (!authority && !repair && !result.environment_failure && !reviewPreparation)
|
|
1433
|
+
return null;
|
|
1434
|
+
const affected = findings.filter((f) => f.status === "open" && f.id && findingIds.includes(f.id) && f.acceptance_id).map((f) => f.acceptance_id);
|
|
1435
|
+
const category = authority ? "authorization" : repair ? "repair" : "environment";
|
|
1436
|
+
return {
|
|
1437
|
+
category,
|
|
1438
|
+
task_id: taskId,
|
|
1439
|
+
run_id: p.run_id ?? null,
|
|
1440
|
+
record_revision: p.record_revision,
|
|
1441
|
+
next_obligation: p.next_obligation,
|
|
1442
|
+
finding_ids: findingIds,
|
|
1443
|
+
acceptance_ids: [...new Set(affected.length ? affected : result.diagnostics?.length ? result.diagnostics.map((d) => d.acceptance_id) : reviewPreparation ? p.fresh_acceptance_ids : p.missing_acceptance_ids)],
|
|
1444
|
+
next_action: scope ? "Reconcile the listed paths with the TaskIntent scope: unstage unrelated changes or submit the required native Intent revision." : migration ? "route Planner for the verification descriptor v2 Intent revision and native gate" : category === "authorization" ? p.authorization.blocked ? "inspect authority state" : p.next_obligation === "revise_intent" ? "route Planner for the required Intent revision and native gate" : "request_authorization" : unstaged ? "Stage only the scoped implementation changes, then call advance_assurance." : category === "repair" ? "Route foreground Executor to repair and verify the identified findings; dispose each by resolve_finding or evidence-bound refute_finding, then run fresh advance_assurance." : reviewPreparation ? "Repair the Review evidence environment, then call advance_assurance to resume run_review; retain fresh QA and do not re-confirm the batch." : "Repair the identified verification environment, then call advance_assurance; a local check is non-attesting."
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1365
1447
|
var QA_MIN_JOB_TIMEOUT_SECONDS = 15 * 60;
|
|
1366
1448
|
var QA_MAX_JOB_TIMEOUT_SECONDS = 60 * 60;
|
|
1367
1449
|
var QA_JOB_OVERHEAD_SECONDS = 2 * 60;
|
|
@@ -1673,6 +1755,36 @@ class AssuranceCoordinator {
|
|
|
1673
1755
|
return this.sessionGeneration;
|
|
1674
1756
|
}
|
|
1675
1757
|
async advance(taskId, ctx, signal, onUpdate) {
|
|
1758
|
+
const checks = new Map;
|
|
1759
|
+
const result = await this.advanceOnce(taskId, ctx, signal, (update) => {
|
|
1760
|
+
const diagnostic = update.details.diagnostic;
|
|
1761
|
+
if (diagnostic) {
|
|
1762
|
+
checks.set(diagnostic.descriptor_ref, diagnostic);
|
|
1763
|
+
if (Buffer.byteLength(JSON.stringify([...checks.values()])) > 16384)
|
|
1764
|
+
throw new QaPreparationError("prepare", [diagnostic.acceptance_id], "execution_metadata_limit_exceeded", [{ ...diagnostic, stage: "prepare", outcome: "execution_metadata_limit_exceeded" }]);
|
|
1765
|
+
}
|
|
1766
|
+
onUpdate?.(update);
|
|
1767
|
+
});
|
|
1768
|
+
const diagnostics = result.diagnostics ?? [...checks.values()];
|
|
1769
|
+
return this.withRecovery(taskId, ctx, diagnostics.length ? { ...result, diagnostics } : result);
|
|
1770
|
+
}
|
|
1771
|
+
async withRecovery(taskId, ctx, enriched) {
|
|
1772
|
+
if (enriched.recovery_error || !["failed", "blocked", "rework", "review_preparation_failed"].includes(enriched.state))
|
|
1773
|
+
return enriched;
|
|
1774
|
+
try {
|
|
1775
|
+
const fresh = await this.ports.projectTask(ctx.cwd, taskId);
|
|
1776
|
+
if (fresh.error || fresh.claim?.task_id !== taskId || fresh.projection.lifecycle !== "active")
|
|
1777
|
+
return { ...enriched, recovery_error: "fresh_kernel_projection_unavailable" };
|
|
1778
|
+
const record = await this.ports.readTaskRecord(ctx.cwd, taskId);
|
|
1779
|
+
if (!record.record || record.revision !== fresh.projection.record_revision)
|
|
1780
|
+
return { ...enriched, recovery_error: "fresh_kernel_projection_unavailable" };
|
|
1781
|
+
const recovery = deriveAssuranceRecovery(taskId, fresh, enriched, record.record?.findings);
|
|
1782
|
+
return recovery ? { ...enriched, recovery } : enriched;
|
|
1783
|
+
} catch {
|
|
1784
|
+
return { ...enriched, recovery_error: "fresh_kernel_projection_unavailable" };
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
async advanceOnce(taskId, ctx, signal, onUpdate) {
|
|
1676
1788
|
if (this.isInvocationOpen(taskId))
|
|
1677
1789
|
return { state: "blocked", reason: "an authority invocation is already open" };
|
|
1678
1790
|
const active = this.active(taskId);
|
|
@@ -1682,7 +1794,7 @@ class AssuranceCoordinator {
|
|
|
1682
1794
|
try {
|
|
1683
1795
|
projection = await this.ports.projectTask(ctx.cwd, taskId);
|
|
1684
1796
|
} catch (error) {
|
|
1685
|
-
return { state: "blocked", reason: `cannot validate Review reservation: ${boundedAssuranceError(error)}
|
|
1797
|
+
return { state: "blocked", reason: `cannot validate Review reservation: ${boundedAssuranceError(error)}`, recovery_error: "fresh_kernel_projection_unavailable" };
|
|
1686
1798
|
}
|
|
1687
1799
|
const matches = reservationStillValid(reservation, projection, taskId);
|
|
1688
1800
|
if (matches)
|
|
@@ -1690,6 +1802,8 @@ class AssuranceCoordinator {
|
|
|
1690
1802
|
if (reservation)
|
|
1691
1803
|
this.releaseReviewReservation(taskId, reservation);
|
|
1692
1804
|
this.rejectedReviewOperations.delete(taskId);
|
|
1805
|
+
if (projection.error || projection.projection.lifecycle !== "done" && projection.projection.lifecycle !== "stopped" && (!projection.claim || projection.claim.task_id !== taskId))
|
|
1806
|
+
return { state: "blocked", reason: projection.error ?? "no active backend claim for this task", recovery_error: "fresh_kernel_projection_unavailable" };
|
|
1693
1807
|
}
|
|
1694
1808
|
const refreshed = this.active(taskId);
|
|
1695
1809
|
if (refreshed?.state === "running")
|
|
@@ -1708,6 +1822,7 @@ class AssuranceCoordinator {
|
|
|
1708
1822
|
let authorityBoundaryStarted = false;
|
|
1709
1823
|
let boundaryBaseline = null;
|
|
1710
1824
|
let reviewPreparationStarted = false;
|
|
1825
|
+
let initialProjectionAvailable = false;
|
|
1711
1826
|
let qaJobBudgetExceeded = false;
|
|
1712
1827
|
let phase = "preparing";
|
|
1713
1828
|
const operationLive = () => this.sessionActive && this.sessionGeneration === operationGeneration && this.activeOperations.get(taskId) === operationId && !operationController.signal.aborted;
|
|
@@ -1744,15 +1859,16 @@ class AssuranceCoordinator {
|
|
|
1744
1859
|
}
|
|
1745
1860
|
ensureOperationLive();
|
|
1746
1861
|
if (projection.error)
|
|
1747
|
-
return { state: "blocked", reason: projection.error };
|
|
1862
|
+
return { state: "blocked", reason: projection.error, recovery_error: "fresh_kernel_projection_unavailable" };
|
|
1748
1863
|
if (projection.projection.lifecycle === "done" || projection.projection.lifecycle === "stopped") {
|
|
1749
1864
|
this.unknownOperations.delete(taskId);
|
|
1750
1865
|
return { state: projection.projection.lifecycle === "done" ? "completed" : "stopped" };
|
|
1751
1866
|
}
|
|
1752
1867
|
if (!projection.claim)
|
|
1753
|
-
return { state: "blocked", reason: "no active backend claim" };
|
|
1868
|
+
return { state: "blocked", reason: "no active backend claim", recovery_error: "fresh_kernel_projection_unavailable" };
|
|
1754
1869
|
if (projection.claim.task_id !== taskId)
|
|
1755
|
-
return { state: "blocked", reason: `backend claim belongs to ${projection.claim.task_id}, not ${taskId}
|
|
1870
|
+
return { state: "blocked", reason: `backend claim belongs to ${projection.claim.task_id}, not ${taskId}`, recovery_error: "fresh_kernel_projection_unavailable" };
|
|
1871
|
+
initialProjectionAvailable = true;
|
|
1756
1872
|
if (projection.projection.lifecycle === "active")
|
|
1757
1873
|
this.unknownOperations.delete(taskId);
|
|
1758
1874
|
const parked = await this.ports.readTaskRecord(ctx.cwd, taskId);
|
|
@@ -1824,6 +1940,7 @@ class AssuranceCoordinator {
|
|
|
1824
1940
|
operationController.abort(new Error(`deterministic QA exceeded its declared ${Math.ceil(qaJobTimeoutMs / 60000)} minute job budget`));
|
|
1825
1941
|
}, qaJobTimeoutMs);
|
|
1826
1942
|
try {
|
|
1943
|
+
phase = "verifying";
|
|
1827
1944
|
qaVerdict = await this.ports.runQa(assurance.snapshot, assurance.descriptors, {
|
|
1828
1945
|
signal: operationController.signal,
|
|
1829
1946
|
onProgress: (item) => progress("verifying", `QA ${item.index}/${item.total} ${item.acceptance_id} ${item.phase}`, {
|
|
@@ -1831,7 +1948,8 @@ class AssuranceCoordinator {
|
|
|
1831
1948
|
total: item.total,
|
|
1832
1949
|
acceptance_id: item.acceptance_id,
|
|
1833
1950
|
acceptance_phase: item.phase,
|
|
1834
|
-
elapsed_ms: item.elapsed_ms
|
|
1951
|
+
elapsed_ms: item.elapsed_ms,
|
|
1952
|
+
...item.diagnostic ? { diagnostic: qaDiagnosticMetadata(item.diagnostic) } : {}
|
|
1835
1953
|
})
|
|
1836
1954
|
});
|
|
1837
1955
|
} finally {
|
|
@@ -1964,6 +2082,31 @@ class AssuranceCoordinator {
|
|
|
1964
2082
|
progress("review_ready", "QA passed; invoke the reserved foreground Agent, then call submit_review", { snapshot_digest: snapshotDigest(review.snapshot), review_bundle_digest: review.snapshot.review_bundle_digest ?? "", agent_params: hostReservation.dispatch });
|
|
1965
2083
|
return { state: "review_ready", operation: "review", operation_id: operationId, snapshot_digest: snapshotDigest(review.snapshot), review_bundle_digest: review.snapshot.review_bundle_digest ?? "", agent_params: hostReservation.dispatch };
|
|
1966
2084
|
} catch (error) {
|
|
2085
|
+
if (!initialProjectionAvailable && !aborted() && !(error instanceof VerificationAbortedError))
|
|
2086
|
+
return {
|
|
2087
|
+
state: "failed",
|
|
2088
|
+
operation: "qa",
|
|
2089
|
+
operation_id: operationId,
|
|
2090
|
+
reason: `${phase}: ${boundedAssuranceError(error)}`,
|
|
2091
|
+
recovery_error: "fresh_kernel_projection_unavailable"
|
|
2092
|
+
};
|
|
2093
|
+
if (error instanceof QaPreparationError && !authorityCommitted && !authorityBoundaryStarted && !aborted())
|
|
2094
|
+
return {
|
|
2095
|
+
state: "failed",
|
|
2096
|
+
operation: "qa",
|
|
2097
|
+
operation_id: operationId,
|
|
2098
|
+
reason: error.message,
|
|
2099
|
+
environment_failure: true,
|
|
2100
|
+
diagnostics: error.diagnostics.map(qaDiagnosticMetadata)
|
|
2101
|
+
};
|
|
2102
|
+
if (phase === "verifying" && !authorityCommitted && !authorityBoundaryStarted && !aborted() && !(error instanceof VerificationAbortedError))
|
|
2103
|
+
return {
|
|
2104
|
+
state: "failed",
|
|
2105
|
+
operation: "qa",
|
|
2106
|
+
operation_id: operationId,
|
|
2107
|
+
reason: "QA execution failed before attestation (execution_failed)",
|
|
2108
|
+
environment_failure: true
|
|
2109
|
+
};
|
|
1967
2110
|
if (reviewPreparationStarted) {
|
|
1968
2111
|
const reason = aborted() || error instanceof VerificationAbortedError ? `${phase}: host cancellation` : `${phase}: ${boundedAssuranceError(error)}`;
|
|
1969
2112
|
return this.reviewPreparationFailed(taskId, operationId, reason);
|
|
@@ -1989,6 +2132,9 @@ class AssuranceCoordinator {
|
|
|
1989
2132
|
}
|
|
1990
2133
|
}
|
|
1991
2134
|
async submitReview(taskId, ctx, verdictInput) {
|
|
2135
|
+
return this.withRecovery(taskId, ctx, await this.submitReviewOnce(taskId, ctx, verdictInput));
|
|
2136
|
+
}
|
|
2137
|
+
async submitReviewOnce(taskId, ctx, verdictInput) {
|
|
1992
2138
|
const unknown = this.unknownOperations.get(taskId);
|
|
1993
2139
|
if (unknown)
|
|
1994
2140
|
return { state: "settlement_unknown", operation: unknown.operation, operation_id: unknown.operationId, reason: unknown.reason };
|
|
@@ -2004,12 +2150,12 @@ class AssuranceCoordinator {
|
|
|
2004
2150
|
} catch (error) {
|
|
2005
2151
|
const reason = boundedAssuranceError(error);
|
|
2006
2152
|
this.releaseReviewReservation(taskId, reservation, reason);
|
|
2007
|
-
return { state: "blocked", reason };
|
|
2153
|
+
return { state: "blocked", reason, recovery_error: "fresh_kernel_projection_unavailable" };
|
|
2008
2154
|
}
|
|
2009
2155
|
if (fresh.error || !fresh.claim || fresh.claim.task_id !== taskId || compareReservationSnapshot(reservation.snapshot, fresh.projection).length > 0 || fresh.projection.lifecycle !== reservation.snapshot.lifecycle || fresh.projection.artifact_state !== reservation.snapshot.artifact_state) {
|
|
2010
2156
|
const reason = fresh.error ?? "assurance snapshot changed before Review submission";
|
|
2011
2157
|
this.releaseReviewReservation(taskId, reservation, reason);
|
|
2012
|
-
return { state: "blocked", reason };
|
|
2158
|
+
return { state: "blocked", reason, ...fresh.error || !fresh.claim || fresh.claim.task_id !== taskId ? { recovery_error: "fresh_kernel_projection_unavailable" } : {} };
|
|
2013
2159
|
}
|
|
2014
2160
|
if (reservation.snapshot.review_revision) {
|
|
2015
2161
|
try {
|
|
@@ -2177,11 +2323,13 @@ import {
|
|
|
2177
2323
|
} from "node:fs";
|
|
2178
2324
|
import { dirname as dirname3, join as join4, resolve as resolve2 } from "node:path";
|
|
2179
2325
|
function git(root, args) {
|
|
2180
|
-
const
|
|
2326
|
+
const diff = args[0] === "diff";
|
|
2327
|
+
const command = diff ? ["diff", "--exit-code", ...args.slice(1)] : args;
|
|
2328
|
+
const result = spawnSync("git", ["--no-optional-locks", "-c", "diff.autoRefreshIndex=false", "-C", root, ...command], {
|
|
2181
2329
|
encoding: "utf8",
|
|
2182
2330
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2183
2331
|
});
|
|
2184
|
-
return result.status === 0 ? result.stdout : null;
|
|
2332
|
+
return result.status === 0 || diff && result.status === 1 ? result.stdout : null;
|
|
2185
2333
|
}
|
|
2186
2334
|
function splitNull(value) {
|
|
2187
2335
|
return value.split("\x00").filter((entry) => entry.length > 0);
|
|
@@ -2300,12 +2448,14 @@ var portablePathCollator = new Intl.Collator("und", {
|
|
|
2300
2448
|
});
|
|
2301
2449
|
var gitTaskSnapshotTestHook;
|
|
2302
2450
|
function gitBytes(root, args) {
|
|
2303
|
-
const
|
|
2451
|
+
const diff = args[0] === "diff";
|
|
2452
|
+
const command = diff ? ["diff", "--exit-code", ...args.slice(1)] : args;
|
|
2453
|
+
const result = spawnSync("git", ["--no-optional-locks", "-c", "diff.autoRefreshIndex=false", "-C", root, ...command], {
|
|
2304
2454
|
encoding: null,
|
|
2305
2455
|
stdio: ["ignore", "pipe", "pipe"],
|
|
2306
2456
|
maxBuffer: 8 * 1024 * 1024
|
|
2307
2457
|
});
|
|
2308
|
-
if (result.status !== 0 || !Buffer.isBuffer(result.stdout)) {
|
|
2458
|
+
if (result.status !== 0 && !(diff && result.status === 1) || !Buffer.isBuffer(result.stdout)) {
|
|
2309
2459
|
throw new Error(`Git task snapshot command failed: git ${args.join(" ")}`);
|
|
2310
2460
|
}
|
|
2311
2461
|
return result.stdout;
|
|
@@ -2595,6 +2745,38 @@ function taskRevisionIdentity(projectRoot, scopeHint, baseHead, taskId) {
|
|
|
2595
2745
|
changed_paths: Object.keys(snapshot.changed_paths).sort(comparePaths)
|
|
2596
2746
|
};
|
|
2597
2747
|
}
|
|
2748
|
+
function taskCommitRevisionIdentity(projectRoot, scopeHint, baseHead, commit) {
|
|
2749
|
+
const requested = resolve2(projectRoot);
|
|
2750
|
+
if (lstatSync2(requested).isSymbolicLink())
|
|
2751
|
+
throw new Error("task commit root must be a real directory");
|
|
2752
|
+
const root = realpathSync3(requested), scope = assertCanonicalTaskScope(scopeHint);
|
|
2753
|
+
if (!GIT_OBJECT_ID.test(baseHead) || !GIT_OBJECT_ID.test(commit))
|
|
2754
|
+
throw new Error("invalid task commit identity");
|
|
2755
|
+
if (gitRequired(root, ["cat-file", "-t", baseHead], "unreadable task base") !== "commit" || gitRequired(root, ["cat-file", "-t", commit], "unreadable task commit") !== "commit" || git(root, ["merge-base", "--is-ancestor", baseHead, commit]) === null)
|
|
2756
|
+
throw new Error("invalid task commit ancestry");
|
|
2757
|
+
const paths = decodeNullPaths(gitBytes(root, ["diff", "--no-renames", "--name-only", "-z", baseHead, commit, "--"]), "task commit paths").filter((path) => !isNonDeliveryPath(path) && taskPathMatchesScope(path, scope)).sort(comparePaths);
|
|
2758
|
+
assertNoCaseFoldCollisions(paths, "task commit paths");
|
|
2759
|
+
const changed_paths = {};
|
|
2760
|
+
for (const path of paths) {
|
|
2761
|
+
const current = headEntry(root, commit, path), base = headEntry(root, baseHead, path);
|
|
2762
|
+
changed_paths[path] = {
|
|
2763
|
+
status: !base ? "added" : !current ? "deleted" : "modified",
|
|
2764
|
+
mode: current?.mode ?? null,
|
|
2765
|
+
oid: current?.oid ?? null,
|
|
2766
|
+
base_mode: base?.mode ?? null,
|
|
2767
|
+
base_oid: base?.oid ?? null
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
const snapshot = {
|
|
2771
|
+
kind: "git-task-revision-v1",
|
|
2772
|
+
repository_root: root,
|
|
2773
|
+
base_head: baseHead,
|
|
2774
|
+
base_tree: gitRequired(root, ["rev-parse", `${baseHead}^{tree}`], "unreadable task base tree"),
|
|
2775
|
+
scope,
|
|
2776
|
+
changed_paths
|
|
2777
|
+
};
|
|
2778
|
+
return { diff_hash: hashTaskSnapshot(snapshot), changed_paths: paths };
|
|
2779
|
+
}
|
|
2598
2780
|
function isGitWorkspaceSnapshot(value) {
|
|
2599
2781
|
return typeof value === "object" && value !== null && !Array.isArray(value) && value.kind === "git-workspace-v1" && typeof value.repository_root === "string" && typeof value.head === "string" && typeof value.dirty_files === "object" && value.dirty_files !== null;
|
|
2600
2782
|
}
|
|
@@ -5582,6 +5764,9 @@ function ensureSecureDirectory(root, relativePath) {
|
|
|
5582
5764
|
return target.path;
|
|
5583
5765
|
}
|
|
5584
5766
|
function readSecureProjectFile(root, relativePath) {
|
|
5767
|
+
return readSecureProjectBytes(root, relativePath).toString("utf8");
|
|
5768
|
+
}
|
|
5769
|
+
function readSecureProjectBytes(root, relativePath) {
|
|
5585
5770
|
const candidate = safeCandidate(root, relativePath);
|
|
5586
5771
|
assertNoSymlinkSegments(candidate.root, candidate.path);
|
|
5587
5772
|
const before = pathStatOrNull(candidate.path);
|
|
@@ -5597,7 +5782,7 @@ function readSecureProjectFile(root, relativePath) {
|
|
|
5597
5782
|
const opened = fstatSync4(fd);
|
|
5598
5783
|
if (opened.dev !== before.dev || opened.ino !== before.ino)
|
|
5599
5784
|
throw new KernelStoreSecurityError(`source identity changed: ${relativePath}`);
|
|
5600
|
-
const content = readFileSync8(fd
|
|
5785
|
+
const content = readFileSync8(fd);
|
|
5601
5786
|
const after = lstatSync7(candidate.path);
|
|
5602
5787
|
if (after.dev !== opened.dev || after.ino !== opened.ino)
|
|
5603
5788
|
throw new KernelStoreSecurityError(`source identity changed: ${relativePath}`);
|
|
@@ -7857,7 +8042,6 @@ function initializeEnrollmentGitBase(root, input, previous, base, signal) {
|
|
|
7857
8042
|
|
|
7858
8043
|
// plugins/immune-brain/runtime/assurance/qa.ts
|
|
7859
8044
|
import { createHash as createHash17 } from "node:crypto";
|
|
7860
|
-
|
|
7861
8045
|
// plugins/immune-brain/runtime/assurance/delivery_workspace.ts
|
|
7862
8046
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
7863
8047
|
import { createHash as createHash16 } from "node:crypto";
|
|
@@ -8117,19 +8301,6 @@ function deliveryTreeForSnapshot(snapshot, writeTree) {
|
|
|
8117
8301
|
throw new Error("QA delivery identity does not match the frozen snapshot");
|
|
8118
8302
|
return writeDeliveryTree(snapshot.root, captured);
|
|
8119
8303
|
}
|
|
8120
|
-
|
|
8121
|
-
class QaPreparationError extends Error {
|
|
8122
|
-
stage;
|
|
8123
|
-
acceptance_ids;
|
|
8124
|
-
reason;
|
|
8125
|
-
constructor(stage, acceptance_ids, reason) {
|
|
8126
|
-
super(`QA ${stage} failed (${reason}); affected checks=${acceptance_ids.join(",")}`);
|
|
8127
|
-
this.stage = stage;
|
|
8128
|
-
this.acceptance_ids = acceptance_ids;
|
|
8129
|
-
this.reason = reason;
|
|
8130
|
-
this.name = "QaPreparationError";
|
|
8131
|
-
}
|
|
8132
|
-
}
|
|
8133
8304
|
async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
8134
8305
|
if (snapshot.role !== "qa")
|
|
8135
8306
|
throw new Error("deterministic QA requires qa role");
|
|
@@ -8157,46 +8328,80 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
|
8157
8328
|
live();
|
|
8158
8329
|
const ids = group.map((item) => item.id);
|
|
8159
8330
|
const environment = group[0].descriptor.environment;
|
|
8160
|
-
const
|
|
8161
|
-
|
|
8331
|
+
const diagnostics = (stage, outcome, affected = group, commandStage = "check", elapsed = 0, result) => affected.map((item) => ({
|
|
8332
|
+
acceptance_id: item.id,
|
|
8333
|
+
descriptor_ref: `acceptance/${item.index}/verification/${commandStage === "prepare" ? "environment/prepare" : "command"}`,
|
|
8334
|
+
descriptor_digest: `sha256:${createHash17("sha256").update(JSON.stringify(item.descriptor)).digest("hex")}`,
|
|
8335
|
+
stage,
|
|
8336
|
+
outcome,
|
|
8337
|
+
elapsed_ms: elapsed,
|
|
8338
|
+
exit_code: result?.exit_code ?? null,
|
|
8339
|
+
stdout_bytes: result ? Buffer.byteLength(result.stdout) : null,
|
|
8340
|
+
stderr_bytes: result ? Buffer.byteLength(result.stderr) : null
|
|
8341
|
+
}));
|
|
8342
|
+
const { delivery, home, deliveryPrefix } = (() => {
|
|
8343
|
+
let delivery, home;
|
|
8344
|
+
try {
|
|
8345
|
+
delivery = (options._materializeDeliveryWorkspace ?? materializeDeliveryWorkspace)(snapshot.root, tree);
|
|
8346
|
+
home = mkdtempSync3(join9(tmpdir4(), "imm-qa-home-"));
|
|
8347
|
+
return { delivery, home, deliveryPrefix: `${realpathSync10(delivery.root)}${sep6}` };
|
|
8348
|
+
} catch {
|
|
8349
|
+
let reason = "delivery_unavailable";
|
|
8350
|
+
try {
|
|
8351
|
+
try {
|
|
8352
|
+
delivery?.cleanup();
|
|
8353
|
+
} finally {
|
|
8354
|
+
if (home)
|
|
8355
|
+
removeTaskOwnedTree(home);
|
|
8356
|
+
}
|
|
8357
|
+
} catch {
|
|
8358
|
+
reason = "delivery_cleanup_failed";
|
|
8359
|
+
}
|
|
8360
|
+
throw new QaPreparationError("prepare", ids, reason, diagnostics("prepare", reason));
|
|
8361
|
+
}
|
|
8362
|
+
})();
|
|
8162
8363
|
const clean = () => {
|
|
8163
8364
|
if (options._materializeDeliveryWorkspace)
|
|
8164
8365
|
return;
|
|
8165
8366
|
try {
|
|
8166
8367
|
assertDeliveryClean(delivery.root, tree, delivery.seal, environment.writable_paths);
|
|
8167
8368
|
} catch {
|
|
8168
|
-
throw new QaPreparationError("integrity", ids, "protected_input_or_output_drift");
|
|
8369
|
+
throw new QaPreparationError("integrity", ids, "protected_input_or_output_drift", diagnostics("integrity", "protected_input_or_output_drift"));
|
|
8169
8370
|
}
|
|
8170
8371
|
};
|
|
8171
8372
|
const groupCommands = [];
|
|
8172
|
-
const
|
|
8173
|
-
|
|
8373
|
+
const execute = async (command, stage, affected = group) => {
|
|
8374
|
+
const started = performance.now();
|
|
8375
|
+
const fail = (errorStage, reason) => new QaPreparationError(errorStage, affected.map((item) => item.id), reason, diagnostics(errorStage, reason, affected, stage, Math.round(performance.now() - started)));
|
|
8174
8376
|
live();
|
|
8175
8377
|
clean();
|
|
8176
8378
|
let frozen;
|
|
8177
8379
|
try {
|
|
8178
8380
|
frozen = (options._resolveVerificationCommand ?? resolveVerificationCommand)(delivery.root, command, path);
|
|
8179
8381
|
} catch {
|
|
8180
|
-
throw
|
|
8382
|
+
throw fail("resolution", "executable_or_cwd_unavailable");
|
|
8181
8383
|
}
|
|
8182
8384
|
let result;
|
|
8183
8385
|
try {
|
|
8184
8386
|
result = await (options._runFixedVerification ?? runFixedVerification)(delivery.root, command, frozen, { signal: options.signal, home, path });
|
|
8185
8387
|
} catch (error) {
|
|
8388
|
+
if (error instanceof VerificationAbortedError)
|
|
8389
|
+
throw error;
|
|
8186
8390
|
if (error instanceof VerificationLaunchError)
|
|
8187
|
-
throw
|
|
8391
|
+
throw fail("resolution", "process_launch_failed");
|
|
8188
8392
|
if (error instanceof VerificationCleanupError)
|
|
8189
|
-
throw
|
|
8190
|
-
throw
|
|
8393
|
+
throw fail("resolution", "process_cleanup_failed");
|
|
8394
|
+
throw fail(stage, "execution_failed");
|
|
8191
8395
|
}
|
|
8192
8396
|
live();
|
|
8193
8397
|
clean();
|
|
8194
8398
|
groupCommands.push(frozen);
|
|
8195
8399
|
if (frozen?.entry) {
|
|
8400
|
+
const identityDiagnostics = diagnostics("integrity", "command_identity_changed", affected, stage, Math.round(performance.now() - started), result);
|
|
8196
8401
|
if (!command.executable.startsWith("./"))
|
|
8197
|
-
allCommands.push(frozen);
|
|
8402
|
+
allCommands.push({ frozen, diagnostics: identityDiagnostics });
|
|
8198
8403
|
else if (frozen.interpreter && !frozen.interpreter.path.startsWith(deliveryPrefix))
|
|
8199
|
-
allCommands.push({ entry: frozen.interpreter, interpreter: null, interpreter_args: [] });
|
|
8404
|
+
allCommands.push({ frozen: { entry: frozen.interpreter, interpreter: null, interpreter_args: [] }, diagnostics: identityDiagnostics });
|
|
8200
8405
|
}
|
|
8201
8406
|
evidence.push({
|
|
8202
8407
|
stage,
|
|
@@ -8209,29 +8414,31 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
|
8209
8414
|
output_limited: result.output_limited
|
|
8210
8415
|
});
|
|
8211
8416
|
if (Buffer.byteLength(JSON.stringify(evidence)) > 16384)
|
|
8212
|
-
throw
|
|
8417
|
+
throw fail("prepare", "execution_metadata_limit_exceeded");
|
|
8213
8418
|
return result;
|
|
8214
8419
|
};
|
|
8215
8420
|
try {
|
|
8216
8421
|
clean();
|
|
8217
8422
|
if (environment.prepare) {
|
|
8423
|
+
const prepareStarted = performance.now();
|
|
8218
8424
|
const result = await execute(environment.prepare, "prepare");
|
|
8219
8425
|
if (result.exit_code !== 0 || result.timed_out || result.output_limited)
|
|
8220
|
-
throw new QaPreparationError("prepare", ids, result.timed_out ? "timeout" : result.output_limited ? "output_limit" : "nonzero_exit");
|
|
8426
|
+
throw new QaPreparationError("prepare", ids, result.timed_out ? "timeout" : result.output_limited ? "output_limit" : "nonzero_exit", diagnostics("prepare", result.timed_out ? "timeout" : result.output_limited ? "output_limit" : "nonzero_exit", group, "prepare", Math.round(performance.now() - prepareStarted), result));
|
|
8221
8427
|
}
|
|
8222
8428
|
for (const item of group) {
|
|
8223
|
-
const started =
|
|
8224
|
-
const progress = (phase) => options.onProgress?.({
|
|
8429
|
+
const started = performance.now();
|
|
8430
|
+
const progress = (phase, outcome, result) => options.onProgress?.({
|
|
8225
8431
|
index: item.index + 1,
|
|
8226
8432
|
total: snapshot.acceptance.length,
|
|
8227
8433
|
acceptance_id: item.id,
|
|
8228
8434
|
phase,
|
|
8229
|
-
elapsed_ms:
|
|
8435
|
+
elapsed_ms: Math.round(performance.now() - started),
|
|
8436
|
+
diagnostic: diagnostics("check", outcome, [item], "check", Math.round(performance.now() - started), result)[0]
|
|
8230
8437
|
});
|
|
8231
|
-
progress("running");
|
|
8232
|
-
const result = await execute(item.descriptor.command, "check");
|
|
8438
|
+
progress("running", "running");
|
|
8439
|
+
const result = await execute(item.descriptor.command, "check", [item]);
|
|
8233
8440
|
const failed = result.exit_code !== 0 || result.timed_out || result.output_limited;
|
|
8234
|
-
progress(failed ? "failed" : "passed");
|
|
8441
|
+
progress(failed ? "failed" : "passed", result.timed_out ? "timeout" : result.output_limited ? "output_limit" : failed ? "nonzero_exit" : "passed", result);
|
|
8235
8442
|
if (failed)
|
|
8236
8443
|
findings.push({
|
|
8237
8444
|
id: qaFindingId(item.id, snapshotDigest(snapshot)),
|
|
@@ -8241,22 +8448,33 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
|
8241
8448
|
findings_digest: ""
|
|
8242
8449
|
});
|
|
8243
8450
|
}
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8451
|
+
try {
|
|
8452
|
+
for (const frozen of groupCommands)
|
|
8453
|
+
if (frozen?.entry)
|
|
8454
|
+
assertCommandIdentity(frozen);
|
|
8455
|
+
} catch {
|
|
8456
|
+
throw new QaPreparationError("integrity", ids, "command_identity_changed", diagnostics("integrity", "command_identity_changed"));
|
|
8247
8457
|
}
|
|
8248
8458
|
} finally {
|
|
8249
8459
|
try {
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8460
|
+
try {
|
|
8461
|
+
delivery.cleanup();
|
|
8462
|
+
} finally {
|
|
8463
|
+
removeTaskOwnedTree(home);
|
|
8464
|
+
}
|
|
8465
|
+
} catch {
|
|
8466
|
+
throw new QaPreparationError("integrity", ids, "delivery_cleanup_failed", diagnostics("integrity", "delivery_cleanup_failed"));
|
|
8253
8467
|
}
|
|
8254
8468
|
}
|
|
8255
8469
|
}
|
|
8256
8470
|
live();
|
|
8257
|
-
for (const
|
|
8258
|
-
|
|
8259
|
-
|
|
8471
|
+
for (const entry of allCommands) {
|
|
8472
|
+
try {
|
|
8473
|
+
if (entry.frozen?.entry)
|
|
8474
|
+
assertCommandIdentity(entry.frozen);
|
|
8475
|
+
} catch {
|
|
8476
|
+
throw new QaPreparationError("integrity", entry.diagnostics.map((item) => item.acceptance_id), "command_identity_changed", entry.diagnostics);
|
|
8477
|
+
}
|
|
8260
8478
|
}
|
|
8261
8479
|
const base = {
|
|
8262
8480
|
contract: "assurance_kernel/assurance_verdict/v2",
|
|
@@ -8296,6 +8514,11 @@ var BATCH_REASONS = Object.freeze({
|
|
|
8296
8514
|
reason: (detail) => `an active workspace claim already exists for task: ${detail}`,
|
|
8297
8515
|
recovery_action: "resolve or stop the active task before starting a batch in the current Host"
|
|
8298
8516
|
},
|
|
8517
|
+
kernel_authority_conflict: {
|
|
8518
|
+
state: "rejected",
|
|
8519
|
+
reason: (detail) => `Kernel authority conflict: ${detail}`,
|
|
8520
|
+
recovery_action: "resolve the Kernel storage conflict before resuming in the current Host"
|
|
8521
|
+
},
|
|
8299
8522
|
git_head_unreadable: {
|
|
8300
8523
|
state: "rejected",
|
|
8301
8524
|
reason: (detail) => detail,
|
|
@@ -8662,6 +8885,17 @@ function createGhTransport(binary = "gh") {
|
|
|
8662
8885
|
return new Promise((complete) => {
|
|
8663
8886
|
let stdout = Buffer.alloc(0);
|
|
8664
8887
|
let stderr = Buffer.alloc(0);
|
|
8888
|
+
const remaining = options.budget === undefined ? GH_TIMEOUT_MS : Math.min(GH_TIMEOUT_MS, options.budget.deadline_ms - Date.now());
|
|
8889
|
+
if (options.budget && (options.budget.signal?.aborted || remaining <= 0)) {
|
|
8890
|
+
complete({
|
|
8891
|
+
exit_code: 1,
|
|
8892
|
+
stdout: "",
|
|
8893
|
+
stderr: options.budget.signal?.aborted ? "operation cancelled by the caller" : "publication deadline exceeded before the next remote call",
|
|
8894
|
+
timed_out: true,
|
|
8895
|
+
output_exceeded: false
|
|
8896
|
+
});
|
|
8897
|
+
return;
|
|
8898
|
+
}
|
|
8665
8899
|
let timedOut = false;
|
|
8666
8900
|
let outputExceeded = false;
|
|
8667
8901
|
let timer;
|
|
@@ -8719,7 +8953,7 @@ function createGhTransport(binary = "gh") {
|
|
|
8719
8953
|
timer = setTimeout(() => {
|
|
8720
8954
|
timedOut = true;
|
|
8721
8955
|
child.kill("SIGKILL");
|
|
8722
|
-
},
|
|
8956
|
+
}, remaining);
|
|
8723
8957
|
child.once("close", (code) => {
|
|
8724
8958
|
finish(code ?? 1);
|
|
8725
8959
|
});
|
|
@@ -8823,6 +9057,33 @@ function initiativeLookup(issues, repositoryId, initiativeId) {
|
|
|
8823
9057
|
const initiative = marker("initiative-id", initiativeId);
|
|
8824
9058
|
return findIssue(issues, [initiative, KIND_INITIATIVE_MARKER], [marker("repo-id", repositoryId), initiative]);
|
|
8825
9059
|
}
|
|
9060
|
+
function publishedIntentHashMarkers(body) {
|
|
9061
|
+
return [...body.matchAll(/<!-- immune-brain:intent-hash=([A-Za-z0-9][A-Za-z0-9._:-]{0,127}) -->/g)].map((match) => match[1]);
|
|
9062
|
+
}
|
|
9063
|
+
function publishedIntentIdentity(body) {
|
|
9064
|
+
const hashes = publishedIntentHashMarkers(body);
|
|
9065
|
+
if (hashes.length === 0)
|
|
9066
|
+
return { kind: "absent" };
|
|
9067
|
+
if (hashes.length === 1)
|
|
9068
|
+
return { kind: "bound", hash: hashes[0] };
|
|
9069
|
+
return { kind: "ambiguous", hashes };
|
|
9070
|
+
}
|
|
9071
|
+
function publishedIntentMismatch(body, taskId, expected) {
|
|
9072
|
+
const identity = publishedIntentIdentity(body);
|
|
9073
|
+
if (identity.kind === "ambiguous")
|
|
9074
|
+
return `published Task ${taskId} carries ${identity.hashes.length} conflicting intent-hash markers; its published Intent identity is ambiguous`;
|
|
9075
|
+
if (identity.kind === "absent" || expected === undefined)
|
|
9076
|
+
return null;
|
|
9077
|
+
if (identity.hash !== expected)
|
|
9078
|
+
return `published Task ${taskId} was published from a different TaskIntent revision; the published identity no longer matches the Intent being published`;
|
|
9079
|
+
return null;
|
|
9080
|
+
}
|
|
9081
|
+
function sameTrackedBody(current, expected) {
|
|
9082
|
+
return stripPublishedIntentHash(current) === stripPublishedIntentHash(expected);
|
|
9083
|
+
}
|
|
9084
|
+
function stripPublishedIntentHash(body) {
|
|
9085
|
+
return body.replace(/<!-- immune-brain:intent-hash=[A-Za-z0-9][A-Za-z0-9._:-]{0,127} -->\n?/g, "");
|
|
9086
|
+
}
|
|
8826
9087
|
function ownershipMarkerValue(body, name) {
|
|
8827
9088
|
const values = [...body.matchAll(new RegExp(`<!-- immune-brain:${name}=([A-Za-z0-9][A-Za-z0-9._-]{0,127}) -->`, "g"))];
|
|
8828
9089
|
return values.length === 1 ? values[0][1] : null;
|
|
@@ -9123,7 +9384,8 @@ function childBody(repository, operation, parent) {
|
|
|
9123
9384
|
marker("repo-id", repository.id),
|
|
9124
9385
|
marker("initiative-id", operation.initiative_id),
|
|
9125
9386
|
marker("slice-id", operation.slice_id),
|
|
9126
|
-
marker("task-id", operation.task_id)
|
|
9387
|
+
marker("task-id", operation.task_id),
|
|
9388
|
+
...operation.intent_hash ? [marker("intent-hash", operation.intent_hash)] : []
|
|
9127
9389
|
].join(`
|
|
9128
9390
|
`)}
|
|
9129
9391
|
|
|
@@ -9292,8 +9554,11 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
|
|
|
9292
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);
|
|
9293
9555
|
return updatePendingChild(root, gh, source, operation, child, undefined, blockers, approvedFinal, amendmentContext);
|
|
9294
9556
|
}
|
|
9295
|
-
if (found.issue.body
|
|
9557
|
+
if (!sameTrackedBody(found.issue.body, body) || found.issue.title !== title)
|
|
9296
9558
|
return result(operation.op, "permanent_failure", "Task Issue already exists with a different title or Agent Brief; edit the GitHub source or retry the original projection before changing native relations", found.issue);
|
|
9559
|
+
const intentMismatch = publishedIntentMismatch(found.issue.body, operation.task_id, operation.intent_hash);
|
|
9560
|
+
if (intentMismatch)
|
|
9561
|
+
return result(operation.op, "ambiguous_remote_state", intentMismatch, found.issue);
|
|
9297
9562
|
const observedLabelArgs = labelMutationArgs(child.labels, desiredTaskLabels(operation));
|
|
9298
9563
|
if (observedLabelArgs.length) {
|
|
9299
9564
|
const edited = await gh.run([
|
|
@@ -9329,8 +9594,11 @@ async function upsertTask(root, gh, operation, source, pendingBinding = null, am
|
|
|
9329
9594
|
if ("contract" in finalSource)
|
|
9330
9595
|
return finalSource;
|
|
9331
9596
|
const finalChild = ownedTaskLookup(finalSource.issues, finalSource.repository.id, operation.task_id, operation.initiative_id, operation.slice_id);
|
|
9332
|
-
if (finalChild.kind !== "found" || finalChild.issue.id !== child.id || finalChild.issue.title !== title || finalChild.issue.body
|
|
9597
|
+
if (finalChild.kind !== "found" || finalChild.issue.id !== child.id || finalChild.issue.title !== title || !sameTrackedBody(finalChild.issue.body, body))
|
|
9333
9598
|
return result(operation.op, "ambiguous_remote_state", "Task Issue changed identity, title, or body during dependency publication", child);
|
|
9599
|
+
const finalIdentityMismatch = publishedIntentMismatch(finalChild.issue.body, operation.task_id, operation.intent_hash);
|
|
9600
|
+
if (finalIdentityMismatch)
|
|
9601
|
+
return result(operation.op, "ambiguous_remote_state", finalIdentityMismatch, child);
|
|
9334
9602
|
const finalChildOwnership = await confirmTerminalOwnership(root, gh, operation.op, finalSource, finalChild.issue);
|
|
9335
9603
|
if (!("owned" in finalChildOwnership))
|
|
9336
9604
|
return finalChildOwnership;
|
|
@@ -9782,7 +10050,7 @@ async function revalidatePendingChildBeforeWrite(root, gh, childNumber, childTas
|
|
|
9782
10050
|
import { existsSync as existsSync7, readdirSync as readdirSync6, readFileSync as readFileSync13 } from "node:fs";
|
|
9783
10051
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
9784
10052
|
import { join as join12 } from "node:path";
|
|
9785
|
-
import { spawnSync as
|
|
10053
|
+
import { spawnSync as spawnSync6 } from "node:child_process";
|
|
9786
10054
|
|
|
9787
10055
|
// plugins/immune-brain/runtime/kernel/batch_authority.ts
|
|
9788
10056
|
import { createHash as createHash18 } from "node:crypto";
|
|
@@ -10031,252 +10299,49 @@ function deriveChildEnrollment(root, registry, input) {
|
|
|
10031
10299
|
};
|
|
10032
10300
|
}
|
|
10033
10301
|
|
|
10034
|
-
// plugins/immune-brain/runtime/unattended/
|
|
10035
|
-
import {
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10044
|
-
|
|
10045
|
-
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10302
|
+
// plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts
|
|
10303
|
+
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
10304
|
+
|
|
10305
|
+
// plugins/immune-brain/runtime/unattended/batch_state.ts
|
|
10306
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync6, openSync as openSync6, closeSync as closeSync6, writeFileSync as writeFileSync6, renameSync as renameSync3, lstatSync as lstatSync9, constants as constants5, rmSync as rmSync6 } from "node:fs";
|
|
10307
|
+
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
10308
|
+
import { dirname as dirname7, join as join11 } from "node:path";
|
|
10309
|
+
var BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10310
|
+
var CHILD_RUN_STATES = new Set([
|
|
10311
|
+
"pending",
|
|
10312
|
+
"enrolled",
|
|
10313
|
+
"settled",
|
|
10314
|
+
"committed",
|
|
10315
|
+
"needs_human",
|
|
10316
|
+
"skipped_blocked"
|
|
10317
|
+
]);
|
|
10318
|
+
var BATCH_RUN_STATES = new Set([
|
|
10319
|
+
"prepared",
|
|
10320
|
+
"running",
|
|
10321
|
+
"needs_human",
|
|
10322
|
+
"completed",
|
|
10323
|
+
"budget_stopped",
|
|
10324
|
+
"failed",
|
|
10325
|
+
"rejected"
|
|
10326
|
+
]);
|
|
10327
|
+
var ISO_TIMESTAMP_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
10328
|
+
function isCanonicalTimestamp(value) {
|
|
10054
10329
|
if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN.test(value))
|
|
10055
|
-
|
|
10330
|
+
return false;
|
|
10056
10331
|
const milliseconds = Date.parse(value);
|
|
10057
|
-
|
|
10058
|
-
throw new Error(`${name} must be an ISO timestamp`);
|
|
10059
|
-
return { milliseconds, iso: new Date(milliseconds).toISOString() };
|
|
10060
|
-
}
|
|
10061
|
-
function normalizeObservation(value, initiativeSlug) {
|
|
10062
|
-
if (!value || value.contract !== "immune_brain/github_initiative_observation/v1")
|
|
10063
|
-
throw new Error("tracker returned an invalid Initiative observation contract");
|
|
10064
|
-
if (value.initiative_id !== initiativeSlug)
|
|
10065
|
-
throw new Error("tracker returned an observation for another Initiative");
|
|
10066
|
-
if (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0)
|
|
10067
|
-
throw new Error("tracker returned an invalid Initiative Issue number");
|
|
10068
|
-
if (!Array.isArray(value.tasks))
|
|
10069
|
-
throw new Error("tracker returned an invalid Initiative Task list");
|
|
10070
|
-
const taskIds = new Set;
|
|
10071
|
-
const sliceIds = new Set;
|
|
10072
|
-
const tasks = value.tasks.map((task, index) => {
|
|
10073
|
-
if (!task || typeof task !== "object")
|
|
10074
|
-
throw new Error(`tracker Task ${index} is invalid`);
|
|
10075
|
-
if (!ID_PATTERN2.test(task.task_id))
|
|
10076
|
-
throw new Error(`tracker Task ${index} has an invalid task_id`);
|
|
10077
|
-
if (!ID_PATTERN2.test(task.slice_id))
|
|
10078
|
-
throw new Error(`tracker Task ${index} has an invalid slice_id`);
|
|
10079
|
-
if (!Number.isSafeInteger(task.issue_number) || task.issue_number <= 0)
|
|
10080
|
-
throw new Error(`tracker Task ${task.task_id} has an invalid Issue number`);
|
|
10081
|
-
if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN2.test(id)))
|
|
10082
|
-
throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
|
|
10083
|
-
if (taskIds.has(task.task_id))
|
|
10084
|
-
throw new Error(`tracker returned duplicate Task ${task.task_id}`);
|
|
10085
|
-
if (sliceIds.has(task.slice_id))
|
|
10086
|
-
throw new Error(`tracker returned duplicate Slice ${task.slice_id}`);
|
|
10087
|
-
taskIds.add(task.task_id);
|
|
10088
|
-
sliceIds.add(task.slice_id);
|
|
10089
|
-
const blockedBy = [...task.blocked_by].sort();
|
|
10090
|
-
if (new Set(blockedBy).size !== blockedBy.length || blockedBy.includes(task.task_id))
|
|
10091
|
-
throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
|
|
10092
|
-
return { ...task, blocked_by: blockedBy };
|
|
10093
|
-
});
|
|
10094
|
-
for (const task of tasks) {
|
|
10095
|
-
const unknown = task.blocked_by.find((id) => !taskIds.has(id));
|
|
10096
|
-
if (unknown)
|
|
10097
|
-
throw new Error(`tracker Task ${task.task_id} depends on unknown Task ${unknown}`);
|
|
10098
|
-
}
|
|
10099
|
-
return { ...value, tasks: tasks.sort((left, right) => compareIds(left.task_id, right.task_id)) };
|
|
10332
|
+
return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === value;
|
|
10100
10333
|
}
|
|
10101
|
-
function
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
const order = [];
|
|
10105
|
-
while (remaining.size) {
|
|
10106
|
-
const ready = [...remaining.values()].filter((task) => task.blocked_by.every((id) => done.has(id))).sort((left, right) => compareIds(left.task_id, right.task_id));
|
|
10107
|
-
if (!ready.length)
|
|
10108
|
-
throw new Error("Initiative Task dependencies must form an acyclic graph");
|
|
10109
|
-
for (const task of ready) {
|
|
10110
|
-
remaining.delete(task.task_id);
|
|
10111
|
-
done.add(task.task_id);
|
|
10112
|
-
order.push(task);
|
|
10113
|
-
}
|
|
10114
|
-
}
|
|
10115
|
-
return order;
|
|
10334
|
+
function validateBatchId(batchId) {
|
|
10335
|
+
if (typeof batchId !== "string" || !BATCH_ID_PATTERN.test(batchId))
|
|
10336
|
+
throw new Error("batch id is not a safe file identity");
|
|
10116
10337
|
}
|
|
10117
|
-
function
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
const closure = new Set;
|
|
10121
|
-
for (const dependency of task.blocked_by) {
|
|
10122
|
-
closure.add(dependency);
|
|
10123
|
-
for (const transitive of closures.get(dependency) ?? [])
|
|
10124
|
-
closure.add(transitive);
|
|
10125
|
-
}
|
|
10126
|
-
closures.set(task.task_id, [...closure].sort());
|
|
10127
|
-
}
|
|
10128
|
-
return closures;
|
|
10338
|
+
function statePath(batchId) {
|
|
10339
|
+
validateBatchId(batchId);
|
|
10340
|
+
return join11(".imm", "state", "batches", `${batchId}.json`);
|
|
10129
10341
|
}
|
|
10130
|
-
function
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
throw new Error("budget.max_children exceeds the enrollable Child count");
|
|
10134
|
-
const qaFailureLimit = input.budget?.qa_failure_limit === undefined ? DEFAULT_QA_FAILURE_LIMIT : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
|
|
10135
|
-
const deadline = input.budget?.deadline_at === undefined ? new Date(confirmationTime.milliseconds + DEFAULT_DEADLINE_MS).toISOString() : timestamp(input.budget.deadline_at, "budget.deadline_at").iso;
|
|
10136
|
-
if (Date.parse(deadline) <= confirmationTime.milliseconds)
|
|
10137
|
-
throw new Error("budget.deadline_at must be later than confirmation_time");
|
|
10138
|
-
return { max_children: maxChildren, deadline_at: deadline, qa_failure_limit: qaFailureLimit };
|
|
10139
|
-
}
|
|
10140
|
-
async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeGithubInitiative) {
|
|
10141
|
-
if (!ID_PATTERN2.test(initiativeSlug))
|
|
10142
|
-
throw new Error("initiative_slug is invalid");
|
|
10143
|
-
if (!input || typeof input !== "object")
|
|
10144
|
-
throw new Error("batch plan input is required");
|
|
10145
|
-
const confirmationTime = timestamp(input.confirmation_time, "confirmation_time");
|
|
10146
|
-
const trackerObservation = normalizeObservation(await readInitiative(root, initiativeSlug), initiativeSlug);
|
|
10147
|
-
const order = dependencyOrder(trackerObservation);
|
|
10148
|
-
const closures = dependencyClosures(order);
|
|
10149
|
-
const children = [];
|
|
10150
|
-
for (const task of order) {
|
|
10151
|
-
const blockedBy = closures.get(task.task_id);
|
|
10152
|
-
const child = { task_id: task.task_id, slice_id: task.slice_id, blocked_by: blockedBy };
|
|
10153
|
-
const tombstone = readTaskTombstone(root, task.task_id);
|
|
10154
|
-
const record = readTaskRecordRaw(root, task.task_id).record;
|
|
10155
|
-
if (tombstone && record)
|
|
10156
|
-
throw new Error(`Task ${task.task_id} has conflicting settled and active state`);
|
|
10157
|
-
if (tombstone) {
|
|
10158
|
-
children.push({ ...child, status: "already_settled", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
|
|
10159
|
-
continue;
|
|
10160
|
-
}
|
|
10161
|
-
if (record) {
|
|
10162
|
-
children.push({ ...child, status: "already_owned", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
|
|
10163
|
-
continue;
|
|
10164
|
-
}
|
|
10165
|
-
const intentPath = `docs/plans/${task.task_id}.intent.json`;
|
|
10166
|
-
try {
|
|
10167
|
-
const read = observeTaskIntent(root, task.task_id, intentPath);
|
|
10168
|
-
if (read.intent.risk === "critical") {
|
|
10169
|
-
children.push({
|
|
10170
|
-
...child,
|
|
10171
|
-
status: "needs_human",
|
|
10172
|
-
reason: "critical",
|
|
10173
|
-
intent_path: read.intent_ref.path,
|
|
10174
|
-
intent_revision: read.intent_ref.revision,
|
|
10175
|
-
intent_content_hash: read.content_hash
|
|
10176
|
-
});
|
|
10177
|
-
continue;
|
|
10178
|
-
}
|
|
10179
|
-
const binding = inspectSpecBinding(read.intent);
|
|
10180
|
-
if (!binding.ok) {
|
|
10181
|
-
children.push({
|
|
10182
|
-
...child,
|
|
10183
|
-
status: "needs_human",
|
|
10184
|
-
reason: specBindingReason(binding),
|
|
10185
|
-
intent_path: read.intent_ref.path,
|
|
10186
|
-
intent_revision: read.intent_ref.revision,
|
|
10187
|
-
intent_content_hash: read.content_hash
|
|
10188
|
-
});
|
|
10189
|
-
continue;
|
|
10190
|
-
}
|
|
10191
|
-
children.push({
|
|
10192
|
-
...child,
|
|
10193
|
-
status: "enrollable",
|
|
10194
|
-
reason: null,
|
|
10195
|
-
intent_path: read.intent_ref.path,
|
|
10196
|
-
intent_revision: read.intent_ref.revision,
|
|
10197
|
-
intent_content_hash: read.content_hash
|
|
10198
|
-
});
|
|
10199
|
-
} catch (error) {
|
|
10200
|
-
if (!(error instanceof TaskIntentObservationError))
|
|
10201
|
-
throw error;
|
|
10202
|
-
children.push({ ...child, status: "needs_human", reason: "invalid_intent", intent_path: null, intent_revision: null, intent_content_hash: null });
|
|
10203
|
-
}
|
|
10204
|
-
}
|
|
10205
|
-
const directDependencies = new Map(order.map((task) => [task.task_id, task.blocked_by]));
|
|
10206
|
-
const childById = new Map(children.map((child) => [child.task_id, child]));
|
|
10207
|
-
for (const child of children) {
|
|
10208
|
-
if (child.status !== "enrollable")
|
|
10209
|
-
continue;
|
|
10210
|
-
if (directDependencies.get(child.task_id).some((id) => {
|
|
10211
|
-
const status = childById.get(id).status;
|
|
10212
|
-
return status === "already_owned" || status === "needs_human" || status === "blocked";
|
|
10213
|
-
})) {
|
|
10214
|
-
child.status = "blocked";
|
|
10215
|
-
child.reason = "dependency_unavailable";
|
|
10216
|
-
}
|
|
10217
|
-
}
|
|
10218
|
-
const enrollable = children.filter((child) => child.status === "enrollable").map((child) => ({
|
|
10219
|
-
task_id: child.task_id,
|
|
10220
|
-
intent_path: child.intent_path,
|
|
10221
|
-
intent_revision: child.intent_revision,
|
|
10222
|
-
intent_content_hash: child.intent_content_hash,
|
|
10223
|
-
blocked_by: child.blocked_by.filter((id) => childById.get(id)?.status === "enrollable")
|
|
10224
|
-
}));
|
|
10225
|
-
if (!enrollable.length)
|
|
10226
|
-
throw new Error("batch plan has no enrollable children; nothing to confirm");
|
|
10227
|
-
const planDigest = `sha256:${createHash19("sha256").update(stableStringify(enrollable)).digest("hex")}`;
|
|
10228
|
-
return {
|
|
10229
|
-
contract: "assurance_kernel/batch_plan/v1",
|
|
10230
|
-
initiative_slug: initiativeSlug,
|
|
10231
|
-
confirmation_time: confirmationTime.iso,
|
|
10232
|
-
tracker_observation: trackerObservation,
|
|
10233
|
-
children,
|
|
10234
|
-
enrollable,
|
|
10235
|
-
plan_digest: planDigest,
|
|
10236
|
-
budget: budget(input, enrollable.length, confirmationTime)
|
|
10237
|
-
};
|
|
10238
|
-
}
|
|
10239
|
-
|
|
10240
|
-
// plugins/immune-brain/runtime/unattended/batch_state.ts
|
|
10241
|
-
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";
|
|
10242
|
-
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
10243
|
-
import { dirname as dirname7, join as join11 } from "node:path";
|
|
10244
|
-
var BATCH_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10245
|
-
var CHILD_RUN_STATES = new Set([
|
|
10246
|
-
"pending",
|
|
10247
|
-
"enrolled",
|
|
10248
|
-
"settled",
|
|
10249
|
-
"committed",
|
|
10250
|
-
"needs_human",
|
|
10251
|
-
"skipped_blocked"
|
|
10252
|
-
]);
|
|
10253
|
-
var BATCH_RUN_STATES = new Set([
|
|
10254
|
-
"prepared",
|
|
10255
|
-
"running",
|
|
10256
|
-
"needs_human",
|
|
10257
|
-
"completed",
|
|
10258
|
-
"budget_stopped",
|
|
10259
|
-
"failed",
|
|
10260
|
-
"rejected"
|
|
10261
|
-
]);
|
|
10262
|
-
var ISO_TIMESTAMP_PATTERN2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
10263
|
-
function isCanonicalTimestamp(value) {
|
|
10264
|
-
if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN2.test(value))
|
|
10265
|
-
return false;
|
|
10266
|
-
const milliseconds = Date.parse(value);
|
|
10267
|
-
return Number.isFinite(milliseconds) && new Date(milliseconds).toISOString() === value;
|
|
10268
|
-
}
|
|
10269
|
-
function validateBatchId(batchId) {
|
|
10270
|
-
if (typeof batchId !== "string" || !BATCH_ID_PATTERN.test(batchId))
|
|
10271
|
-
throw new Error("batch id is not a safe file identity");
|
|
10272
|
-
}
|
|
10273
|
-
function statePath(batchId) {
|
|
10274
|
-
validateBatchId(batchId);
|
|
10275
|
-
return join11(".imm", "state", "batches", `${batchId}.json`);
|
|
10276
|
-
}
|
|
10277
|
-
function canonicalBytes(record) {
|
|
10278
|
-
return `${JSON.stringify(record, null, 2)}
|
|
10279
|
-
`;
|
|
10342
|
+
function canonicalBytes(record) {
|
|
10343
|
+
return `${JSON.stringify(record, null, 2)}
|
|
10344
|
+
`;
|
|
10280
10345
|
}
|
|
10281
10346
|
function validateRecordShape(value, batchId) {
|
|
10282
10347
|
if (typeof value !== "object" || value === null)
|
|
@@ -10366,13 +10431,17 @@ function prepareBatchRunState(input) {
|
|
|
10366
10431
|
};
|
|
10367
10432
|
return prepared;
|
|
10368
10433
|
}
|
|
10434
|
+
function parseBatchRunState(raw, batchId) {
|
|
10435
|
+
validateBatchId(batchId);
|
|
10436
|
+
const parsed = JSON.parse(raw);
|
|
10437
|
+
validateRecordShape(parsed, batchId);
|
|
10438
|
+
return parsed;
|
|
10439
|
+
}
|
|
10369
10440
|
function readBatchRunState(root, batchId) {
|
|
10370
10441
|
const path = statePath(batchId);
|
|
10371
10442
|
if (!existsSync6(join11(root, path)))
|
|
10372
10443
|
return null;
|
|
10373
|
-
|
|
10374
|
-
validateRecordShape(parsed, batchId);
|
|
10375
|
-
return parsed;
|
|
10444
|
+
return parseBatchRunState(readSecureProjectFile(root, path), batchId);
|
|
10376
10445
|
}
|
|
10377
10446
|
function ensureSecureDirectory2(root, relative) {
|
|
10378
10447
|
const target = join11(root, relative);
|
|
@@ -10412,6 +10481,18 @@ function writeFileAtomically(root, relative, bytes) {
|
|
|
10412
10481
|
}
|
|
10413
10482
|
}
|
|
10414
10483
|
}
|
|
10484
|
+
function replaceBatchRunState(root, expected, next, validate) {
|
|
10485
|
+
const path = statePath(next.batch_id);
|
|
10486
|
+
validateRecordShape(next, next.batch_id);
|
|
10487
|
+
return withKernelStoreLock(root, () => {
|
|
10488
|
+
if (!readSecureProjectBytes(root, path).equals(expected))
|
|
10489
|
+
throw new Error("batch state CAS mismatch");
|
|
10490
|
+
validate();
|
|
10491
|
+
const stored = { ...next, updated_at: new Date().toISOString() };
|
|
10492
|
+
writeFileAtomically(root, path, canonicalBytes(stored));
|
|
10493
|
+
return stored;
|
|
10494
|
+
});
|
|
10495
|
+
}
|
|
10415
10496
|
function writeBatchRunState(root, record) {
|
|
10416
10497
|
const path = statePath(record.batch_id);
|
|
10417
10498
|
validateRecordShape(record, record.batch_id);
|
|
@@ -10459,6 +10540,428 @@ function isTerminalBatchState(state) {
|
|
|
10459
10540
|
return state === "completed" || state === "budget_stopped" || state === "failed" || state === "rejected";
|
|
10460
10541
|
}
|
|
10461
10542
|
|
|
10543
|
+
// plugins/immune-brain/runtime/unattended/batch_reconfirmation.ts
|
|
10544
|
+
var oid = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/;
|
|
10545
|
+
var id = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10546
|
+
var batchAuthor = process.env.GIT_AUTHOR_NAME || "Immune-Brain Batch";
|
|
10547
|
+
var decode = (bytes) => new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
10548
|
+
function refuse() {
|
|
10549
|
+
throw new Error("plan_digest mismatch: batch plan reconfirmation is not eligible");
|
|
10550
|
+
}
|
|
10551
|
+
function matchesLocalProof(localJson, exported) {
|
|
10552
|
+
return localJson !== null && JSON.stringify(Object.entries(JSON.parse(localJson)).sort()) === JSON.stringify(Object.entries(exported).sort());
|
|
10553
|
+
}
|
|
10554
|
+
function git4(root, args) {
|
|
10555
|
+
const r = spawnSync5("git", ["-C", root, ...args], { encoding: "buffer", maxBuffer: 262144, env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
|
|
10556
|
+
if (r.status !== 0)
|
|
10557
|
+
refuse();
|
|
10558
|
+
return decode(r.stdout);
|
|
10559
|
+
}
|
|
10560
|
+
function ownUnpersistedBatchHead(root, record, head) {
|
|
10561
|
+
try {
|
|
10562
|
+
if (!oid.test(head) || !oid.test(record.base_head) || !id.test(record.batch_id) || record.commits.length || record.children[0]?.state !== "settled" || record.children.some((c) => c.commit !== null))
|
|
10563
|
+
return false;
|
|
10564
|
+
const task = record.children[0].task_id;
|
|
10565
|
+
if (!id.test(task))
|
|
10566
|
+
return false;
|
|
10567
|
+
const evidence = JSON.parse(decode(readSecureProjectBytes(root, `.imm/state/batches/commits/${record.batch_id}-${task}.json`)));
|
|
10568
|
+
if (evidence.contract !== "assurance_kernel/batch_commit_evidence/v1" || evidence.batch_id !== record.batch_id || evidence.task_id !== task || evidence.commit !== head || evidence.parent_head !== record.base_head)
|
|
10569
|
+
return false;
|
|
10570
|
+
if (git4(root, ["symbolic-ref", "--short", "HEAD"]).trim() !== record.branch)
|
|
10571
|
+
return false;
|
|
10572
|
+
const parents = git4(root, ["rev-list", "--parents", "-n", "1", head]).trim().split(/\s+/).slice(1);
|
|
10573
|
+
if (parents.length !== 1 || parents[0] !== record.base_head)
|
|
10574
|
+
return false;
|
|
10575
|
+
const metadata = git4(root, ["show", "-s", "--format=%an%x00%s%x00%(trailers:key=Immune-Brain-Batch,valueonly)", head]).split("\x00");
|
|
10576
|
+
if (metadata[0] !== batchAuthor || !metadata[1]?.startsWith(`imm(${task}):`) || metadata[2]?.trim() !== record.batch_id)
|
|
10577
|
+
return false;
|
|
10578
|
+
const run = localRunId(root, task);
|
|
10579
|
+
if (!run || !id.test(run))
|
|
10580
|
+
return false;
|
|
10581
|
+
const pair = readAuditTaskPair(root, task, run);
|
|
10582
|
+
if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4" || pair.record.lifecycle !== "done" || pair.proof.terminal_lifecycle !== "done")
|
|
10583
|
+
return false;
|
|
10584
|
+
const local = withKernelRead(root, (db) => readRunRowByTask(db, task));
|
|
10585
|
+
if (!local || local.run_id !== run || local.state !== "done" || !matchesLocalProof(local.terminal_proof_json, pair.proof))
|
|
10586
|
+
return false;
|
|
10587
|
+
for (const file of ["task-record.json", "terminal-proof.json"]) {
|
|
10588
|
+
const path = `.imm/audit/${task}/${run}/${file}`;
|
|
10589
|
+
if (!readSecureProjectBytes(root, path).equals(Buffer.from(git4(root, ["show", `${head}:${path}`]))))
|
|
10590
|
+
return false;
|
|
10591
|
+
}
|
|
10592
|
+
const paths = git4(root, ["diff-tree", "--no-commit-id", "--name-only", "--no-renames", "-z", "-r", head]).split("\x00").filter(Boolean);
|
|
10593
|
+
if (!paths.length || paths.some((path) => !path.startsWith(`.imm/audit/${task}/${run}/`) && !pair.record.intent_snapshot.scope_hint.some((s) => pathMatchesScope(path, s))))
|
|
10594
|
+
return false;
|
|
10595
|
+
const identity = taskCommitRevisionIdentity(root, pair.record.intent_snapshot.scope_hint, pair.record.git_base_head, head);
|
|
10596
|
+
if (!projectTask(pair.record.intent_snapshot, pair.record, identity.diff_hash, pair.record.intent_ref.content_hash, identity.changed_paths).complete)
|
|
10597
|
+
return false;
|
|
10598
|
+
return git4(root, ["rev-parse", "HEAD"]).trim() === head;
|
|
10599
|
+
} catch {
|
|
10600
|
+
return false;
|
|
10601
|
+
}
|
|
10602
|
+
}
|
|
10603
|
+
var confirmations = new Map;
|
|
10604
|
+
function retainReconfirmation(nonce, expires, snapshot) {
|
|
10605
|
+
for (const [key, value] of confirmations)
|
|
10606
|
+
if (value.expires <= Date.now())
|
|
10607
|
+
confirmations.delete(key);
|
|
10608
|
+
if (confirmations.has(nonce))
|
|
10609
|
+
refuse();
|
|
10610
|
+
confirmations.set(nonce, { snapshot, expires: Date.parse(expires) });
|
|
10611
|
+
}
|
|
10612
|
+
function takeReconfirmation(nonce) {
|
|
10613
|
+
const found = confirmations.get(nonce);
|
|
10614
|
+
confirmations.delete(nonce);
|
|
10615
|
+
if (!found || found.expires <= Date.now())
|
|
10616
|
+
refuse();
|
|
10617
|
+
return found.snapshot;
|
|
10618
|
+
}
|
|
10619
|
+
async function captureBatchReconfirmation(root, record, children) {
|
|
10620
|
+
if (!id.test(record.batch_id) || !oid.test(record.base_head) || isTerminalBatchState(record.batch_state) || record.commits.length || record.children.some((c) => c.commit !== null || c.state === "committed") || record.children.length !== children.length || !children.length)
|
|
10621
|
+
refuse();
|
|
10622
|
+
const assertNoCommitEvidence = () => {
|
|
10623
|
+
for (const child of record.children) {
|
|
10624
|
+
if (!id.test(child.task_id))
|
|
10625
|
+
refuse();
|
|
10626
|
+
const path = `.imm/state/batches/commits/${record.batch_id}-${child.task_id}.json`;
|
|
10627
|
+
let missing = false;
|
|
10628
|
+
try {
|
|
10629
|
+
readSecureProjectBytes(root, path);
|
|
10630
|
+
} catch (error) {
|
|
10631
|
+
if (error instanceof Error && error.message === `source_missing: ${path}`)
|
|
10632
|
+
missing = true;
|
|
10633
|
+
else
|
|
10634
|
+
refuse();
|
|
10635
|
+
}
|
|
10636
|
+
if (!missing)
|
|
10637
|
+
refuse();
|
|
10638
|
+
}
|
|
10639
|
+
};
|
|
10640
|
+
assertNoCommitEvidence();
|
|
10641
|
+
const statePath = `.imm/state/batches/${record.batch_id}.json`;
|
|
10642
|
+
const stateBytes = readSecureProjectBytes(root, statePath);
|
|
10643
|
+
if (JSON.stringify(JSON.parse(decode(stateBytes))) !== JSON.stringify(record))
|
|
10644
|
+
refuse();
|
|
10645
|
+
const files = new Map;
|
|
10646
|
+
const capture = (path) => {
|
|
10647
|
+
const bytes = readSecureProjectBytes(root, path);
|
|
10648
|
+
files.set(path, bytes);
|
|
10649
|
+
return bytes;
|
|
10650
|
+
};
|
|
10651
|
+
const oldChildren = [];
|
|
10652
|
+
const changed = [];
|
|
10653
|
+
let scope = null;
|
|
10654
|
+
const workspace = readWorkspaceStateRaw(root);
|
|
10655
|
+
if (workspace.state.current_working !== null)
|
|
10656
|
+
refuse();
|
|
10657
|
+
for (const [index, child] of children.entries()) {
|
|
10658
|
+
const previous = record.children[index];
|
|
10659
|
+
if (!id.test(child.task_id) || child.task_id !== previous.task_id || child.slice_id !== previous.slice_id || JSON.stringify(child.blocked_by) !== JSON.stringify(previous.blocked_by) || child.intent_path !== `docs/plans/${child.task_id}.intent.json`)
|
|
10660
|
+
refuse();
|
|
10661
|
+
const old = parseTaskIntentV1(JSON.parse(git4(root, ["show", `${record.base_head}:${child.intent_path}`])));
|
|
10662
|
+
if (old.task_id !== child.task_id)
|
|
10663
|
+
refuse();
|
|
10664
|
+
const oldHash = canonicalIntentHash(old);
|
|
10665
|
+
oldChildren.push({ task_id: child.task_id, blocked_by: [...child.blocked_by], intent_path: child.intent_path, intent_revision: old.revision, intent_content_hash: oldHash });
|
|
10666
|
+
const currentBytes = capture(child.intent_path);
|
|
10667
|
+
const current = parseTaskIntentV1(JSON.parse(decode(currentBytes)));
|
|
10668
|
+
const currentHash = canonicalIntentHash(current);
|
|
10669
|
+
const tracked = readTaskIntent(root, child.task_id, child.intent_path);
|
|
10670
|
+
if (currentHash !== tracked.content_hash || currentHash !== child.intent_content_hash || current.revision !== child.intent_revision)
|
|
10671
|
+
refuse();
|
|
10672
|
+
if (currentHash === oldHash)
|
|
10673
|
+
continue;
|
|
10674
|
+
if (scope !== null || previous.state === "pending" || previous.state === "skipped_blocked" || current.revision <= old.revision || current.goal !== old.goal || current.owner !== old.owner)
|
|
10675
|
+
refuse();
|
|
10676
|
+
const run = localRunId(root, child.task_id);
|
|
10677
|
+
if (!run || !id.test(run))
|
|
10678
|
+
refuse();
|
|
10679
|
+
const local = withKernelRead(root, (db) => readRunRowByTask(db, child.task_id));
|
|
10680
|
+
if (!local || local.run_id !== run || local.state !== "done" || local.enrollment_event_id !== `enroll-${child.task_id}-${local.created_at}` || Date.parse(local.created_at) > Date.parse(record.updated_at))
|
|
10681
|
+
refuse();
|
|
10682
|
+
const pair = readAuditTaskPair(root, child.task_id, run);
|
|
10683
|
+
if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4")
|
|
10684
|
+
refuse();
|
|
10685
|
+
const r = pair.record;
|
|
10686
|
+
if (r.lifecycle !== "done" || r.intent_ref.content_hash !== currentHash || canonicalIntentHash(r.intent_snapshot) !== currentHash || r.git_base_head !== record.base_head || pair.proof.terminal_lifecycle !== "done")
|
|
10687
|
+
refuse();
|
|
10688
|
+
const terminal = r.history.at(-1);
|
|
10689
|
+
if (terminal?.type !== "complete" || terminal.from_state !== "active:frozen" || terminal.to_state !== "done:frozen" || terminal.id !== pair.proof.terminal_event_id)
|
|
10690
|
+
refuse();
|
|
10691
|
+
const recordPath = `.imm/audit/${child.task_id}/${run}/task-record.json`;
|
|
10692
|
+
const proofPath = `.imm/audit/${child.task_id}/${run}/terminal-proof.json`;
|
|
10693
|
+
if (!capture(recordPath).equals(Buffer.from(local.record_json)))
|
|
10694
|
+
refuse();
|
|
10695
|
+
const capturedProof = JSON.parse(decode(capture(proofPath)));
|
|
10696
|
+
if (!matchesLocalProof(local.terminal_proof_json, capturedProof) || !matchesLocalProof(local.terminal_proof_json, pair.proof))
|
|
10697
|
+
refuse();
|
|
10698
|
+
const identity = taskRevisionIdentity(root, r.intent_snapshot.scope_hint, r.git_base_head, r.task_id);
|
|
10699
|
+
const projection = await projectAssurance(root, child.task_id, (cwd, task) => {
|
|
10700
|
+
if (task.contract !== "assurance_kernel/task_record/v4" || !task.git_base_head)
|
|
10701
|
+
refuse();
|
|
10702
|
+
return taskRevisionIdentity(cwd, task.intent_snapshot.scope_hint, task.git_base_head, task.task_id);
|
|
10703
|
+
});
|
|
10704
|
+
if (projection.error || projection.claim || projection.projection.run_id !== run || projection.projection.lifecycle !== "done" || !projection.projection.completion_ready || projection.projection.intent_content_hash !== currentHash || projection.projection.diff_hash !== identity.diff_hash || !projection.projection.fresh_approval_kinds.includes("qa") || projection.projection.risk !== "routine" && !projection.projection.fresh_approval_kinds.includes("review"))
|
|
10705
|
+
refuse();
|
|
10706
|
+
scope = r.intent_snapshot.scope_hint;
|
|
10707
|
+
changed.push({ task: child.task_id, run, identity: JSON.stringify(identity), local: JSON.stringify(local) });
|
|
10708
|
+
}
|
|
10709
|
+
if (computeBatchPlanDigest(oldChildren) !== record.plan_digest || changed.length !== 1 || scope === null)
|
|
10710
|
+
refuse();
|
|
10711
|
+
const confirmedScope = scope;
|
|
10712
|
+
const owner = changed[0];
|
|
10713
|
+
const status = () => git4(root, ["status", "--porcelain=v1", "-z", "--untracked-files=all", "--ignore-submodules=none"]);
|
|
10714
|
+
const statusBytes = status();
|
|
10715
|
+
const entries = statusBytes.split("\x00").filter(Boolean);
|
|
10716
|
+
for (const entry of entries) {
|
|
10717
|
+
const code = entry.slice(0, 2), path = entry.slice(3);
|
|
10718
|
+
if (code[1] !== " " || !["A", "M", "D"].includes(code[0]) || !path.startsWith(`.imm/audit/${owner.task}/${owner.run}/`) && !confirmedScope.some((s) => pathMatchesScope(path, s)))
|
|
10719
|
+
refuse();
|
|
10720
|
+
}
|
|
10721
|
+
const flagsBytes = git4(root, ["ls-files", "-v"]);
|
|
10722
|
+
const flags = flagsBytes.split(`
|
|
10723
|
+
`).filter(Boolean);
|
|
10724
|
+
if (flags.some((line) => line[0] === "S" || line[0] !== line[0]?.toUpperCase()))
|
|
10725
|
+
refuse();
|
|
10726
|
+
const head = () => git4(root, ["rev-parse", "HEAD"]).trim();
|
|
10727
|
+
const branch = () => git4(root, ["symbolic-ref", "--short", "HEAD"]).trim();
|
|
10728
|
+
if (head() !== record.base_head || branch() !== record.branch)
|
|
10729
|
+
refuse();
|
|
10730
|
+
const assertDeliveryUnchanged = () => {
|
|
10731
|
+
for (const c of changed) {
|
|
10732
|
+
const pair = readAuditTaskPair(root, c.task, c.run);
|
|
10733
|
+
if (!pair || pair.record.contract !== "assurance_kernel/task_record/v4" || !pair.record.git_base_head)
|
|
10734
|
+
refuse();
|
|
10735
|
+
const local = withKernelRead(root, (db) => readRunRowByTask(db, c.task));
|
|
10736
|
+
const diff = taskRevisionIdentity(root, pair.record.intent_snapshot.scope_hint, pair.record.git_base_head, c.task);
|
|
10737
|
+
if (localRunId(root, c.task) !== c.run || JSON.stringify(local) !== c.local || JSON.stringify(diff) !== c.identity)
|
|
10738
|
+
refuse();
|
|
10739
|
+
}
|
|
10740
|
+
};
|
|
10741
|
+
const assertUnchanged = () => {
|
|
10742
|
+
assertNoCommitEvidence();
|
|
10743
|
+
if (!readSecureProjectBytes(root, statePath).equals(stateBytes) || head() !== record.base_head || branch() !== record.branch || status() !== statusBytes || git4(root, ["ls-files", "-v"]) !== flagsBytes || readWorkspaceStateRaw(root).revision !== workspace.revision)
|
|
10744
|
+
refuse();
|
|
10745
|
+
for (const [path, bytes] of files)
|
|
10746
|
+
if (!readSecureProjectBytes(root, path).equals(bytes))
|
|
10747
|
+
refuse();
|
|
10748
|
+
assertDeliveryUnchanged();
|
|
10749
|
+
};
|
|
10750
|
+
assertUnchanged();
|
|
10751
|
+
const plan_digest = computeBatchPlanDigest(oldChildren.map((old, index) => ({
|
|
10752
|
+
...old,
|
|
10753
|
+
intent_revision: children[index].intent_revision,
|
|
10754
|
+
intent_content_hash: children[index].intent_content_hash
|
|
10755
|
+
})));
|
|
10756
|
+
return { root, plan_digest, stateBytes, assertUnchanged };
|
|
10757
|
+
}
|
|
10758
|
+
|
|
10759
|
+
// plugins/immune-brain/runtime/unattended/batch_plan.ts
|
|
10760
|
+
import { createHash as createHash19 } from "node:crypto";
|
|
10761
|
+
var ID_PATTERN2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10762
|
+
var DEFAULT_DEADLINE_MS = 8 * 60 * 60 * 1000;
|
|
10763
|
+
var DEFAULT_QA_FAILURE_LIMIT = 2;
|
|
10764
|
+
function specBindingReason(inspection) {
|
|
10765
|
+
if (inspection.missing.length > 0)
|
|
10766
|
+
return `spec_binding_incomplete: ${inspection.missing.join(", ")}`;
|
|
10767
|
+
return inspection.code === "binding_ambiguous" ? "spec_binding_ambiguous" : "spec_binding_missing";
|
|
10768
|
+
}
|
|
10769
|
+
function compareIds(left, right) {
|
|
10770
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
10771
|
+
}
|
|
10772
|
+
function positiveSafeInteger(value, name) {
|
|
10773
|
+
if (!Number.isSafeInteger(value) || value <= 0)
|
|
10774
|
+
throw new Error(`${name} must be a positive safe integer`);
|
|
10775
|
+
return value;
|
|
10776
|
+
}
|
|
10777
|
+
var ISO_TIMESTAMP_PATTERN2 = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/;
|
|
10778
|
+
function timestamp(value, name) {
|
|
10779
|
+
if (typeof value !== "string" || !ISO_TIMESTAMP_PATTERN2.test(value))
|
|
10780
|
+
throw new Error(`${name} must be an ISO timestamp`);
|
|
10781
|
+
const milliseconds = Date.parse(value);
|
|
10782
|
+
if (!Number.isFinite(milliseconds))
|
|
10783
|
+
throw new Error(`${name} must be an ISO timestamp`);
|
|
10784
|
+
return { milliseconds, iso: new Date(milliseconds).toISOString() };
|
|
10785
|
+
}
|
|
10786
|
+
function normalizeObservation(value, initiativeSlug) {
|
|
10787
|
+
if (!value || value.contract !== "immune_brain/github_initiative_observation/v1")
|
|
10788
|
+
throw new Error("tracker returned an invalid Initiative observation contract");
|
|
10789
|
+
if (value.initiative_id !== initiativeSlug)
|
|
10790
|
+
throw new Error("tracker returned an observation for another Initiative");
|
|
10791
|
+
if (!Number.isSafeInteger(value.issue_number) || value.issue_number <= 0)
|
|
10792
|
+
throw new Error("tracker returned an invalid Initiative Issue number");
|
|
10793
|
+
if (!Array.isArray(value.tasks))
|
|
10794
|
+
throw new Error("tracker returned an invalid Initiative Task list");
|
|
10795
|
+
const taskIds = new Set;
|
|
10796
|
+
const sliceIds = new Set;
|
|
10797
|
+
const tasks = value.tasks.map((task, index) => {
|
|
10798
|
+
if (!task || typeof task !== "object")
|
|
10799
|
+
throw new Error(`tracker Task ${index} is invalid`);
|
|
10800
|
+
if (!ID_PATTERN2.test(task.task_id))
|
|
10801
|
+
throw new Error(`tracker Task ${index} has an invalid task_id`);
|
|
10802
|
+
if (!ID_PATTERN2.test(task.slice_id))
|
|
10803
|
+
throw new Error(`tracker Task ${index} has an invalid slice_id`);
|
|
10804
|
+
if (!Number.isSafeInteger(task.issue_number) || task.issue_number <= 0)
|
|
10805
|
+
throw new Error(`tracker Task ${task.task_id} has an invalid Issue number`);
|
|
10806
|
+
if (!Array.isArray(task.blocked_by) || task.blocked_by.some((id) => typeof id !== "string" || !ID_PATTERN2.test(id)))
|
|
10807
|
+
throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
|
|
10808
|
+
if (taskIds.has(task.task_id))
|
|
10809
|
+
throw new Error(`tracker returned duplicate Task ${task.task_id}`);
|
|
10810
|
+
if (sliceIds.has(task.slice_id))
|
|
10811
|
+
throw new Error(`tracker returned duplicate Slice ${task.slice_id}`);
|
|
10812
|
+
taskIds.add(task.task_id);
|
|
10813
|
+
sliceIds.add(task.slice_id);
|
|
10814
|
+
const blockedBy = [...task.blocked_by].sort();
|
|
10815
|
+
if (new Set(blockedBy).size !== blockedBy.length || blockedBy.includes(task.task_id))
|
|
10816
|
+
throw new Error(`tracker Task ${task.task_id} has invalid blocked_by dependencies`);
|
|
10817
|
+
return { ...task, blocked_by: blockedBy };
|
|
10818
|
+
});
|
|
10819
|
+
for (const task of tasks) {
|
|
10820
|
+
const unknown = task.blocked_by.find((id) => !taskIds.has(id));
|
|
10821
|
+
if (unknown)
|
|
10822
|
+
throw new Error(`tracker Task ${task.task_id} depends on unknown Task ${unknown}`);
|
|
10823
|
+
}
|
|
10824
|
+
return { ...value, tasks: tasks.sort((left, right) => compareIds(left.task_id, right.task_id)) };
|
|
10825
|
+
}
|
|
10826
|
+
function dependencyOrder(observation) {
|
|
10827
|
+
const remaining = new Map(observation.tasks.map((task) => [task.task_id, task]));
|
|
10828
|
+
const done = new Set;
|
|
10829
|
+
const order = [];
|
|
10830
|
+
while (remaining.size) {
|
|
10831
|
+
const ready = [...remaining.values()].filter((task) => task.blocked_by.every((id) => done.has(id))).sort((left, right) => compareIds(left.task_id, right.task_id));
|
|
10832
|
+
if (!ready.length)
|
|
10833
|
+
throw new Error("Initiative Task dependencies must form an acyclic graph");
|
|
10834
|
+
for (const task of ready) {
|
|
10835
|
+
remaining.delete(task.task_id);
|
|
10836
|
+
done.add(task.task_id);
|
|
10837
|
+
order.push(task);
|
|
10838
|
+
}
|
|
10839
|
+
}
|
|
10840
|
+
return order;
|
|
10841
|
+
}
|
|
10842
|
+
function dependencyClosures(order) {
|
|
10843
|
+
const closures = new Map;
|
|
10844
|
+
for (const task of order) {
|
|
10845
|
+
const closure = new Set;
|
|
10846
|
+
for (const dependency of task.blocked_by) {
|
|
10847
|
+
closure.add(dependency);
|
|
10848
|
+
for (const transitive of closures.get(dependency) ?? [])
|
|
10849
|
+
closure.add(transitive);
|
|
10850
|
+
}
|
|
10851
|
+
closures.set(task.task_id, [...closure].sort());
|
|
10852
|
+
}
|
|
10853
|
+
return closures;
|
|
10854
|
+
}
|
|
10855
|
+
function budget(input, enrollableCount, confirmationTime) {
|
|
10856
|
+
const maxChildren = input.budget?.max_children === undefined ? positiveSafeInteger(enrollableCount, "budget.max_children") : positiveSafeInteger(input.budget.max_children, "budget.max_children");
|
|
10857
|
+
if (maxChildren > enrollableCount)
|
|
10858
|
+
throw new Error("budget.max_children exceeds the enrollable Child count");
|
|
10859
|
+
const qaFailureLimit = input.budget?.qa_failure_limit === undefined ? DEFAULT_QA_FAILURE_LIMIT : positiveSafeInteger(input.budget.qa_failure_limit, "budget.qa_failure_limit");
|
|
10860
|
+
const deadline = input.budget?.deadline_at === undefined ? new Date(confirmationTime.milliseconds + DEFAULT_DEADLINE_MS).toISOString() : timestamp(input.budget.deadline_at, "budget.deadline_at").iso;
|
|
10861
|
+
if (Date.parse(deadline) <= confirmationTime.milliseconds)
|
|
10862
|
+
throw new Error("budget.deadline_at must be later than confirmation_time");
|
|
10863
|
+
return { max_children: maxChildren, deadline_at: deadline, qa_failure_limit: qaFailureLimit };
|
|
10864
|
+
}
|
|
10865
|
+
async function projectBatchPlan(root, initiativeSlug, input, readInitiative = observeGithubInitiative) {
|
|
10866
|
+
if (!ID_PATTERN2.test(initiativeSlug))
|
|
10867
|
+
throw new Error("initiative_slug is invalid");
|
|
10868
|
+
if (!input || typeof input !== "object")
|
|
10869
|
+
throw new Error("batch plan input is required");
|
|
10870
|
+
const confirmationTime = timestamp(input.confirmation_time, "confirmation_time");
|
|
10871
|
+
const trackerObservation = normalizeObservation(await readInitiative(root, initiativeSlug), initiativeSlug);
|
|
10872
|
+
const order = dependencyOrder(trackerObservation);
|
|
10873
|
+
const closures = dependencyClosures(order);
|
|
10874
|
+
const children = [];
|
|
10875
|
+
for (const task of order) {
|
|
10876
|
+
const blockedBy = closures.get(task.task_id);
|
|
10877
|
+
const child = { task_id: task.task_id, slice_id: task.slice_id, blocked_by: blockedBy };
|
|
10878
|
+
const tombstone = readTaskTombstone(root, task.task_id);
|
|
10879
|
+
const record = readTaskRecordRaw(root, task.task_id).record;
|
|
10880
|
+
if (tombstone && record)
|
|
10881
|
+
throw new Error(`Task ${task.task_id} has conflicting settled and active state`);
|
|
10882
|
+
if (tombstone) {
|
|
10883
|
+
children.push({ ...child, status: "already_settled", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
|
|
10884
|
+
continue;
|
|
10885
|
+
}
|
|
10886
|
+
if (record) {
|
|
10887
|
+
children.push({ ...child, status: "already_owned", reason: null, intent_path: null, intent_revision: null, intent_content_hash: null });
|
|
10888
|
+
continue;
|
|
10889
|
+
}
|
|
10890
|
+
const intentPath = `docs/plans/${task.task_id}.intent.json`;
|
|
10891
|
+
try {
|
|
10892
|
+
const read = observeTaskIntent(root, task.task_id, intentPath);
|
|
10893
|
+
if (read.intent.risk === "critical") {
|
|
10894
|
+
children.push({
|
|
10895
|
+
...child,
|
|
10896
|
+
status: "needs_human",
|
|
10897
|
+
reason: "critical",
|
|
10898
|
+
intent_path: read.intent_ref.path,
|
|
10899
|
+
intent_revision: read.intent_ref.revision,
|
|
10900
|
+
intent_content_hash: read.content_hash
|
|
10901
|
+
});
|
|
10902
|
+
continue;
|
|
10903
|
+
}
|
|
10904
|
+
const binding = inspectSpecBinding(read.intent);
|
|
10905
|
+
if (!binding.ok) {
|
|
10906
|
+
children.push({
|
|
10907
|
+
...child,
|
|
10908
|
+
status: "needs_human",
|
|
10909
|
+
reason: specBindingReason(binding),
|
|
10910
|
+
intent_path: read.intent_ref.path,
|
|
10911
|
+
intent_revision: read.intent_ref.revision,
|
|
10912
|
+
intent_content_hash: read.content_hash
|
|
10913
|
+
});
|
|
10914
|
+
continue;
|
|
10915
|
+
}
|
|
10916
|
+
children.push({
|
|
10917
|
+
...child,
|
|
10918
|
+
status: "enrollable",
|
|
10919
|
+
reason: null,
|
|
10920
|
+
intent_path: read.intent_ref.path,
|
|
10921
|
+
intent_revision: read.intent_ref.revision,
|
|
10922
|
+
intent_content_hash: read.content_hash
|
|
10923
|
+
});
|
|
10924
|
+
} catch (error) {
|
|
10925
|
+
if (!(error instanceof TaskIntentObservationError))
|
|
10926
|
+
throw error;
|
|
10927
|
+
children.push({ ...child, status: "needs_human", reason: "invalid_intent", intent_path: null, intent_revision: null, intent_content_hash: null });
|
|
10928
|
+
}
|
|
10929
|
+
}
|
|
10930
|
+
const directDependencies = new Map(order.map((task) => [task.task_id, task.blocked_by]));
|
|
10931
|
+
const childById = new Map(children.map((child) => [child.task_id, child]));
|
|
10932
|
+
for (const child of children) {
|
|
10933
|
+
if (child.status !== "enrollable")
|
|
10934
|
+
continue;
|
|
10935
|
+
if (directDependencies.get(child.task_id).some((id) => {
|
|
10936
|
+
const status = childById.get(id).status;
|
|
10937
|
+
return status === "already_owned" || status === "needs_human" || status === "blocked";
|
|
10938
|
+
})) {
|
|
10939
|
+
child.status = "blocked";
|
|
10940
|
+
child.reason = "dependency_unavailable";
|
|
10941
|
+
}
|
|
10942
|
+
}
|
|
10943
|
+
const enrollable = children.filter((child) => child.status === "enrollable").map((child) => ({
|
|
10944
|
+
task_id: child.task_id,
|
|
10945
|
+
intent_path: child.intent_path,
|
|
10946
|
+
intent_revision: child.intent_revision,
|
|
10947
|
+
intent_content_hash: child.intent_content_hash,
|
|
10948
|
+
blocked_by: child.blocked_by.filter((id) => childById.get(id)?.status === "enrollable")
|
|
10949
|
+
}));
|
|
10950
|
+
if (!enrollable.length)
|
|
10951
|
+
throw new Error("batch plan has no enrollable children; nothing to confirm");
|
|
10952
|
+
const planDigest = `sha256:${createHash19("sha256").update(stableStringify(enrollable)).digest("hex")}`;
|
|
10953
|
+
return {
|
|
10954
|
+
contract: "assurance_kernel/batch_plan/v1",
|
|
10955
|
+
initiative_slug: initiativeSlug,
|
|
10956
|
+
confirmation_time: confirmationTime.iso,
|
|
10957
|
+
tracker_observation: trackerObservation,
|
|
10958
|
+
children,
|
|
10959
|
+
enrollable,
|
|
10960
|
+
plan_digest: planDigest,
|
|
10961
|
+
budget: budget(input, enrollable.length, confirmationTime)
|
|
10962
|
+
};
|
|
10963
|
+
}
|
|
10964
|
+
|
|
10462
10965
|
// plugins/immune-brain/runtime/unattended/batch_preflight.ts
|
|
10463
10966
|
var INITIATIVE_SLUG_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
10464
10967
|
function batchRejection(key, detail = "") {
|
|
@@ -10509,7 +11012,12 @@ function findExistingActiveBatch(root, initiativeSlug) {
|
|
|
10509
11012
|
}
|
|
10510
11013
|
if (isTerminalBatchState(candidate.batch_state))
|
|
10511
11014
|
continue;
|
|
10512
|
-
|
|
11015
|
+
try {
|
|
11016
|
+
const record = readBatchRunState(root, file.slice(0, -5));
|
|
11017
|
+
if (record)
|
|
11018
|
+
return { corrupt: false, record };
|
|
11019
|
+
} catch {}
|
|
11020
|
+
return { corrupt: true, path: file };
|
|
10513
11021
|
}
|
|
10514
11022
|
return null;
|
|
10515
11023
|
}
|
|
@@ -10559,7 +11067,7 @@ function isOwnBatchClaim(root, existingBatch, taskId, batchBranch) {
|
|
|
10559
11067
|
const currentTaskId = workspaceOwner || (claim?.lifecycle_status === "active" ? claim?.task_id : null);
|
|
10560
11068
|
if (currentTaskId !== taskId || !claim)
|
|
10561
11069
|
return false;
|
|
10562
|
-
const branch =
|
|
11070
|
+
const branch = spawnSync6("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" }).stdout.trim();
|
|
10563
11071
|
if (branch !== batchBranch)
|
|
10564
11072
|
return false;
|
|
10565
11073
|
const childInBatch = existingBatch.children.find((c) => c.task_id === taskId);
|
|
@@ -10616,7 +11124,7 @@ function authorizedScopeOf(root, taskId, state) {
|
|
|
10616
11124
|
return scope;
|
|
10617
11125
|
}
|
|
10618
11126
|
function porcelainEntries(root) {
|
|
10619
|
-
const statusProc =
|
|
11127
|
+
const statusProc = spawnSync6("git", ["-C", root, "status", "--porcelain=v1", "-z", "--no-renames", "--untracked-files=all", "--ignore-submodules=none"], { encoding: "utf8", env: { ...process.env, GIT_OPTIONAL_LOCKS: "0" } });
|
|
10620
11128
|
if (statusProc.status !== 0)
|
|
10621
11129
|
return null;
|
|
10622
11130
|
const entries = [];
|
|
@@ -10635,7 +11143,9 @@ async function projectPlanSurface(input) {
|
|
|
10635
11143
|
const riskByTask = new Map;
|
|
10636
11144
|
let budget;
|
|
10637
11145
|
if (isResuming && existingBatch) {
|
|
10638
|
-
budget = existingBatch.budget;
|
|
11146
|
+
budget = { ...existingBatch.budget };
|
|
11147
|
+
if (Date.parse(budget.deadline_at) <= Date.parse(now))
|
|
11148
|
+
budget.deadline_at = new Date(Date.parse(now) + DEFAULT_DEADLINE_MS).toISOString();
|
|
10639
11149
|
try {
|
|
10640
11150
|
recoveryChildren = existingBatch.children.map((c) => {
|
|
10641
11151
|
const intentPath = `docs/plans/${c.task_id}.intent.json`;
|
|
@@ -10738,6 +11248,11 @@ async function projectBatchPreflight(options) {
|
|
|
10738
11248
|
const isResuming = activeRecord !== null;
|
|
10739
11249
|
const batchBranch = `imm/${initiativeSlug}`;
|
|
10740
11250
|
const activeTaskId = readActiveClaimTaskId(root);
|
|
11251
|
+
if (activeTaskId) {
|
|
11252
|
+
const authority = reconcileKernelAuthority(root, activeTaskId);
|
|
11253
|
+
if (authority.state === "authority_conflict")
|
|
11254
|
+
return reject("kernel_authority_conflict", authority.diagnostic ?? "unverifiable workspace authority");
|
|
11255
|
+
}
|
|
10741
11256
|
const ownClaim = isResuming && activeTaskId !== null && isOwnBatchClaim(root, activeRecord, activeTaskId, batchBranch);
|
|
10742
11257
|
if (activeTaskId && !ownClaim)
|
|
10743
11258
|
return reject("claim_already_active", activeTaskId);
|
|
@@ -10747,7 +11262,7 @@ async function projectBatchPreflight(options) {
|
|
|
10747
11262
|
} catch (err) {
|
|
10748
11263
|
return reject("git_head_unreadable", err instanceof Error ? err.message : String(err));
|
|
10749
11264
|
}
|
|
10750
|
-
const branchExists =
|
|
11265
|
+
const branchExists = spawnSync6("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${batchBranch}`]);
|
|
10751
11266
|
if (branchExists.status === 0 && !existingBatch)
|
|
10752
11267
|
return reject("branch_already_exists", batchBranch);
|
|
10753
11268
|
const statusEntries = porcelainEntries(root);
|
|
@@ -10796,21 +11311,29 @@ async function projectBatchPreflight(options) {
|
|
|
10796
11311
|
});
|
|
10797
11312
|
if (!planSurface.ok)
|
|
10798
11313
|
return reject(planSurface.key, planSurface.detail);
|
|
10799
|
-
|
|
10800
|
-
|
|
10801
|
-
|
|
10802
|
-
|
|
10803
|
-
|
|
10804
|
-
|
|
10805
|
-
existing_batch: existingBatch,
|
|
10806
|
-
base_head: baseHead,
|
|
10807
|
-
budget: planSurface.surface.budget,
|
|
10808
|
-
plan_digest: planSurface.surface.plan_digest,
|
|
10809
|
-
recovery_children: planSurface.surface.recovery_children,
|
|
10810
|
-
risk_by_task: planSurface.surface.risk_by_task,
|
|
10811
|
-
excluded: planSurface.surface.excluded
|
|
11314
|
+
let reconfirmation;
|
|
11315
|
+
if (activeRecord && activeRecord.plan_digest !== planSurface.surface.plan_digest) {
|
|
11316
|
+
try {
|
|
11317
|
+
reconfirmation = await captureBatchReconfirmation(root, activeRecord, planSurface.surface.recovery_children);
|
|
11318
|
+
} catch {
|
|
11319
|
+
return reject("plan_projection_failed", "batch plan reconfirmation is not eligible");
|
|
10812
11320
|
}
|
|
11321
|
+
}
|
|
11322
|
+
const projection = {
|
|
11323
|
+
initiative_slug: initiativeSlug,
|
|
11324
|
+
batch_branch: batchBranch,
|
|
11325
|
+
is_resuming: isResuming,
|
|
11326
|
+
existing_batch: existingBatch,
|
|
11327
|
+
base_head: baseHead,
|
|
11328
|
+
budget: planSurface.surface.budget,
|
|
11329
|
+
plan_digest: planSurface.surface.plan_digest,
|
|
11330
|
+
recovery_children: planSurface.surface.recovery_children,
|
|
11331
|
+
risk_by_task: planSurface.surface.risk_by_task,
|
|
11332
|
+
excluded: planSurface.surface.excluded
|
|
10813
11333
|
};
|
|
11334
|
+
if (reconfirmation)
|
|
11335
|
+
Object.defineProperty(projection, "reconfirmation", { value: reconfirmation });
|
|
11336
|
+
return { ok: true, projection };
|
|
10814
11337
|
}
|
|
10815
11338
|
async function projectBatchDrift(options) {
|
|
10816
11339
|
const { root, initiative_slug: initiativeSlug, readInitiative } = options;
|
|
@@ -10851,21 +11374,29 @@ async function authorizeBatch(options) {
|
|
|
10851
11374
|
} = projection;
|
|
10852
11375
|
const existingBatch = projection.existing_batch;
|
|
10853
11376
|
const isExistingExpired = isResuming && Date.parse(existingBatch.authorization_expires_at) <= Date.now();
|
|
10854
|
-
const
|
|
11377
|
+
const branchBefore = spawnSync6("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
|
|
11378
|
+
if (branchBefore.status !== 0)
|
|
11379
|
+
return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
|
|
11380
|
+
const ownsUnpersistedHead = isResuming && existingBatch !== null && existingBatch.plan_digest === planDigest && expectedBatchHead(existingBatch) !== baseHead && ownUnpersistedBatchHead(root, existingBatch, baseHead);
|
|
11381
|
+
if (isResuming && existingBatch && !existingBatch.commits.length && existingBatch.children[0]?.state === "settled" && expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead)
|
|
11382
|
+
return { outcome: "rejected", rejection: batchRejection("head_moved", "first unpersisted commit provenance is invalid") };
|
|
10855
11383
|
const reuseBlockers = [];
|
|
10856
11384
|
if (isResuming && existingBatch) {
|
|
10857
11385
|
if (isExistingExpired)
|
|
10858
11386
|
reuseBlockers.push("batch_authorization_expired");
|
|
11387
|
+
if (Date.parse(existingBatch.budget.deadline_at) <= Date.now())
|
|
11388
|
+
reuseBlockers.push("batch_budget_expired");
|
|
10859
11389
|
if (existingBatch.batch_state !== "running")
|
|
10860
11390
|
reuseBlockers.push("batch_not_running");
|
|
10861
11391
|
if (existingBatch.plan_digest !== planDigest)
|
|
10862
11392
|
reuseBlockers.push("batch_plan_digest_changed");
|
|
10863
11393
|
if (existingBatch.branch !== batchBranch)
|
|
10864
11394
|
reuseBlockers.push("batch_branch_changed");
|
|
10865
|
-
if (expectedBatchHead(existingBatch) !== baseHead)
|
|
11395
|
+
if (expectedBatchHead(existingBatch) !== baseHead && !ownsUnpersistedHead)
|
|
10866
11396
|
reuseBlockers.push("batch_head_lineage_moved");
|
|
10867
11397
|
}
|
|
10868
11398
|
const reuseAuthorization = isResuming && reuseBlockers.length === 0;
|
|
11399
|
+
const expiresAt = reuseAuthorization ? existingBatch.authorization_expires_at : budget.deadline_at;
|
|
10869
11400
|
const batchId = isResuming && existingBatch ? existingBatch.batch_id : `batch-${initiativeSlug}-${randomUUID7()}`;
|
|
10870
11401
|
const facts = {
|
|
10871
11402
|
initiative_slug: initiativeSlug,
|
|
@@ -10903,12 +11434,29 @@ async function authorizeBatch(options) {
|
|
|
10903
11434
|
return { outcome: "rejected", rejection: batchRejection("plan_changed", drift.plan_digest) };
|
|
10904
11435
|
if (drift.base_head === null)
|
|
10905
11436
|
return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
|
|
10906
|
-
if (drift.base_head !== baseHead)
|
|
11437
|
+
if (drift.base_head !== baseHead || ownsUnpersistedHead && !ownUnpersistedBatchHead(root, existingBatch, baseHead))
|
|
10907
11438
|
return { outcome: "rejected", rejection: batchRejection("head_moved", drift.base_head) };
|
|
10908
11439
|
const finalClaimTaskId = readActiveClaimTaskId(root);
|
|
10909
11440
|
const finalOwnClaim = isResuming && finalClaimTaskId !== null && isOwnBatchClaim(root, existingBatch, finalClaimTaskId, batchBranch);
|
|
10910
11441
|
if (finalClaimTaskId && !finalOwnClaim)
|
|
10911
11442
|
return { outcome: "rejected", rejection: batchRejection("claim_appeared_during_confirmation", finalClaimTaskId) };
|
|
11443
|
+
if (isResuming) {
|
|
11444
|
+
const current = findExistingActiveBatch(root, initiativeSlug);
|
|
11445
|
+
if (!current || current.corrupt || JSON.stringify(current.record) !== JSON.stringify(existingBatch))
|
|
11446
|
+
return { outcome: "rejected", rejection: batchRejection("plan_changed") };
|
|
11447
|
+
}
|
|
11448
|
+
const branchAfter = spawnSync6("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
|
|
11449
|
+
if (branchAfter.status !== 0 || branchAfter.stdout !== branchBefore.stdout)
|
|
11450
|
+
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
|
+
if (projection.reconfirmation) {
|
|
11454
|
+
try {
|
|
11455
|
+
projection.reconfirmation.assertUnchanged();
|
|
11456
|
+
} catch {
|
|
11457
|
+
return { outcome: "rejected", rejection: batchRejection("plan_changed") };
|
|
11458
|
+
}
|
|
11459
|
+
}
|
|
10912
11460
|
const binding = {
|
|
10913
11461
|
batch_id: batchId,
|
|
10914
11462
|
initiative_slug: initiativeSlug,
|
|
@@ -10921,6 +11469,8 @@ async function authorizeBatch(options) {
|
|
|
10921
11469
|
expires_at: expiresAt,
|
|
10922
11470
|
nonce: options.nonce
|
|
10923
11471
|
};
|
|
11472
|
+
if (projection.reconfirmation)
|
|
11473
|
+
retainReconfirmation(binding.nonce, binding.expires_at, projection.reconfirmation);
|
|
10924
11474
|
return {
|
|
10925
11475
|
outcome: "authorized",
|
|
10926
11476
|
batch_id: batchId,
|
|
@@ -10932,12 +11482,12 @@ async function authorizeBatch(options) {
|
|
|
10932
11482
|
}
|
|
10933
11483
|
|
|
10934
11484
|
// plugins/immune-brain/runtime/unattended/batch_runner.ts
|
|
10935
|
-
import { spawnSync as
|
|
11485
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
10936
11486
|
import { existsSync as existsSync9 } from "node:fs";
|
|
10937
11487
|
import { join as join14 } from "node:path";
|
|
10938
11488
|
|
|
10939
11489
|
// plugins/immune-brain/runtime/unattended/batch_git.ts
|
|
10940
|
-
import { spawnSync as
|
|
11490
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
10941
11491
|
import { createHash as createHash20, randomUUID as randomUUID8 } from "node:crypto";
|
|
10942
11492
|
import {
|
|
10943
11493
|
constants as constants6,
|
|
@@ -10962,7 +11512,7 @@ var DEFAULT_GIT_ENV = {
|
|
|
10962
11512
|
function runBatchGitPreflight(input) {
|
|
10963
11513
|
const { root, initiative_slug: initiativeSlug, base_head: baseHead } = input;
|
|
10964
11514
|
const branch = `imm/${initiativeSlug}`;
|
|
10965
|
-
const toplevelResult =
|
|
11515
|
+
const toplevelResult = spawnSync7("git", ["-C", root, "rev-parse", "--show-toplevel"], {
|
|
10966
11516
|
encoding: "utf8",
|
|
10967
11517
|
stdio: ["ignore", "pipe", "pipe"]
|
|
10968
11518
|
});
|
|
@@ -11000,7 +11550,7 @@ function runBatchGitPreflight(input) {
|
|
|
11000
11550
|
message: `unsupported index flags (assume-unchanged/skip-worktree) detected: ${flaggedPreflight.join(", ")}`
|
|
11001
11551
|
};
|
|
11002
11552
|
}
|
|
11003
|
-
const statusResult =
|
|
11553
|
+
const statusResult = spawnSync7("git", ["-C", root, "status", "--porcelain", "--untracked-files=all", "--ignore-submodules=none"], {
|
|
11004
11554
|
encoding: "utf8",
|
|
11005
11555
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11006
11556
|
});
|
|
@@ -11018,7 +11568,7 @@ function runBatchGitPreflight(input) {
|
|
|
11018
11568
|
message: "working tree is dirty before batch preflight"
|
|
11019
11569
|
};
|
|
11020
11570
|
}
|
|
11021
|
-
const headResult =
|
|
11571
|
+
const headResult = spawnSync7("git", ["-C", root, "rev-parse", "--verify", "HEAD^{commit}"], {
|
|
11022
11572
|
encoding: "utf8",
|
|
11023
11573
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11024
11574
|
});
|
|
@@ -11029,11 +11579,11 @@ function runBatchGitPreflight(input) {
|
|
|
11029
11579
|
message: "HEAD is uncommitted or not a valid commit"
|
|
11030
11580
|
};
|
|
11031
11581
|
}
|
|
11032
|
-
const branchCheck =
|
|
11582
|
+
const branchCheck = spawnSync7("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { stdio: ["ignore", "ignore", "ignore"] });
|
|
11033
11583
|
if (branchCheck.status === 0) {
|
|
11034
11584
|
const settled = findSettledBatchRecord(root, initiativeSlug);
|
|
11035
|
-
const currentBranch =
|
|
11036
|
-
if (findExistingActiveBatch(root, initiativeSlug) === null && settled?.branch === branch && currentBranch.status === 0 && currentBranch.stdout.trim() === branch && headResult.stdout.trim() === baseHead &&
|
|
11585
|
+
const currentBranch = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], { encoding: "utf8" });
|
|
11586
|
+
if (findExistingActiveBatch(root, initiativeSlug) === null && settled?.branch === branch && currentBranch.status === 0 && currentBranch.stdout.trim() === branch && headResult.stdout.trim() === baseHead && spawnSync7("git", ["-C", root, "merge-base", "--is-ancestor", expectedBatchHead(settled), baseHead]).status === 0)
|
|
11037
11587
|
return { ok: true, branch };
|
|
11038
11588
|
return {
|
|
11039
11589
|
ok: false,
|
|
@@ -11041,25 +11591,25 @@ function runBatchGitPreflight(input) {
|
|
|
11041
11591
|
message: `branch refs/heads/${branch} already exists`
|
|
11042
11592
|
};
|
|
11043
11593
|
}
|
|
11044
|
-
const originalBranchResult =
|
|
11594
|
+
const originalBranchResult = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
11045
11595
|
encoding: "utf8",
|
|
11046
11596
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11047
11597
|
});
|
|
11048
11598
|
const originalBranch = originalBranchResult.stdout.trim();
|
|
11049
|
-
const checkoutResult =
|
|
11599
|
+
const checkoutResult = spawnSync7("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", "-b", branch, baseHead], {
|
|
11050
11600
|
encoding: "utf8",
|
|
11051
11601
|
stdio: ["ignore", "pipe", "pipe"],
|
|
11052
11602
|
env: DEFAULT_GIT_ENV
|
|
11053
11603
|
});
|
|
11054
11604
|
if (checkoutResult.status !== 0) {
|
|
11055
11605
|
const stderr = checkoutResult.stderr?.trim() || "";
|
|
11056
|
-
const branchExists = stderr.includes("already exists") ||
|
|
11057
|
-
const currentBranchCheck =
|
|
11606
|
+
const branchExists = stderr.includes("already exists") || spawnSync7("git", ["-C", root, "show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
|
|
11607
|
+
const currentBranchCheck = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
11058
11608
|
encoding: "utf8",
|
|
11059
11609
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11060
11610
|
}).stdout.trim();
|
|
11061
11611
|
if (currentBranchCheck === branch && originalBranch && originalBranch !== branch) {
|
|
11062
|
-
|
|
11612
|
+
spawnSync7("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "checkout", originalBranch], {
|
|
11063
11613
|
stdio: ["ignore", "ignore", "ignore"]
|
|
11064
11614
|
});
|
|
11065
11615
|
}
|
|
@@ -11082,7 +11632,7 @@ function hasBoundaryWhitespace(path) {
|
|
|
11082
11632
|
return path.split("/").some((segment) => segment.trim() !== segment || segment.length === 0);
|
|
11083
11633
|
}
|
|
11084
11634
|
function getUnsupportedIndexFlags(root) {
|
|
11085
|
-
const result =
|
|
11635
|
+
const result = spawnSync7("git", ["-C", root, "ls-files", "-v", "-z", "--"], {
|
|
11086
11636
|
encoding: "utf8",
|
|
11087
11637
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11088
11638
|
});
|
|
@@ -11116,13 +11666,13 @@ function isPathAllowedForChild(path, taskId, scopeHint) {
|
|
|
11116
11666
|
});
|
|
11117
11667
|
}
|
|
11118
11668
|
function getChangedProjectPaths(root) {
|
|
11119
|
-
const tracked =
|
|
11669
|
+
const tracked = spawnSync7("git", ["-C", root, "diff-index", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
|
|
11120
11670
|
encoding: "utf8",
|
|
11121
11671
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11122
11672
|
});
|
|
11123
11673
|
if (tracked.status !== 0)
|
|
11124
11674
|
throw new Error("failed to inspect tracked diff vs HEAD");
|
|
11125
|
-
const untracked =
|
|
11675
|
+
const untracked = spawnSync7("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
|
|
11126
11676
|
encoding: "utf8",
|
|
11127
11677
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11128
11678
|
});
|
|
@@ -11132,7 +11682,7 @@ function getChangedProjectPaths(root) {
|
|
|
11132
11682
|
return [...new Set([...splitZ(tracked.stdout), ...splitZ(untracked.stdout)])];
|
|
11133
11683
|
}
|
|
11134
11684
|
function getStagedProjectPaths(root) {
|
|
11135
|
-
const staged =
|
|
11685
|
+
const staged = spawnSync7("git", ["-C", root, "diff-index", "--cached", "--name-only", "-z", "--ignore-submodules=none", "HEAD", "--"], {
|
|
11136
11686
|
encoding: "utf8",
|
|
11137
11687
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11138
11688
|
});
|
|
@@ -11141,13 +11691,13 @@ function getStagedProjectPaths(root) {
|
|
|
11141
11691
|
return staged.stdout.split("\x00").filter((p) => p.length > 0);
|
|
11142
11692
|
}
|
|
11143
11693
|
function getUnstagedProjectPaths(root) {
|
|
11144
|
-
const diffFiles =
|
|
11694
|
+
const diffFiles = spawnSync7("git", ["-C", root, "diff-files", "--name-only", "-z", "--ignore-submodules=none", "--"], {
|
|
11145
11695
|
encoding: "utf8",
|
|
11146
11696
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11147
11697
|
});
|
|
11148
11698
|
if (diffFiles.status !== 0)
|
|
11149
11699
|
throw new Error("failed to inspect unstaged tracked changes");
|
|
11150
|
-
const untracked =
|
|
11700
|
+
const untracked = spawnSync7("git", ["-C", root, "ls-files", "--others", "--exclude-standard", "-z", "--"], {
|
|
11151
11701
|
encoding: "utf8",
|
|
11152
11702
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11153
11703
|
});
|
|
@@ -11157,7 +11707,7 @@ function getUnstagedProjectPaths(root) {
|
|
|
11157
11707
|
return [...new Set([...splitZ(diffFiles.stdout), ...splitZ(untracked.stdout)])];
|
|
11158
11708
|
}
|
|
11159
11709
|
function getCommittedDeltaPaths(root) {
|
|
11160
|
-
const delta =
|
|
11710
|
+
const delta = spawnSync7("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", "HEAD~1", "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
11161
11711
|
if (delta.status !== 0)
|
|
11162
11712
|
throw new Error("failed to inspect committed tree delta");
|
|
11163
11713
|
return delta.stdout.split("\x00").filter((p) => p.length > 0);
|
|
@@ -11263,7 +11813,7 @@ async function commitBatchChild(input) {
|
|
|
11263
11813
|
const goal = typeof intentSnapshot.goal === "string" ? intentSnapshot.goal : "";
|
|
11264
11814
|
const scopeHint = Array.isArray(intentSnapshot.scope_hint) ? intentSnapshot.scope_hint.filter((s) => typeof s === "string") : [];
|
|
11265
11815
|
if (expectedBranch !== undefined) {
|
|
11266
|
-
const currentBranchResult =
|
|
11816
|
+
const currentBranchResult = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
11267
11817
|
encoding: "utf8",
|
|
11268
11818
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11269
11819
|
});
|
|
@@ -11272,7 +11822,7 @@ async function commitBatchChild(input) {
|
|
|
11272
11822
|
throw new Error(`batch_head_lineage_broken: current branch ${currentBranch} does not match expected branch ${expectedBranch}`);
|
|
11273
11823
|
}
|
|
11274
11824
|
}
|
|
11275
|
-
const currentHeadResult =
|
|
11825
|
+
const currentHeadResult = spawnSync7("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
11276
11826
|
encoding: "utf8",
|
|
11277
11827
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11278
11828
|
});
|
|
@@ -11291,7 +11841,7 @@ async function commitBatchChild(input) {
|
|
|
11291
11841
|
}
|
|
11292
11842
|
const pathsToStage = getUnstagedProjectPaths(root);
|
|
11293
11843
|
if (pathsToStage.length > 0) {
|
|
11294
|
-
const addResult =
|
|
11844
|
+
const addResult = spawnSync7("git", ["-C", root, "--literal-pathspecs", "add", "-A", "--", ...pathsToStage], {
|
|
11295
11845
|
encoding: "utf8",
|
|
11296
11846
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11297
11847
|
});
|
|
@@ -11306,7 +11856,7 @@ async function commitBatchChild(input) {
|
|
|
11306
11856
|
const staged = getStagedProjectPaths(root);
|
|
11307
11857
|
const stagedOutside = staged.filter((p) => !isPathAllowedForChild(p, taskId, scopeHint));
|
|
11308
11858
|
if (stagedOutside.length > 0) {
|
|
11309
|
-
|
|
11859
|
+
spawnSync7("git", ["-C", root, "reset", "--quiet"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
11310
11860
|
throw new Error("dirty_outside_scope");
|
|
11311
11861
|
}
|
|
11312
11862
|
const record = auditPair.record;
|
|
@@ -11324,7 +11874,7 @@ async function commitBatchChild(input) {
|
|
|
11324
11874
|
|
|
11325
11875
|
Immune-Brain-Batch: ${batchId}
|
|
11326
11876
|
`;
|
|
11327
|
-
const commitResult =
|
|
11877
|
+
const commitResult = spawnSync7("git", ["-C", root, "-c", "core.hooksPath=/dev/null", "commit", "--no-verify", "-F", "-"], {
|
|
11328
11878
|
input: commitMessage,
|
|
11329
11879
|
encoding: "utf8",
|
|
11330
11880
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -11333,7 +11883,7 @@ Immune-Brain-Batch: ${batchId}
|
|
|
11333
11883
|
if (commitResult.status !== 0) {
|
|
11334
11884
|
throw new Error(`commit failed for child ${taskId}: ${commitResult.stderr?.trim() || "git commit failed"}`);
|
|
11335
11885
|
}
|
|
11336
|
-
const newHeadResult =
|
|
11886
|
+
const newHeadResult = spawnSync7("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
11337
11887
|
encoding: "utf8",
|
|
11338
11888
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11339
11889
|
});
|
|
@@ -11341,7 +11891,7 @@ Immune-Brain-Batch: ${batchId}
|
|
|
11341
11891
|
if (newHeadResult.status !== 0 || !newHead || newHead === expectedHead) {
|
|
11342
11892
|
throw new Error("commit_failed");
|
|
11343
11893
|
}
|
|
11344
|
-
const parentsResult =
|
|
11894
|
+
const parentsResult = spawnSync7("git", ["-C", root, "rev-parse", "HEAD^@"], {
|
|
11345
11895
|
encoding: "utf8",
|
|
11346
11896
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11347
11897
|
});
|
|
@@ -11374,7 +11924,7 @@ async function lookupBatchCommit(input) {
|
|
|
11374
11924
|
let candidates = [];
|
|
11375
11925
|
let evidenceBacked = false;
|
|
11376
11926
|
if (evidence && evidence.commit) {
|
|
11377
|
-
const direct =
|
|
11927
|
+
const direct = spawnSync7("git", ["-C", root, "log", "-n", "1", `--format=${FORMAT}`, evidence.commit, "--"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
11378
11928
|
if (direct.status !== 0) {
|
|
11379
11929
|
throw new Error(`failed to inspect batch commit for ${taskId}: ${direct.stderr?.trim() || "git log failed"}`);
|
|
11380
11930
|
}
|
|
@@ -11387,7 +11937,7 @@ async function lookupBatchCommit(input) {
|
|
|
11387
11937
|
}
|
|
11388
11938
|
}
|
|
11389
11939
|
if (!candidates.length) {
|
|
11390
|
-
const result =
|
|
11940
|
+
const result = spawnSync7("git", [
|
|
11391
11941
|
"-C",
|
|
11392
11942
|
root,
|
|
11393
11943
|
"log",
|
|
@@ -11424,7 +11974,7 @@ async function lookupBatchCommit(input) {
|
|
|
11424
11974
|
throw new Error(`batch_head_lineage_broken: adopted commit author ${match.authorName} does not match batch authority ${DEFAULT_GIT_ENV.GIT_AUTHOR_NAME}`);
|
|
11425
11975
|
}
|
|
11426
11976
|
if (expectedBranch !== undefined) {
|
|
11427
|
-
const currentBranchResult =
|
|
11977
|
+
const currentBranchResult = spawnSync7("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
11428
11978
|
encoding: "utf8",
|
|
11429
11979
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11430
11980
|
});
|
|
@@ -11434,7 +11984,7 @@ async function lookupBatchCommit(input) {
|
|
|
11434
11984
|
}
|
|
11435
11985
|
}
|
|
11436
11986
|
if (expectedHead !== undefined) {
|
|
11437
|
-
const currentHeadResult =
|
|
11987
|
+
const currentHeadResult = spawnSync7("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
11438
11988
|
encoding: "utf8",
|
|
11439
11989
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11440
11990
|
});
|
|
@@ -11442,7 +11992,7 @@ async function lookupBatchCommit(input) {
|
|
|
11442
11992
|
if (currentHead !== commit) {
|
|
11443
11993
|
throw new Error(`batch_head_lineage_broken: current HEAD ${currentHead} diverged from adopted commit ${commit}`);
|
|
11444
11994
|
}
|
|
11445
|
-
const parentsResult =
|
|
11995
|
+
const parentsResult = spawnSync7("git", ["-C", root, "rev-parse", `${commit}^@`], {
|
|
11446
11996
|
encoding: "utf8",
|
|
11447
11997
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11448
11998
|
});
|
|
@@ -11460,7 +12010,7 @@ async function lookupBatchCommit(input) {
|
|
|
11460
12010
|
throw new Error(`batch_head_lineage_broken: adopted commit task lifecycle is not done: ${lifecycle}`);
|
|
11461
12011
|
}
|
|
11462
12012
|
const scopeHint = Array.isArray(auditPair.record.intent_snapshot.scope_hint) ? auditPair.record.intent_snapshot.scope_hint.filter((s) => typeof s === "string") : [];
|
|
11463
|
-
const deltaResult =
|
|
12013
|
+
const deltaResult = spawnSync7("git", ["-C", root, "diff-tree", "--no-commit-id", "--name-only", "-z", "-r", expectedHead, commit], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
11464
12014
|
if (deltaResult.status !== 0) {
|
|
11465
12015
|
throw new Error("batch_head_lineage_broken: failed to inspect adopted commit delta");
|
|
11466
12016
|
}
|
|
@@ -11474,6 +12024,14 @@ async function lookupBatchCommit(input) {
|
|
|
11474
12024
|
}
|
|
11475
12025
|
|
|
11476
12026
|
// plugins/immune-brain/runtime/unattended/batch_runner.ts
|
|
12027
|
+
function batchQaFailureFacts(record) {
|
|
12028
|
+
const failed = record.history.filter((event) => event.type === "request_rework" && event.authority?.authority_kind === "qa");
|
|
12029
|
+
return {
|
|
12030
|
+
qa_failure_count: failed.length,
|
|
12031
|
+
last_qa_failure_at: failed.at(-1)?.at ?? null,
|
|
12032
|
+
recovery_findings: record.findings.filter((f) => f.status === "open").map((f) => ({ id: f.id, kind: f.kind, status: f.status, acceptance_id: f.acceptance_id }))
|
|
12033
|
+
};
|
|
12034
|
+
}
|
|
11477
12035
|
function dependentsOf(record, taskId) {
|
|
11478
12036
|
return record.children.filter((child) => child.blocked_by.includes(taskId));
|
|
11479
12037
|
}
|
|
@@ -11533,8 +12091,23 @@ function reportFor(record, reason, nextAction) {
|
|
|
11533
12091
|
created_at: record.updated_at
|
|
11534
12092
|
};
|
|
11535
12093
|
}
|
|
11536
|
-
function
|
|
11537
|
-
const
|
|
12094
|
+
function executorHandoff(record, taskId, fresh, failure = {}) {
|
|
12095
|
+
const recovery = deriveAssuranceRecovery(taskId, fresh, failure, fresh.recovery_findings);
|
|
12096
|
+
return {
|
|
12097
|
+
...reportFor(record, null, recovery?.next_action ?? (fresh.projection.artifact_state === "frozen" ? "The child is frozen with pending QA. Call advance_assurance once; follow its diagnostics before retrying, then submit any reserved Review before batch continuation." : "Route the enrolled child to foreground Executor, implement and stage its scoped work, then call Kernel advance_assurance. Submit any reserved Review verdict before continuing start_unattended_batch with the same Initiative.")),
|
|
12098
|
+
...recovery ? { recovery } : {},
|
|
12099
|
+
...failure.diagnostics?.length ? { diagnostics: failure.diagnostics } : {},
|
|
12100
|
+
handoff: recovery?.category === "authorization" ? undefined : {
|
|
12101
|
+
role: "executor",
|
|
12102
|
+
task_id: taskId,
|
|
12103
|
+
run_id: fresh.projection.run_id ?? null,
|
|
12104
|
+
record_revision: fresh.projection.record_revision,
|
|
12105
|
+
next_obligation: fresh.projection.next_obligation
|
|
12106
|
+
}
|
|
12107
|
+
};
|
|
12108
|
+
}
|
|
12109
|
+
function finalize(root, record, reason, nextAction, details = {}) {
|
|
12110
|
+
const report = { ...reportFor(record, reason, nextAction), ...details };
|
|
11538
12111
|
if (isTerminalBatchState(record.batch_state) || record.batch_state === "needs_human") {
|
|
11539
12112
|
writeBatchRunReport(root, report);
|
|
11540
12113
|
}
|
|
@@ -11557,14 +12130,14 @@ function externalHeadDriftMessage(root, record) {
|
|
|
11557
12130
|
if (!existsSync9(join14(root, ".git")))
|
|
11558
12131
|
return null;
|
|
11559
12132
|
const head = record.commits.length ? record.commits[record.commits.length - 1] : record.base_head;
|
|
11560
|
-
const headCheck =
|
|
12133
|
+
const headCheck = spawnSync8("git", ["-C", root, "rev-parse", "HEAD"], {
|
|
11561
12134
|
encoding: "utf8",
|
|
11562
12135
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11563
12136
|
});
|
|
11564
12137
|
if (headCheck.status === 0 && headCheck.stdout.trim() && headCheck.stdout.trim() !== head) {
|
|
11565
12138
|
return `batch_head_lineage_broken: current HEAD ${headCheck.stdout.trim()} does not match expected batch head ${head}`;
|
|
11566
12139
|
}
|
|
11567
|
-
const branchCheck =
|
|
12140
|
+
const branchCheck = spawnSync8("git", ["-C", root, "symbolic-ref", "--short", "HEAD"], {
|
|
11568
12141
|
encoding: "utf8",
|
|
11569
12142
|
stdio: ["ignore", "pipe", "pipe"]
|
|
11570
12143
|
});
|
|
@@ -11575,6 +12148,11 @@ function externalHeadDriftMessage(root, record) {
|
|
|
11575
12148
|
return null;
|
|
11576
12149
|
}
|
|
11577
12150
|
async function validatePersistedRun(input, record) {
|
|
12151
|
+
if (!record.commits.length && record.children[0]?.state === "settled" && existsSync9(join14(input.root, ".git"))) {
|
|
12152
|
+
const head = spawnSync8("git", ["-C", input.root, "rev-parse", "HEAD"], { encoding: "utf8" });
|
|
12153
|
+
if (head.status !== 0 || head.stdout.trim() !== record.base_head && !ownUnpersistedBatchHead(input.root, record, head.stdout.trim()))
|
|
12154
|
+
throw new Error("first unpersisted batch commit provenance is invalid");
|
|
12155
|
+
}
|
|
11578
12156
|
const plan = input.registry.children(input.capability);
|
|
11579
12157
|
if (record.plan_digest !== computeBatchPlanDigest(plan) || record.children.length !== plan.length || record.children.some((child, index) => {
|
|
11580
12158
|
const expected = plan[index];
|
|
@@ -11596,7 +12174,7 @@ async function validatePersistedRun(input, record) {
|
|
|
11596
12174
|
});
|
|
11597
12175
|
if (!evidence || evidence.commit !== child.commit) {
|
|
11598
12176
|
if (evidence === null && typeof child.commit === "string" && child.commit.length > 0 && existsSync9(join14(input.root, ".git"))) {
|
|
11599
|
-
const reach =
|
|
12177
|
+
const reach = spawnSync8("git", ["-C", input.root, "merge-base", "--is-ancestor", child.commit, "HEAD"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
11600
12178
|
if (reach.status !== 0) {
|
|
11601
12179
|
throw new Error(`batch_head_lineage_broken: recorded commit ${child.commit} for ${child.task_id} is no longer reachable from HEAD`);
|
|
11602
12180
|
}
|
|
@@ -11634,6 +12212,41 @@ function validateRunAuthorization(input, existing) {
|
|
|
11634
12212
|
});
|
|
11635
12213
|
if (computeBatchPlanDigest(children) !== authorized.plan_digest || input.plan_digest !== authorized.plan_digest || input.base_head !== authorized.base_head)
|
|
11636
12214
|
throw new Error("batch run input does not match the authorized plan or base_head");
|
|
12215
|
+
return authorized;
|
|
12216
|
+
}
|
|
12217
|
+
function applyPlanReconfirmation(input, existing) {
|
|
12218
|
+
if (existing.plan_digest === input.plan_digest)
|
|
12219
|
+
return existing;
|
|
12220
|
+
const next = {
|
|
12221
|
+
...existing,
|
|
12222
|
+
plan_digest: input.plan_digest,
|
|
12223
|
+
confirmation_time: input.confirmation_time,
|
|
12224
|
+
authorization_expires_at: input.authorization_expires_at,
|
|
12225
|
+
budget: input.budget
|
|
12226
|
+
};
|
|
12227
|
+
if (!existing.branch || input.batch_id !== existing.batch_id || input.initiative_slug !== existing.initiative_slug || input.base_head !== existing.base_head || input.budget.max_children !== existing.budget.max_children || input.budget.qa_failure_limit !== existing.budget.qa_failure_limit)
|
|
12228
|
+
throw new Error("plan_digest mismatch: reconfirmation binding changed");
|
|
12229
|
+
const validate = () => {
|
|
12230
|
+
const plan = input.registry.children(input.capability);
|
|
12231
|
+
if (existing.children.length !== plan.length || plan.some((child, index) => {
|
|
12232
|
+
const old = existing.children[index], nextChild = input.children[index];
|
|
12233
|
+
return !nextChild || old.task_id !== child.task_id || old.slice_id !== nextChild.slice_id || JSON.stringify(old.blocked_by) !== JSON.stringify(child.blocked_by) || JSON.stringify(nextChild.blocked_by) !== JSON.stringify(child.blocked_by);
|
|
12234
|
+
}))
|
|
12235
|
+
throw new Error("plan_digest mismatch: reconfirmation topology changed");
|
|
12236
|
+
const { issued_at: _issuedAt, ...binding } = validateRunAuthorization(input, { ...existing, plan_digest: input.plan_digest });
|
|
12237
|
+
if (binding.branch !== existing.branch)
|
|
12238
|
+
throw new Error("plan_digest mismatch: reconfirmation branch changed");
|
|
12239
|
+
prepareBatchRunState(input);
|
|
12240
|
+
return input.registry.inspect(input.capability, binding);
|
|
12241
|
+
};
|
|
12242
|
+
const authority = validate();
|
|
12243
|
+
const captured = takeReconfirmation(authority.nonce);
|
|
12244
|
+
if (captured.root !== input.root || captured.plan_digest !== input.plan_digest)
|
|
12245
|
+
throw new Error("plan_digest mismatch: captured reconfirmation does not match run input");
|
|
12246
|
+
return replaceBatchRunState(input.root, captured.stateBytes, next, () => {
|
|
12247
|
+
captured.assertUnchanged();
|
|
12248
|
+
validate();
|
|
12249
|
+
});
|
|
11637
12250
|
}
|
|
11638
12251
|
async function startBatch(input) {
|
|
11639
12252
|
try {
|
|
@@ -11664,7 +12277,9 @@ async function startBatchLocked(input) {
|
|
|
11664
12277
|
return reportFor({ ...rejected, batch_state: "rejected" }, "batch plan is empty", "Provide a non-empty enrollable child plan.");
|
|
11665
12278
|
}
|
|
11666
12279
|
let existing = readBatchRunState(input.root, input.batch_id);
|
|
12280
|
+
const reconfirmed = existing !== null && existing.plan_digest !== input.plan_digest;
|
|
11667
12281
|
if (existing) {
|
|
12282
|
+
existing = applyPlanReconfirmation(input, existing);
|
|
11668
12283
|
try {
|
|
11669
12284
|
await validatePersistedRun(input, existing);
|
|
11670
12285
|
} catch (error) {
|
|
@@ -11686,10 +12301,11 @@ async function startBatchLocked(input) {
|
|
|
11686
12301
|
const nextConfirmation = Date.parse(input.confirmation_time);
|
|
11687
12302
|
const nextExpiry = Date.parse(input.authorization_expires_at);
|
|
11688
12303
|
const freshAuthorization = Number.isFinite(nextConfirmation) && nextConfirmation > priorConfirmation && Number.isFinite(nextExpiry) && nextExpiry > Date.now();
|
|
11689
|
-
if (!freshAuthorization) {
|
|
12304
|
+
if (!freshAuthorization && !reconfirmed) {
|
|
11690
12305
|
return finalize(input.root, existing, "the parked batch requires a fresh literal-user confirmation", "Resolve the parked child, then re-confirm the batch to continue.");
|
|
11691
12306
|
}
|
|
11692
|
-
|
|
12307
|
+
if (!reconfirmed)
|
|
12308
|
+
validateRunAuthorization(input, existing);
|
|
11693
12309
|
prepareBatchRunState({ ...input, now: input.now });
|
|
11694
12310
|
const remapped = await Promise.all(existing.children.map(async (child) => {
|
|
11695
12311
|
if (child.state !== "needs_human")
|
|
@@ -11724,7 +12340,6 @@ async function startBatchLocked(input) {
|
|
|
11724
12340
|
authorization_expires_at: input.authorization_expires_at,
|
|
11725
12341
|
budget: input.budget,
|
|
11726
12342
|
batch_state: "running",
|
|
11727
|
-
consecutive_qa_failures: 0,
|
|
11728
12343
|
children: resumedChildren
|
|
11729
12344
|
});
|
|
11730
12345
|
}
|
|
@@ -11882,67 +12497,8 @@ async function startBatchLocked(input) {
|
|
|
11882
12497
|
break;
|
|
11883
12498
|
}
|
|
11884
12499
|
}
|
|
11885
|
-
|
|
11886
|
-
|
|
11887
|
-
const terminal = await input.kernel.advanceTask(input.root, child.task_id);
|
|
11888
|
-
if (terminal.state === "completed") {
|
|
11889
|
-
childTerminal = true;
|
|
11890
|
-
record.consecutive_qa_failures = 0;
|
|
11891
|
-
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "settled", reason: null } : c);
|
|
11892
|
-
persist();
|
|
11893
|
-
try {
|
|
11894
|
-
const planChild = input.children.find((c) => c.task_id === child.task_id);
|
|
11895
|
-
const intentPath = planChild?.intent_path ?? undefined;
|
|
11896
|
-
const { commit } = input.git?.commitChild ? await input.git.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath) : input.kernel.commitChild ? await input.kernel.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath) : await commitBatchChild({
|
|
11897
|
-
root: input.root,
|
|
11898
|
-
taskId: child.task_id,
|
|
11899
|
-
batchId: input.batch_id,
|
|
11900
|
-
expectedHead: head,
|
|
11901
|
-
branch: record.branch,
|
|
11902
|
-
intentPath
|
|
11903
|
-
});
|
|
11904
|
-
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "committed", commit } : c);
|
|
11905
|
-
record.commits.push(commit);
|
|
11906
|
-
head = commit;
|
|
11907
|
-
persist();
|
|
11908
|
-
} catch (error) {
|
|
11909
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
11910
|
-
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
|
|
11911
|
-
skipDependents(record, child.task_id, `dependency ${child.task_id} failed to commit`);
|
|
11912
|
-
record.batch_state = "failed";
|
|
11913
|
-
persist();
|
|
11914
|
-
break;
|
|
11915
|
-
}
|
|
11916
|
-
} else if (terminal.state === "stopped") {
|
|
11917
|
-
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: "Kernel reported the child stopped" } : c);
|
|
11918
|
-
childTerminal = true;
|
|
11919
|
-
skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
11920
|
-
record.batch_state = "needs_human";
|
|
11921
|
-
persist();
|
|
11922
|
-
} else if (terminal.state === "rework" && terminal.operation === "qa") {
|
|
11923
|
-
record.consecutive_qa_failures += 1;
|
|
11924
|
-
if (record.consecutive_qa_failures >= record.budget.qa_failure_limit) {
|
|
11925
|
-
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: `QA failure limit reached: ${terminal.summary}` } : c);
|
|
11926
|
-
record.batch_state = "needs_human";
|
|
11927
|
-
childTerminal = true;
|
|
11928
|
-
skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
11929
|
-
persist();
|
|
11930
|
-
} else {
|
|
11931
|
-
persist();
|
|
11932
|
-
}
|
|
11933
|
-
} else if (terminal.state === "failed" || terminal.state === "blocked" || terminal.state === "rework") {
|
|
11934
|
-
const reason = terminal.state === "rework" ? terminal.summary : terminal.reason;
|
|
11935
|
-
childTerminal = true;
|
|
11936
|
-
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
|
|
11937
|
-
skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
11938
|
-
record.batch_state = "needs_human";
|
|
11939
|
-
persist();
|
|
11940
|
-
} else if (terminal.state === "review_ready") {
|
|
11941
|
-
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "enrolled", reason: `review reservation ${terminal.operation_id} open` } : c);
|
|
11942
|
-
persist();
|
|
11943
|
-
return reportFor(record, `child ${child.task_id} holds an open Review reservation`, "Submit the reserved foreground Review verdict, then call startBatch again to continue.");
|
|
11944
|
-
}
|
|
11945
|
-
}
|
|
12500
|
+
const enrolled = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
|
|
12501
|
+
return executorHandoff(record, child.task_id, enrolled);
|
|
11946
12502
|
}
|
|
11947
12503
|
return finalize(input.root, record, stopReasonFor(record), "");
|
|
11948
12504
|
}
|
|
@@ -11964,8 +12520,9 @@ function stopReasonFor(record) {
|
|
|
11964
12520
|
}
|
|
11965
12521
|
async function resumeBatch(input, projection) {
|
|
11966
12522
|
let existing = readBatchRunState(input.root, input.batch_id);
|
|
11967
|
-
if (!existing)
|
|
12523
|
+
if (!existing || existing.batch_state === "needs_human")
|
|
11968
12524
|
return startBatch(input);
|
|
12525
|
+
existing = applyPlanReconfirmation(input, existing);
|
|
11969
12526
|
try {
|
|
11970
12527
|
await validatePersistedRun(input, existing);
|
|
11971
12528
|
} catch (error) {
|
|
@@ -11980,8 +12537,6 @@ async function resumeBatch(input, projection) {
|
|
|
11980
12537
|
}
|
|
11981
12538
|
if (isTerminalBatchState(existing.batch_state))
|
|
11982
12539
|
return replayTerminal(input.root, existing);
|
|
11983
|
-
if (existing.batch_state === "needs_human")
|
|
11984
|
-
return startBatch(input);
|
|
11985
12540
|
const driven = existing.children.find((child) => child.state === "enrolled" || child.state === "settled");
|
|
11986
12541
|
if (driven) {
|
|
11987
12542
|
if (driven.state === "enrolled") {
|
|
@@ -12001,11 +12556,30 @@ async function resumeBatch(input, projection) {
|
|
|
12001
12556
|
});
|
|
12002
12557
|
return finalize(input.root, existing, "claim held by another batch", "needs-human-attention");
|
|
12003
12558
|
}
|
|
12559
|
+
if (holdsClaim) {
|
|
12560
|
+
const failures = Math.max(existing.consecutive_qa_failures, fresh.qa_failure_count ?? 0);
|
|
12561
|
+
if (failures >= existing.budget.qa_failure_limit && fresh.last_qa_failure_at && Date.parse(fresh.last_qa_failure_at) > Date.parse(existing.confirmation_time)) {
|
|
12562
|
+
const reason = "QA failure limit reached; repair and settle the own child through Kernel before resuming the batch";
|
|
12563
|
+
skipDependents(existing, driven.task_id, `dependency ${driven.task_id} parked`);
|
|
12564
|
+
existing = writeBatchRunState(input.root, {
|
|
12565
|
+
...existing,
|
|
12566
|
+
batch_state: "needs_human",
|
|
12567
|
+
consecutive_qa_failures: failures,
|
|
12568
|
+
children: existing.children.map((c) => c.task_id === driven.task_id ? { ...c, state: "needs_human", reason } : c)
|
|
12569
|
+
});
|
|
12570
|
+
return finalize(input.root, existing, reason, "Repair and settle the own child through Kernel, then re-confirm the batch.", { recovery: deriveAssuranceRecovery(driven.task_id, fresh, {}, fresh.recovery_findings) ?? undefined });
|
|
12571
|
+
}
|
|
12572
|
+
if (failures !== existing.consecutive_qa_failures)
|
|
12573
|
+
existing = writeBatchRunState(input.root, { ...existing, consecutive_qa_failures: failures });
|
|
12574
|
+
if (fresh.projection.next_obligation === "run_qa" || fresh.projection.artifact_state !== "frozen")
|
|
12575
|
+
return executorHandoff(existing, driven.task_id, fresh);
|
|
12576
|
+
}
|
|
12004
12577
|
if (!holdsClaim) {
|
|
12005
12578
|
const settledRemotely = fresh.projection.lifecycle === "done" && fresh.projection.completion_ready === true;
|
|
12006
12579
|
if (settledRemotely) {
|
|
12007
12580
|
existing = writeBatchRunState(input.root, {
|
|
12008
12581
|
...existing,
|
|
12582
|
+
consecutive_qa_failures: 0,
|
|
12009
12583
|
children: existing.children.map((c) => c.task_id === driven.task_id ? { ...c, state: "settled", reason: "crash after settlement; resuming at commit" } : c)
|
|
12010
12584
|
});
|
|
12011
12585
|
} else {
|
|
@@ -12087,6 +12661,8 @@ async function driveInterruptedChild(input, child) {
|
|
|
12087
12661
|
intentPath
|
|
12088
12662
|
});
|
|
12089
12663
|
};
|
|
12664
|
+
if (existing && !record.commits.length && existsSync9(join14(input.root, ".git")) && !ownUnpersistedBatchHead(input.root, record, existing.commit))
|
|
12665
|
+
throw new Error("first unpersisted batch commit provenance is invalid");
|
|
12090
12666
|
const adopted = existing ?? await doCommit().catch((error) => {
|
|
12091
12667
|
const message = error instanceof Error ? error.message : String(error);
|
|
12092
12668
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
|
|
@@ -12108,30 +12684,8 @@ async function driveInterruptedChild(input, child) {
|
|
|
12108
12684
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "settled", reason: null } : c);
|
|
12109
12685
|
record.consecutive_qa_failures = 0;
|
|
12110
12686
|
persist();
|
|
12111
|
-
|
|
12112
|
-
|
|
12113
|
-
const intentPath = planChild?.intent_path ?? undefined;
|
|
12114
|
-
const { commit } = input.git?.commitChild ? await input.git.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath) : input.kernel.commitChild ? await input.kernel.commitChild(input.root, child.task_id, input.batch_id, head, record.branch, intentPath) : await commitBatchChild({
|
|
12115
|
-
root: input.root,
|
|
12116
|
-
taskId: child.task_id,
|
|
12117
|
-
batchId: input.batch_id,
|
|
12118
|
-
expectedHead: head,
|
|
12119
|
-
branch: record.branch,
|
|
12120
|
-
intentPath
|
|
12121
|
-
});
|
|
12122
|
-
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "committed", commit } : c);
|
|
12123
|
-
record.commits.push(commit);
|
|
12124
|
-
head = commit;
|
|
12125
|
-
persist();
|
|
12126
|
-
child = { ...child, state: "committed", commit };
|
|
12127
|
-
} catch (error) {
|
|
12128
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
12129
|
-
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason: message } : c);
|
|
12130
|
-
skipDependents(record, child.task_id, `dependency ${child.task_id} failed to commit`);
|
|
12131
|
-
record.batch_state = "failed";
|
|
12132
|
-
persist();
|
|
12133
|
-
return finalize(input.root, record, message, "");
|
|
12134
|
-
}
|
|
12687
|
+
child = { ...child, state: "settled", reason: null };
|
|
12688
|
+
continue;
|
|
12135
12689
|
} else if (terminal.state === "review_ready") {
|
|
12136
12690
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, reason: `review reservation ${terminal.operation_id} open` } : c);
|
|
12137
12691
|
persist();
|
|
@@ -12139,16 +12693,22 @@ async function driveInterruptedChild(input, child) {
|
|
|
12139
12693
|
} else if (terminal.state === "rework" && terminal.operation === "qa") {
|
|
12140
12694
|
record.consecutive_qa_failures += 1;
|
|
12141
12695
|
if (record.consecutive_qa_failures >= record.budget.qa_failure_limit) {
|
|
12142
|
-
const reason =
|
|
12696
|
+
const reason = "QA failure limit reached";
|
|
12143
12697
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
|
|
12144
12698
|
record.batch_state = "needs_human";
|
|
12145
12699
|
skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
12146
12700
|
persist();
|
|
12147
|
-
|
|
12701
|
+
const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
|
|
12702
|
+
return finalize(input.root, record, reason, "Repair and settle the own child through Kernel, then re-confirm the batch.", { recovery: deriveAssuranceRecovery(child.task_id, fresh, terminal, fresh.recovery_findings) ?? undefined, diagnostics: terminal.diagnostics });
|
|
12148
12703
|
}
|
|
12149
12704
|
persist();
|
|
12705
|
+
const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
|
|
12706
|
+
return executorHandoff(record, child.task_id, fresh, terminal);
|
|
12707
|
+
} else if (terminal.state === "rework" || terminal.environment_failure || terminal.state === "review_preparation_failed" || terminal.recovery?.category === "repair") {
|
|
12708
|
+
const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
|
|
12709
|
+
return executorHandoff(record, child.task_id, fresh, terminal);
|
|
12150
12710
|
} else {
|
|
12151
|
-
const reason = terminal.state === "stopped" ? "Kernel reported the child stopped" : terminal.
|
|
12711
|
+
const reason = terminal.state === "stopped" ? "Kernel reported the child stopped" : terminal.reason;
|
|
12152
12712
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
|
|
12153
12713
|
record.batch_state = "needs_human";
|
|
12154
12714
|
skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
@@ -12855,7 +13415,7 @@ class ClaudeRuntime {
|
|
|
12855
13415
|
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
12856
13416
|
if (!this.requestConfirmation)
|
|
12857
13417
|
throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
|
|
12858
|
-
|
|
13418
|
+
let now = new Date().toISOString();
|
|
12859
13419
|
const preflight = await projectBatchPreflight({
|
|
12860
13420
|
root: this.cwd,
|
|
12861
13421
|
initiative_slug: initiativeSlug,
|
|
@@ -12969,6 +13529,9 @@ class ClaudeRuntime {
|
|
|
12969
13529
|
const { binding, batch_id: batchId, expires_at: expiresAt } = authorization;
|
|
12970
13530
|
if (meta.signal?.aborted)
|
|
12971
13531
|
return batchReason("cancelled_before_execution");
|
|
13532
|
+
now = new Date().toISOString();
|
|
13533
|
+
if (Date.parse(expiresAt) <= Date.parse(now))
|
|
13534
|
+
return batchReason("confirmation_failed", "confirmed batch deadline expired before authorization issuance");
|
|
12972
13535
|
const capability = this.batchRegistry.issue(binding, recoveryChildren, now);
|
|
12973
13536
|
const basePort = this.createBatchKernelPort(this.batchRegistry, capability, binding);
|
|
12974
13537
|
const kernelPort = {
|
|
@@ -13041,19 +13604,30 @@ class ClaudeRuntime {
|
|
|
13041
13604
|
},
|
|
13042
13605
|
advanceTask: async (root, taskId) => {
|
|
13043
13606
|
const result = await this.coordinator.advance(taskId, { cwd: root });
|
|
13607
|
+
const facts = { diagnostics: result.diagnostics, environment_failure: result.environment_failure, recovery: result.recovery };
|
|
13044
13608
|
if (result.state === "completed")
|
|
13045
|
-
return { state: "completed" };
|
|
13609
|
+
return { state: "completed", ...facts };
|
|
13046
13610
|
if (result.state === "stopped")
|
|
13047
|
-
return { state: "stopped" };
|
|
13611
|
+
return { state: "stopped", ...facts };
|
|
13048
13612
|
if (result.state === "rework")
|
|
13049
|
-
return { state: "rework", operation: result.operation, summary: result.summary };
|
|
13613
|
+
return { state: "rework", operation: result.operation, summary: result.summary, ...facts };
|
|
13050
13614
|
if (result.state === "review_ready")
|
|
13051
|
-
return { state: "review_ready", operation_id: result.operation_id };
|
|
13615
|
+
return { state: "review_ready", operation_id: result.operation_id, ...facts };
|
|
13616
|
+
if (result.state === "review_preparation_failed")
|
|
13617
|
+
return { state: result.state, operation: result.operation, operation_id: result.operation_id, reason: result.reason, ...facts };
|
|
13052
13618
|
if (result.state === "blocked")
|
|
13053
|
-
return { state: "blocked", reason: result.reason };
|
|
13054
|
-
return { state: "failed", reason: result.reason ?? "advance failed" };
|
|
13619
|
+
return { state: "blocked", reason: result.reason, ...facts };
|
|
13620
|
+
return { state: "failed", reason: result.reason ?? "advance failed", ...facts };
|
|
13621
|
+
},
|
|
13622
|
+
projectTask: async (root, taskId) => {
|
|
13623
|
+
const fresh = await projectAssurance(root, taskId, diffSnapshotOf);
|
|
13624
|
+
if (!fresh.claim || fresh.projection.lifecycle !== "active" || fresh.error !== null)
|
|
13625
|
+
return fresh;
|
|
13626
|
+
const { record, revision } = readTaskRecordRaw(root, taskId);
|
|
13627
|
+
if (!record || revision !== fresh.projection.record_revision)
|
|
13628
|
+
throw new Error(`Kernel recovery projection changed for ${taskId}`);
|
|
13629
|
+
return { ...fresh, ...batchQaFailureFacts(record) };
|
|
13055
13630
|
},
|
|
13056
|
-
projectTask: async (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
|
|
13057
13631
|
ownsTaskClaim: (taskId) => {
|
|
13058
13632
|
return registry.isChildConsumed(capability, taskId);
|
|
13059
13633
|
},
|