immune-brain 4.4.0 → 4.4.1
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/claude/mcp-server.mjs +343 -152
- package/plugins/immune-brain/dist/imm-loop.md +35 -2
- package/plugins/immune-brain/dist/role-prompts/executor.md +6 -0
- 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/plugin_version.ts +1 -1
- package/plugins/immune-brain/runtime/prompts/executor.md +6 -0
- package/plugins/immune-brain/runtime/unattended/batch_plan.ts +1 -1
- package/plugins/immune-brain/runtime/unattended/batch_preflight.ts +38 -8
- package/plugins/immune-brain/runtime/unattended/batch_reasons.ts +6 -0
- package/plugins/immune-brain/runtime/unattended/batch_runner.ts +84 -146
- package/plugins/immune-brain/runtime/unattended/batch_state.ts +11 -0
|
@@ -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.4.
|
|
42
|
+
var PLUGIN_VERSION = "4.4.1";
|
|
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 {
|
|
@@ -7857,7 +8003,6 @@ function initializeEnrollmentGitBase(root, input, previous, base, signal) {
|
|
|
7857
8003
|
|
|
7858
8004
|
// plugins/immune-brain/runtime/assurance/qa.ts
|
|
7859
8005
|
import { createHash as createHash17 } from "node:crypto";
|
|
7860
|
-
|
|
7861
8006
|
// plugins/immune-brain/runtime/assurance/delivery_workspace.ts
|
|
7862
8007
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
7863
8008
|
import { createHash as createHash16 } from "node:crypto";
|
|
@@ -8117,19 +8262,6 @@ function deliveryTreeForSnapshot(snapshot, writeTree) {
|
|
|
8117
8262
|
throw new Error("QA delivery identity does not match the frozen snapshot");
|
|
8118
8263
|
return writeDeliveryTree(snapshot.root, captured);
|
|
8119
8264
|
}
|
|
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
8265
|
async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
8134
8266
|
if (snapshot.role !== "qa")
|
|
8135
8267
|
throw new Error("deterministic QA requires qa role");
|
|
@@ -8157,46 +8289,80 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
|
8157
8289
|
live();
|
|
8158
8290
|
const ids = group.map((item) => item.id);
|
|
8159
8291
|
const environment = group[0].descriptor.environment;
|
|
8160
|
-
const
|
|
8161
|
-
|
|
8292
|
+
const diagnostics = (stage, outcome, affected = group, commandStage = "check", elapsed = 0, result) => affected.map((item) => ({
|
|
8293
|
+
acceptance_id: item.id,
|
|
8294
|
+
descriptor_ref: `acceptance/${item.index}/verification/${commandStage === "prepare" ? "environment/prepare" : "command"}`,
|
|
8295
|
+
descriptor_digest: `sha256:${createHash17("sha256").update(JSON.stringify(item.descriptor)).digest("hex")}`,
|
|
8296
|
+
stage,
|
|
8297
|
+
outcome,
|
|
8298
|
+
elapsed_ms: elapsed,
|
|
8299
|
+
exit_code: result?.exit_code ?? null,
|
|
8300
|
+
stdout_bytes: result ? Buffer.byteLength(result.stdout) : null,
|
|
8301
|
+
stderr_bytes: result ? Buffer.byteLength(result.stderr) : null
|
|
8302
|
+
}));
|
|
8303
|
+
const { delivery, home, deliveryPrefix } = (() => {
|
|
8304
|
+
let delivery, home;
|
|
8305
|
+
try {
|
|
8306
|
+
delivery = (options._materializeDeliveryWorkspace ?? materializeDeliveryWorkspace)(snapshot.root, tree);
|
|
8307
|
+
home = mkdtempSync3(join9(tmpdir4(), "imm-qa-home-"));
|
|
8308
|
+
return { delivery, home, deliveryPrefix: `${realpathSync10(delivery.root)}${sep6}` };
|
|
8309
|
+
} catch {
|
|
8310
|
+
let reason = "delivery_unavailable";
|
|
8311
|
+
try {
|
|
8312
|
+
try {
|
|
8313
|
+
delivery?.cleanup();
|
|
8314
|
+
} finally {
|
|
8315
|
+
if (home)
|
|
8316
|
+
removeTaskOwnedTree(home);
|
|
8317
|
+
}
|
|
8318
|
+
} catch {
|
|
8319
|
+
reason = "delivery_cleanup_failed";
|
|
8320
|
+
}
|
|
8321
|
+
throw new QaPreparationError("prepare", ids, reason, diagnostics("prepare", reason));
|
|
8322
|
+
}
|
|
8323
|
+
})();
|
|
8162
8324
|
const clean = () => {
|
|
8163
8325
|
if (options._materializeDeliveryWorkspace)
|
|
8164
8326
|
return;
|
|
8165
8327
|
try {
|
|
8166
8328
|
assertDeliveryClean(delivery.root, tree, delivery.seal, environment.writable_paths);
|
|
8167
8329
|
} catch {
|
|
8168
|
-
throw new QaPreparationError("integrity", ids, "protected_input_or_output_drift");
|
|
8330
|
+
throw new QaPreparationError("integrity", ids, "protected_input_or_output_drift", diagnostics("integrity", "protected_input_or_output_drift"));
|
|
8169
8331
|
}
|
|
8170
8332
|
};
|
|
8171
8333
|
const groupCommands = [];
|
|
8172
|
-
const
|
|
8173
|
-
|
|
8334
|
+
const execute = async (command, stage, affected = group) => {
|
|
8335
|
+
const started = performance.now();
|
|
8336
|
+
const fail = (errorStage, reason) => new QaPreparationError(errorStage, affected.map((item) => item.id), reason, diagnostics(errorStage, reason, affected, stage, Math.round(performance.now() - started)));
|
|
8174
8337
|
live();
|
|
8175
8338
|
clean();
|
|
8176
8339
|
let frozen;
|
|
8177
8340
|
try {
|
|
8178
8341
|
frozen = (options._resolveVerificationCommand ?? resolveVerificationCommand)(delivery.root, command, path);
|
|
8179
8342
|
} catch {
|
|
8180
|
-
throw
|
|
8343
|
+
throw fail("resolution", "executable_or_cwd_unavailable");
|
|
8181
8344
|
}
|
|
8182
8345
|
let result;
|
|
8183
8346
|
try {
|
|
8184
8347
|
result = await (options._runFixedVerification ?? runFixedVerification)(delivery.root, command, frozen, { signal: options.signal, home, path });
|
|
8185
8348
|
} catch (error) {
|
|
8349
|
+
if (error instanceof VerificationAbortedError)
|
|
8350
|
+
throw error;
|
|
8186
8351
|
if (error instanceof VerificationLaunchError)
|
|
8187
|
-
throw
|
|
8352
|
+
throw fail("resolution", "process_launch_failed");
|
|
8188
8353
|
if (error instanceof VerificationCleanupError)
|
|
8189
|
-
throw
|
|
8190
|
-
throw
|
|
8354
|
+
throw fail("resolution", "process_cleanup_failed");
|
|
8355
|
+
throw fail(stage, "execution_failed");
|
|
8191
8356
|
}
|
|
8192
8357
|
live();
|
|
8193
8358
|
clean();
|
|
8194
8359
|
groupCommands.push(frozen);
|
|
8195
8360
|
if (frozen?.entry) {
|
|
8361
|
+
const identityDiagnostics = diagnostics("integrity", "command_identity_changed", affected, stage, Math.round(performance.now() - started), result);
|
|
8196
8362
|
if (!command.executable.startsWith("./"))
|
|
8197
|
-
allCommands.push(frozen);
|
|
8363
|
+
allCommands.push({ frozen, diagnostics: identityDiagnostics });
|
|
8198
8364
|
else if (frozen.interpreter && !frozen.interpreter.path.startsWith(deliveryPrefix))
|
|
8199
|
-
allCommands.push({ entry: frozen.interpreter, interpreter: null, interpreter_args: [] });
|
|
8365
|
+
allCommands.push({ frozen: { entry: frozen.interpreter, interpreter: null, interpreter_args: [] }, diagnostics: identityDiagnostics });
|
|
8200
8366
|
}
|
|
8201
8367
|
evidence.push({
|
|
8202
8368
|
stage,
|
|
@@ -8209,29 +8375,31 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
|
8209
8375
|
output_limited: result.output_limited
|
|
8210
8376
|
});
|
|
8211
8377
|
if (Buffer.byteLength(JSON.stringify(evidence)) > 16384)
|
|
8212
|
-
throw
|
|
8378
|
+
throw fail("prepare", "execution_metadata_limit_exceeded");
|
|
8213
8379
|
return result;
|
|
8214
8380
|
};
|
|
8215
8381
|
try {
|
|
8216
8382
|
clean();
|
|
8217
8383
|
if (environment.prepare) {
|
|
8384
|
+
const prepareStarted = performance.now();
|
|
8218
8385
|
const result = await execute(environment.prepare, "prepare");
|
|
8219
8386
|
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");
|
|
8387
|
+
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
8388
|
}
|
|
8222
8389
|
for (const item of group) {
|
|
8223
|
-
const started =
|
|
8224
|
-
const progress = (phase) => options.onProgress?.({
|
|
8390
|
+
const started = performance.now();
|
|
8391
|
+
const progress = (phase, outcome, result) => options.onProgress?.({
|
|
8225
8392
|
index: item.index + 1,
|
|
8226
8393
|
total: snapshot.acceptance.length,
|
|
8227
8394
|
acceptance_id: item.id,
|
|
8228
8395
|
phase,
|
|
8229
|
-
elapsed_ms:
|
|
8396
|
+
elapsed_ms: Math.round(performance.now() - started),
|
|
8397
|
+
diagnostic: diagnostics("check", outcome, [item], "check", Math.round(performance.now() - started), result)[0]
|
|
8230
8398
|
});
|
|
8231
|
-
progress("running");
|
|
8232
|
-
const result = await execute(item.descriptor.command, "check");
|
|
8399
|
+
progress("running", "running");
|
|
8400
|
+
const result = await execute(item.descriptor.command, "check", [item]);
|
|
8233
8401
|
const failed = result.exit_code !== 0 || result.timed_out || result.output_limited;
|
|
8234
|
-
progress(failed ? "failed" : "passed");
|
|
8402
|
+
progress(failed ? "failed" : "passed", result.timed_out ? "timeout" : result.output_limited ? "output_limit" : failed ? "nonzero_exit" : "passed", result);
|
|
8235
8403
|
if (failed)
|
|
8236
8404
|
findings.push({
|
|
8237
8405
|
id: qaFindingId(item.id, snapshotDigest(snapshot)),
|
|
@@ -8241,22 +8409,33 @@ async function runDeterministicQa(snapshot, descriptors, options = {}) {
|
|
|
8241
8409
|
findings_digest: ""
|
|
8242
8410
|
});
|
|
8243
8411
|
}
|
|
8244
|
-
|
|
8245
|
-
|
|
8246
|
-
|
|
8412
|
+
try {
|
|
8413
|
+
for (const frozen of groupCommands)
|
|
8414
|
+
if (frozen?.entry)
|
|
8415
|
+
assertCommandIdentity(frozen);
|
|
8416
|
+
} catch {
|
|
8417
|
+
throw new QaPreparationError("integrity", ids, "command_identity_changed", diagnostics("integrity", "command_identity_changed"));
|
|
8247
8418
|
}
|
|
8248
8419
|
} finally {
|
|
8249
8420
|
try {
|
|
8250
|
-
|
|
8251
|
-
|
|
8252
|
-
|
|
8421
|
+
try {
|
|
8422
|
+
delivery.cleanup();
|
|
8423
|
+
} finally {
|
|
8424
|
+
removeTaskOwnedTree(home);
|
|
8425
|
+
}
|
|
8426
|
+
} catch {
|
|
8427
|
+
throw new QaPreparationError("integrity", ids, "delivery_cleanup_failed", diagnostics("integrity", "delivery_cleanup_failed"));
|
|
8253
8428
|
}
|
|
8254
8429
|
}
|
|
8255
8430
|
}
|
|
8256
8431
|
live();
|
|
8257
|
-
for (const
|
|
8258
|
-
|
|
8259
|
-
|
|
8432
|
+
for (const entry of allCommands) {
|
|
8433
|
+
try {
|
|
8434
|
+
if (entry.frozen?.entry)
|
|
8435
|
+
assertCommandIdentity(entry.frozen);
|
|
8436
|
+
} catch {
|
|
8437
|
+
throw new QaPreparationError("integrity", entry.diagnostics.map((item) => item.acceptance_id), "command_identity_changed", entry.diagnostics);
|
|
8438
|
+
}
|
|
8260
8439
|
}
|
|
8261
8440
|
const base = {
|
|
8262
8441
|
contract: "assurance_kernel/assurance_verdict/v2",
|
|
@@ -8296,6 +8475,11 @@ var BATCH_REASONS = Object.freeze({
|
|
|
8296
8475
|
reason: (detail) => `an active workspace claim already exists for task: ${detail}`,
|
|
8297
8476
|
recovery_action: "resolve or stop the active task before starting a batch in the current Host"
|
|
8298
8477
|
},
|
|
8478
|
+
kernel_authority_conflict: {
|
|
8479
|
+
state: "rejected",
|
|
8480
|
+
reason: (detail) => `Kernel authority conflict: ${detail}`,
|
|
8481
|
+
recovery_action: "resolve the Kernel storage conflict before resuming in the current Host"
|
|
8482
|
+
},
|
|
8299
8483
|
git_head_unreadable: {
|
|
8300
8484
|
state: "rejected",
|
|
8301
8485
|
reason: (detail) => detail,
|
|
@@ -10509,7 +10693,12 @@ function findExistingActiveBatch(root, initiativeSlug) {
|
|
|
10509
10693
|
}
|
|
10510
10694
|
if (isTerminalBatchState(candidate.batch_state))
|
|
10511
10695
|
continue;
|
|
10512
|
-
|
|
10696
|
+
try {
|
|
10697
|
+
const record = readBatchRunState(root, file.slice(0, -5));
|
|
10698
|
+
if (record)
|
|
10699
|
+
return { corrupt: false, record };
|
|
10700
|
+
} catch {}
|
|
10701
|
+
return { corrupt: true, path: file };
|
|
10513
10702
|
}
|
|
10514
10703
|
return null;
|
|
10515
10704
|
}
|
|
@@ -10635,7 +10824,9 @@ async function projectPlanSurface(input) {
|
|
|
10635
10824
|
const riskByTask = new Map;
|
|
10636
10825
|
let budget;
|
|
10637
10826
|
if (isResuming && existingBatch) {
|
|
10638
|
-
budget = existingBatch.budget;
|
|
10827
|
+
budget = { ...existingBatch.budget };
|
|
10828
|
+
if (Date.parse(budget.deadline_at) <= Date.parse(now))
|
|
10829
|
+
budget.deadline_at = new Date(Date.parse(now) + DEFAULT_DEADLINE_MS).toISOString();
|
|
10639
10830
|
try {
|
|
10640
10831
|
recoveryChildren = existingBatch.children.map((c) => {
|
|
10641
10832
|
const intentPath = `docs/plans/${c.task_id}.intent.json`;
|
|
@@ -10738,6 +10929,11 @@ async function projectBatchPreflight(options) {
|
|
|
10738
10929
|
const isResuming = activeRecord !== null;
|
|
10739
10930
|
const batchBranch = `imm/${initiativeSlug}`;
|
|
10740
10931
|
const activeTaskId = readActiveClaimTaskId(root);
|
|
10932
|
+
if (activeTaskId) {
|
|
10933
|
+
const authority = reconcileKernelAuthority(root, activeTaskId);
|
|
10934
|
+
if (authority.state === "authority_conflict")
|
|
10935
|
+
return reject("kernel_authority_conflict", authority.diagnostic ?? "unverifiable workspace authority");
|
|
10936
|
+
}
|
|
10741
10937
|
const ownClaim = isResuming && activeTaskId !== null && isOwnBatchClaim(root, activeRecord, activeTaskId, batchBranch);
|
|
10742
10938
|
if (activeTaskId && !ownClaim)
|
|
10743
10939
|
return reject("claim_already_active", activeTaskId);
|
|
@@ -10851,11 +11047,15 @@ async function authorizeBatch(options) {
|
|
|
10851
11047
|
} = projection;
|
|
10852
11048
|
const existingBatch = projection.existing_batch;
|
|
10853
11049
|
const isExistingExpired = isResuming && Date.parse(existingBatch.authorization_expires_at) <= Date.now();
|
|
10854
|
-
const
|
|
11050
|
+
const branchBefore = spawnSync5("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
|
|
11051
|
+
if (branchBefore.status !== 0)
|
|
11052
|
+
return { outcome: "rejected", rejection: batchRejection("repository_became_unreadable") };
|
|
10855
11053
|
const reuseBlockers = [];
|
|
10856
11054
|
if (isResuming && existingBatch) {
|
|
10857
11055
|
if (isExistingExpired)
|
|
10858
11056
|
reuseBlockers.push("batch_authorization_expired");
|
|
11057
|
+
if (Date.parse(existingBatch.budget.deadline_at) <= Date.now())
|
|
11058
|
+
reuseBlockers.push("batch_budget_expired");
|
|
10859
11059
|
if (existingBatch.batch_state !== "running")
|
|
10860
11060
|
reuseBlockers.push("batch_not_running");
|
|
10861
11061
|
if (existingBatch.plan_digest !== planDigest)
|
|
@@ -10866,6 +11066,7 @@ async function authorizeBatch(options) {
|
|
|
10866
11066
|
reuseBlockers.push("batch_head_lineage_moved");
|
|
10867
11067
|
}
|
|
10868
11068
|
const reuseAuthorization = isResuming && reuseBlockers.length === 0;
|
|
11069
|
+
const expiresAt = reuseAuthorization ? existingBatch.authorization_expires_at : budget.deadline_at;
|
|
10869
11070
|
const batchId = isResuming && existingBatch ? existingBatch.batch_id : `batch-${initiativeSlug}-${randomUUID7()}`;
|
|
10870
11071
|
const facts = {
|
|
10871
11072
|
initiative_slug: initiativeSlug,
|
|
@@ -10909,6 +11110,16 @@ async function authorizeBatch(options) {
|
|
|
10909
11110
|
const finalOwnClaim = isResuming && finalClaimTaskId !== null && isOwnBatchClaim(root, existingBatch, finalClaimTaskId, batchBranch);
|
|
10910
11111
|
if (finalClaimTaskId && !finalOwnClaim)
|
|
10911
11112
|
return { outcome: "rejected", rejection: batchRejection("claim_appeared_during_confirmation", finalClaimTaskId) };
|
|
11113
|
+
if (isResuming) {
|
|
11114
|
+
const current = findExistingActiveBatch(root, initiativeSlug);
|
|
11115
|
+
if (!current || current.corrupt || JSON.stringify(current.record) !== JSON.stringify(existingBatch))
|
|
11116
|
+
return { outcome: "rejected", rejection: batchRejection("plan_changed") };
|
|
11117
|
+
}
|
|
11118
|
+
const branchAfter = spawnSync5("git", ["-C", root, "branch", "--show-current"], { encoding: "utf8" });
|
|
11119
|
+
if (branchAfter.status !== 0 || branchAfter.stdout !== branchBefore.stdout)
|
|
11120
|
+
return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "Git branch moved after native confirmation") };
|
|
11121
|
+
if (Date.parse(expiresAt) <= Date.now())
|
|
11122
|
+
return { outcome: "rejected", rejection: batchRejection("confirmation_failed", "confirmed batch deadline expired before authorization issuance") };
|
|
10912
11123
|
const binding = {
|
|
10913
11124
|
batch_id: batchId,
|
|
10914
11125
|
initiative_slug: initiativeSlug,
|
|
@@ -11474,6 +11685,14 @@ async function lookupBatchCommit(input) {
|
|
|
11474
11685
|
}
|
|
11475
11686
|
|
|
11476
11687
|
// plugins/immune-brain/runtime/unattended/batch_runner.ts
|
|
11688
|
+
function batchQaFailureFacts(record) {
|
|
11689
|
+
const failed = record.history.filter((event) => event.type === "request_rework" && event.authority?.authority_kind === "qa");
|
|
11690
|
+
return {
|
|
11691
|
+
qa_failure_count: failed.length,
|
|
11692
|
+
last_qa_failure_at: failed.at(-1)?.at ?? null,
|
|
11693
|
+
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 }))
|
|
11694
|
+
};
|
|
11695
|
+
}
|
|
11477
11696
|
function dependentsOf(record, taskId) {
|
|
11478
11697
|
return record.children.filter((child) => child.blocked_by.includes(taskId));
|
|
11479
11698
|
}
|
|
@@ -11533,8 +11752,23 @@ function reportFor(record, reason, nextAction) {
|
|
|
11533
11752
|
created_at: record.updated_at
|
|
11534
11753
|
};
|
|
11535
11754
|
}
|
|
11536
|
-
function
|
|
11537
|
-
const
|
|
11755
|
+
function executorHandoff(record, taskId, fresh, failure = {}) {
|
|
11756
|
+
const recovery = deriveAssuranceRecovery(taskId, fresh, failure, fresh.recovery_findings);
|
|
11757
|
+
return {
|
|
11758
|
+
...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.")),
|
|
11759
|
+
...recovery ? { recovery } : {},
|
|
11760
|
+
...failure.diagnostics?.length ? { diagnostics: failure.diagnostics } : {},
|
|
11761
|
+
handoff: recovery?.category === "authorization" ? undefined : {
|
|
11762
|
+
role: "executor",
|
|
11763
|
+
task_id: taskId,
|
|
11764
|
+
run_id: fresh.projection.run_id ?? null,
|
|
11765
|
+
record_revision: fresh.projection.record_revision,
|
|
11766
|
+
next_obligation: fresh.projection.next_obligation
|
|
11767
|
+
}
|
|
11768
|
+
};
|
|
11769
|
+
}
|
|
11770
|
+
function finalize(root, record, reason, nextAction, details = {}) {
|
|
11771
|
+
const report = { ...reportFor(record, reason, nextAction), ...details };
|
|
11538
11772
|
if (isTerminalBatchState(record.batch_state) || record.batch_state === "needs_human") {
|
|
11539
11773
|
writeBatchRunReport(root, report);
|
|
11540
11774
|
}
|
|
@@ -11724,7 +11958,6 @@ async function startBatchLocked(input) {
|
|
|
11724
11958
|
authorization_expires_at: input.authorization_expires_at,
|
|
11725
11959
|
budget: input.budget,
|
|
11726
11960
|
batch_state: "running",
|
|
11727
|
-
consecutive_qa_failures: 0,
|
|
11728
11961
|
children: resumedChildren
|
|
11729
11962
|
});
|
|
11730
11963
|
}
|
|
@@ -11882,67 +12115,8 @@ async function startBatchLocked(input) {
|
|
|
11882
12115
|
break;
|
|
11883
12116
|
}
|
|
11884
12117
|
}
|
|
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
|
-
}
|
|
12118
|
+
const enrolled = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
|
|
12119
|
+
return executorHandoff(record, child.task_id, enrolled);
|
|
11946
12120
|
}
|
|
11947
12121
|
return finalize(input.root, record, stopReasonFor(record), "");
|
|
11948
12122
|
}
|
|
@@ -12001,11 +12175,30 @@ async function resumeBatch(input, projection) {
|
|
|
12001
12175
|
});
|
|
12002
12176
|
return finalize(input.root, existing, "claim held by another batch", "needs-human-attention");
|
|
12003
12177
|
}
|
|
12178
|
+
if (holdsClaim) {
|
|
12179
|
+
const failures = Math.max(existing.consecutive_qa_failures, fresh.qa_failure_count ?? 0);
|
|
12180
|
+
if (failures >= existing.budget.qa_failure_limit && fresh.last_qa_failure_at && Date.parse(fresh.last_qa_failure_at) > Date.parse(existing.confirmation_time)) {
|
|
12181
|
+
const reason = "QA failure limit reached; repair and settle the own child through Kernel before resuming the batch";
|
|
12182
|
+
skipDependents(existing, driven.task_id, `dependency ${driven.task_id} parked`);
|
|
12183
|
+
existing = writeBatchRunState(input.root, {
|
|
12184
|
+
...existing,
|
|
12185
|
+
batch_state: "needs_human",
|
|
12186
|
+
consecutive_qa_failures: failures,
|
|
12187
|
+
children: existing.children.map((c) => c.task_id === driven.task_id ? { ...c, state: "needs_human", reason } : c)
|
|
12188
|
+
});
|
|
12189
|
+
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 });
|
|
12190
|
+
}
|
|
12191
|
+
if (failures !== existing.consecutive_qa_failures)
|
|
12192
|
+
existing = writeBatchRunState(input.root, { ...existing, consecutive_qa_failures: failures });
|
|
12193
|
+
if (fresh.projection.next_obligation === "run_qa" || fresh.projection.artifact_state !== "frozen")
|
|
12194
|
+
return executorHandoff(existing, driven.task_id, fresh);
|
|
12195
|
+
}
|
|
12004
12196
|
if (!holdsClaim) {
|
|
12005
12197
|
const settledRemotely = fresh.projection.lifecycle === "done" && fresh.projection.completion_ready === true;
|
|
12006
12198
|
if (settledRemotely) {
|
|
12007
12199
|
existing = writeBatchRunState(input.root, {
|
|
12008
12200
|
...existing,
|
|
12201
|
+
consecutive_qa_failures: 0,
|
|
12009
12202
|
children: existing.children.map((c) => c.task_id === driven.task_id ? { ...c, state: "settled", reason: "crash after settlement; resuming at commit" } : c)
|
|
12010
12203
|
});
|
|
12011
12204
|
} else {
|
|
@@ -12108,30 +12301,8 @@ async function driveInterruptedChild(input, child) {
|
|
|
12108
12301
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "settled", reason: null } : c);
|
|
12109
12302
|
record.consecutive_qa_failures = 0;
|
|
12110
12303
|
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
|
-
}
|
|
12304
|
+
child = { ...child, state: "settled", reason: null };
|
|
12305
|
+
continue;
|
|
12135
12306
|
} else if (terminal.state === "review_ready") {
|
|
12136
12307
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, reason: `review reservation ${terminal.operation_id} open` } : c);
|
|
12137
12308
|
persist();
|
|
@@ -12139,16 +12310,22 @@ async function driveInterruptedChild(input, child) {
|
|
|
12139
12310
|
} else if (terminal.state === "rework" && terminal.operation === "qa") {
|
|
12140
12311
|
record.consecutive_qa_failures += 1;
|
|
12141
12312
|
if (record.consecutive_qa_failures >= record.budget.qa_failure_limit) {
|
|
12142
|
-
const reason =
|
|
12313
|
+
const reason = "QA failure limit reached";
|
|
12143
12314
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
|
|
12144
12315
|
record.batch_state = "needs_human";
|
|
12145
12316
|
skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
12146
12317
|
persist();
|
|
12147
|
-
|
|
12318
|
+
const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
|
|
12319
|
+
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
12320
|
}
|
|
12149
12321
|
persist();
|
|
12322
|
+
const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
|
|
12323
|
+
return executorHandoff(record, child.task_id, fresh, terminal);
|
|
12324
|
+
} else if (terminal.state === "rework" || terminal.environment_failure || terminal.state === "review_preparation_failed" || terminal.recovery?.category === "repair") {
|
|
12325
|
+
const fresh = requireFreshProjection(await input.kernel.projectTask(input.root, child.task_id), child.task_id);
|
|
12326
|
+
return executorHandoff(record, child.task_id, fresh, terminal);
|
|
12150
12327
|
} else {
|
|
12151
|
-
const reason = terminal.state === "stopped" ? "Kernel reported the child stopped" : terminal.
|
|
12328
|
+
const reason = terminal.state === "stopped" ? "Kernel reported the child stopped" : terminal.reason;
|
|
12152
12329
|
record.children = record.children.map((c) => c.task_id === child.task_id ? { ...c, state: "needs_human", reason } : c);
|
|
12153
12330
|
record.batch_state = "needs_human";
|
|
12154
12331
|
skipDependents(record, child.task_id, `dependency ${child.task_id} parked`);
|
|
@@ -12855,7 +13032,7 @@ class ClaudeRuntime {
|
|
|
12855
13032
|
throw new NativeAuthorityError("unsupported_host", "interactive MCP elicitation is unavailable");
|
|
12856
13033
|
if (!this.requestConfirmation)
|
|
12857
13034
|
throw new NativeAuthorityError("interaction_not_opened", batchReason("confirmation_port_unavailable").reason);
|
|
12858
|
-
|
|
13035
|
+
let now = new Date().toISOString();
|
|
12859
13036
|
const preflight = await projectBatchPreflight({
|
|
12860
13037
|
root: this.cwd,
|
|
12861
13038
|
initiative_slug: initiativeSlug,
|
|
@@ -12969,6 +13146,9 @@ class ClaudeRuntime {
|
|
|
12969
13146
|
const { binding, batch_id: batchId, expires_at: expiresAt } = authorization;
|
|
12970
13147
|
if (meta.signal?.aborted)
|
|
12971
13148
|
return batchReason("cancelled_before_execution");
|
|
13149
|
+
now = new Date().toISOString();
|
|
13150
|
+
if (Date.parse(expiresAt) <= Date.parse(now))
|
|
13151
|
+
return batchReason("confirmation_failed", "confirmed batch deadline expired before authorization issuance");
|
|
12972
13152
|
const capability = this.batchRegistry.issue(binding, recoveryChildren, now);
|
|
12973
13153
|
const basePort = this.createBatchKernelPort(this.batchRegistry, capability, binding);
|
|
12974
13154
|
const kernelPort = {
|
|
@@ -13041,19 +13221,30 @@ class ClaudeRuntime {
|
|
|
13041
13221
|
},
|
|
13042
13222
|
advanceTask: async (root, taskId) => {
|
|
13043
13223
|
const result = await this.coordinator.advance(taskId, { cwd: root });
|
|
13224
|
+
const facts = { diagnostics: result.diagnostics, environment_failure: result.environment_failure, recovery: result.recovery };
|
|
13044
13225
|
if (result.state === "completed")
|
|
13045
|
-
return { state: "completed" };
|
|
13226
|
+
return { state: "completed", ...facts };
|
|
13046
13227
|
if (result.state === "stopped")
|
|
13047
|
-
return { state: "stopped" };
|
|
13228
|
+
return { state: "stopped", ...facts };
|
|
13048
13229
|
if (result.state === "rework")
|
|
13049
|
-
return { state: "rework", operation: result.operation, summary: result.summary };
|
|
13230
|
+
return { state: "rework", operation: result.operation, summary: result.summary, ...facts };
|
|
13050
13231
|
if (result.state === "review_ready")
|
|
13051
|
-
return { state: "review_ready", operation_id: result.operation_id };
|
|
13232
|
+
return { state: "review_ready", operation_id: result.operation_id, ...facts };
|
|
13233
|
+
if (result.state === "review_preparation_failed")
|
|
13234
|
+
return { state: result.state, operation: result.operation, operation_id: result.operation_id, reason: result.reason, ...facts };
|
|
13052
13235
|
if (result.state === "blocked")
|
|
13053
|
-
return { state: "blocked", reason: result.reason };
|
|
13054
|
-
return { state: "failed", reason: result.reason ?? "advance failed" };
|
|
13236
|
+
return { state: "blocked", reason: result.reason, ...facts };
|
|
13237
|
+
return { state: "failed", reason: result.reason ?? "advance failed", ...facts };
|
|
13238
|
+
},
|
|
13239
|
+
projectTask: async (root, taskId) => {
|
|
13240
|
+
const fresh = await projectAssurance(root, taskId, diffSnapshotOf);
|
|
13241
|
+
if (!fresh.claim || fresh.projection.lifecycle !== "active" || fresh.error !== null)
|
|
13242
|
+
return fresh;
|
|
13243
|
+
const { record, revision } = readTaskRecordRaw(root, taskId);
|
|
13244
|
+
if (!record || revision !== fresh.projection.record_revision)
|
|
13245
|
+
throw new Error(`Kernel recovery projection changed for ${taskId}`);
|
|
13246
|
+
return { ...fresh, ...batchQaFailureFacts(record) };
|
|
13055
13247
|
},
|
|
13056
|
-
projectTask: async (root, taskId) => projectAssurance(root, taskId, diffSnapshotOf),
|
|
13057
13248
|
ownsTaskClaim: (taskId) => {
|
|
13058
13249
|
return registry.isChildConsumed(capability, taskId);
|
|
13059
13250
|
},
|