@henryqw/pi-subagent 27.5.3 → 27.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -3
- package/dist/direct-herdr.d.ts +10 -5
- package/dist/direct-herdr.js +9 -4
- package/dist/integration-git.d.ts +3 -1
- package/dist/integration-git.js +2 -1
- package/dist/launch-runtime.d.ts +3 -1
- package/dist/launch-runtime.js +7 -4
- package/dist/runner.d.ts +17 -0
- package/dist/runner.js +138 -88
- package/dist/schema.d.ts +143 -2
- package/dist/schema.js +38 -6
- package/docs/orchestration.md +37 -6
- package/extensions/isolated.ts +56 -3
- package/extensions/result-transport.ts +55 -2
- package/extensions/subagent-command.ts +8 -4
- package/extensions/subagent.ts +89 -23
- package/extensions/workflow.ts +144 -4
- package/package.json +1 -1
package/dist/runner.js
CHANGED
|
@@ -5,7 +5,7 @@ import { utf8Prefix } from "./ephemeral.js";
|
|
|
5
5
|
import { createChildWorktree, finalizeChildWorktree, inspectWorktreeDirty } from "./worktree.js";
|
|
6
6
|
import { runGit } from "./git-process.js";
|
|
7
7
|
import { IntegrationGit } from "./integration-git.js";
|
|
8
|
-
import { checkBatchPasses, CLEANUP_KINDS, isCleanCommitted, MAX_PERSISTED_RUNTIME_TEXT_BYTES, MAX_POSSIBLE_RESOURCES, parseExecuteRequest, parseIntegrationAction, integrationGenerationPasses, parseResumeRequest, parseStageRequest, reviewEvidencePasses, sameIdentity, RUN_STATE_VERSION, } from "./schema.js";
|
|
8
|
+
import { checkBatchPasses, CLEANUP_KINDS, isCleanCommitted, MAX_PERSISTED_RUNTIME_TEXT_BYTES, MAX_POSSIBLE_RESOURCES, parseExecuteRequest, parseIntegrationAction, integrationGenerationPasses, parseResumeRequest, parseStageRequest, reusableCheckSource, reviewEvidencePasses, sameIdentity, formatCheckCommand, stagedCheckSources, RUN_STATE_VERSION, } from "./schema.js";
|
|
9
9
|
import { FileRunStore, ProductiveRunLeaseBusyError } from "./store.js";
|
|
10
10
|
const TRUNCATION_MARKER = "\n[truncated]";
|
|
11
11
|
const TEXT_TASK_PROMPT_MAX_BYTES = 64 * 1024;
|
|
@@ -391,6 +391,11 @@ function exactCommandResults(results, checks) {
|
|
|
391
391
|
&& result.args.every((arg, argIndex) => arg === check.args[argIndex]);
|
|
392
392
|
});
|
|
393
393
|
}
|
|
394
|
+
function reusedCheckLines(checks) {
|
|
395
|
+
return (checks?.results ?? []).flatMap((result) => result.reusedFrom
|
|
396
|
+
? [`Combined check ${formatCheckCommand(result)} reused the passed preliminary result of ${result.reusedFrom.taskId}#${result.reusedFrom.attempt}@${result.reusedFrom.candidate.head} on the same tree; it did not run again.`]
|
|
397
|
+
: []);
|
|
398
|
+
}
|
|
394
399
|
function isCommitNudge(prompt) {
|
|
395
400
|
return prompt.kind === "followup" && prompt.instruction === COMMIT_NUDGE_INSTRUCTION;
|
|
396
401
|
}
|
|
@@ -592,6 +597,8 @@ export function nextIntegrationAction(state) {
|
|
|
592
597
|
return `Call subagent_integrate refresh with generation ${generation.number + 1}, expectedTip ${JSON.stringify(generation.combinedTip)}, and identical expectedMain and newMain; then restage the candidates and validate again.`;
|
|
593
598
|
}
|
|
594
599
|
return undefined;
|
|
600
|
+
case "promoted":
|
|
601
|
+
return `Promotion is proven, but cleanup retained resources; resolve the recorded cleanup failures without discarding work, then call subagent_integrate cleanup with ${exact}.`;
|
|
595
602
|
default:
|
|
596
603
|
return undefined;
|
|
597
604
|
}
|
|
@@ -800,12 +807,12 @@ export class IsolatedRunner {
|
|
|
800
807
|
if (error instanceof DurableRunStopped)
|
|
801
808
|
throw error;
|
|
802
809
|
}
|
|
803
|
-
|
|
810
|
+
/** Every execute check before durable state; a dry run also skips the Herdr host preflight. */
|
|
811
|
+
async admitExecute(value, cwd, scope, dryRun) {
|
|
804
812
|
const request = parseExecuteRequest(value);
|
|
805
813
|
const policy = Object.freeze({ ...this.currentPolicy() });
|
|
806
|
-
const scope = new ProductiveScope(outerSignal);
|
|
807
814
|
const canonicalCwd = realpathSync.native(cwd);
|
|
808
|
-
const prepared = await scope.call(async (context) => await this.coordinatorRuntime.preflight({ request, cwd: canonicalCwd }, context));
|
|
815
|
+
const prepared = await scope.call(async (context) => await this.coordinatorRuntime.preflight({ request, cwd: canonicalCwd, ...(dryRun ? { dryRun } : {}) }, context));
|
|
809
816
|
const root = realpathSync.native(prepared.root);
|
|
810
817
|
if (root !== prepared.root)
|
|
811
818
|
throw new Error("Preflight repository root must be canonical.");
|
|
@@ -822,6 +829,16 @@ export class IsolatedRunner {
|
|
|
822
829
|
if (this.hasAnyActiveControl(root)) {
|
|
823
830
|
throw new Error("Another Pi Subagent request has active changeset work in this repository.");
|
|
824
831
|
}
|
|
832
|
+
return { request, policy, root, preparedMain, routes: prepared.routes ?? [] };
|
|
833
|
+
}
|
|
834
|
+
/** Dry run of execute: no state, lock, lease, worktree, or Herdr call. */
|
|
835
|
+
async plan(value, cwd, outerSignal) {
|
|
836
|
+
const { request, root, preparedMain, routes } = await this.admitExecute(value, cwd, new ProductiveScope(outerSignal), true);
|
|
837
|
+
return { request, root, main: preparedMain, routes };
|
|
838
|
+
}
|
|
839
|
+
async execute(value, cwd, outerSignal) {
|
|
840
|
+
const scope = new ProductiveScope(outerSignal);
|
|
841
|
+
const { request, policy, root, preparedMain } = await this.admitExecute(value, cwd, scope, false);
|
|
825
842
|
return await this.withProductiveRun(root, async (lifecycle) => {
|
|
826
843
|
const createdAt = this.coordinatorRuntime.now();
|
|
827
844
|
const state = {
|
|
@@ -1062,6 +1079,8 @@ export class IsolatedRunner {
|
|
|
1062
1079
|
if (action.action === "reject" && allocationByKind(attempt, "agent")) {
|
|
1063
1080
|
if (await this.terminateWithSafety(handle, task, attempt, action.candidate, scope.signal))
|
|
1064
1081
|
task.status = "rejected";
|
|
1082
|
+
else
|
|
1083
|
+
this.attention(task, `Worker termination is unproved: ${attempt.termination.failure}`);
|
|
1065
1084
|
await this.saveProductive(handle);
|
|
1066
1085
|
}
|
|
1067
1086
|
if (action.action === "revise") {
|
|
@@ -1449,10 +1468,11 @@ export class IsolatedRunner {
|
|
|
1449
1468
|
throw new Error("Combined tip changed the admitted canonical root test script.");
|
|
1450
1469
|
}
|
|
1451
1470
|
await this.requireExactIntegration(handle, generation, receipts, scope);
|
|
1452
|
-
const
|
|
1471
|
+
const sources = stagedCheckSources(generation, state.integration.candidates);
|
|
1472
|
+
const checks = await this.runCheckBatch(handle, request.finalChecks, action.expectedTip, "final", scope, undefined, generation.worktree.path, request.finalChecks.map((check) => reusableCheckSource(check, action.expectedTip, sources)));
|
|
1453
1473
|
generation.checks = checks;
|
|
1454
1474
|
await this.saveProductive(handle);
|
|
1455
|
-
if (!checkBatchPasses(checks, request.finalChecks, action.expectedTip))
|
|
1475
|
+
if (!checkBatchPasses(checks, request.finalChecks, action.expectedTip, sources))
|
|
1456
1476
|
throw new Error("Combined full-suite check failed or changed the integration tip.");
|
|
1457
1477
|
await this.requireExactIntegration(handle, generation, receipts, scope);
|
|
1458
1478
|
if (request.finalJudgment) {
|
|
@@ -1464,12 +1484,12 @@ export class IsolatedRunner {
|
|
|
1464
1484
|
}
|
|
1465
1485
|
}
|
|
1466
1486
|
await this.requireExactIntegration(handle, generation, receipts, scope);
|
|
1467
|
-
if (!integrationGenerationPasses(generation, request))
|
|
1487
|
+
if (!integrationGenerationPasses(generation, request, state.integration.candidates))
|
|
1468
1488
|
throw new Error("Combined validation is incomplete.");
|
|
1469
1489
|
generation.status = "ready";
|
|
1470
1490
|
}
|
|
1471
1491
|
catch (error) {
|
|
1472
|
-
if (integrationGenerationPasses(generation, request)) {
|
|
1492
|
+
if (integrationGenerationPasses(generation, request, state.integration.candidates)) {
|
|
1473
1493
|
delete generation.checks;
|
|
1474
1494
|
delete generation.review; // Drift invalidates otherwise passing evidence.
|
|
1475
1495
|
}
|
|
@@ -1479,7 +1499,7 @@ export class IsolatedRunner {
|
|
|
1479
1499
|
await this.saveProductive(handle);
|
|
1480
1500
|
return this.response(state);
|
|
1481
1501
|
}
|
|
1482
|
-
if (generation.status !== "ready" || !integrationGenerationPasses(generation, request)) {
|
|
1502
|
+
if (generation.status !== "ready" || !integrationGenerationPasses(generation, request, state.integration.candidates)) {
|
|
1483
1503
|
throw new Error("Promotion requires exact successful combined validation.");
|
|
1484
1504
|
}
|
|
1485
1505
|
for (const definition of request.tasks.filter((task) => hasChangesetDependency(state, task.id))) {
|
|
@@ -1506,6 +1526,7 @@ export class IsolatedRunner {
|
|
|
1506
1526
|
outcome = await scope.call((context) => this.integrationGit.promote({
|
|
1507
1527
|
root, integration: generation.worktree, base: generation.expectedMain, stages: receipts,
|
|
1508
1528
|
checks: generation.checks, commands: request.finalChecks,
|
|
1529
|
+
sources: stagedCheckSources(generation, state.integration.candidates),
|
|
1509
1530
|
review: generation.review, criterion: request.finalJudgment?.criterion, correction: generation.correction,
|
|
1510
1531
|
}, context.signal));
|
|
1511
1532
|
}
|
|
@@ -1605,101 +1626,123 @@ export class IsolatedRunner {
|
|
|
1605
1626
|
// Promotion proves selection independently of worker or checkout cleanup.
|
|
1606
1627
|
for (const stage of generation.stages)
|
|
1607
1628
|
changesetTaskState(state, stage.taskId).status = "completed";
|
|
1629
|
+
const generationCleanup = generation.cleanup ??= [{ kind: "worktree", status: "pending" }, { kind: "branch", status: "pending" }];
|
|
1630
|
+
const stepLists = [...generation.stages.map((stage) => changesetTaskState(state, stage.taskId).attempts[stage.attempt - 1].cleanup),
|
|
1631
|
+
generationCleanup];
|
|
1632
|
+
// Record each failure on the step it blocks; other stages and the integration checkout continue.
|
|
1633
|
+
const blocked = (steps, error) => {
|
|
1634
|
+
const step = steps.find((item) => item.status === "running") ?? steps.find((item) => item.status !== "completed");
|
|
1635
|
+
if (step)
|
|
1636
|
+
step.failure = bounded(`Cleanup requires attention: ${errorText(error)}`);
|
|
1637
|
+
};
|
|
1608
1638
|
await this.saveProductive(handle);
|
|
1639
|
+
let main;
|
|
1609
1640
|
try {
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1641
|
+
main = await this.callProductive(handle, scope, (context) => this.gitRuntime.inspectMain({ root: state.root }, context));
|
|
1642
|
+
}
|
|
1643
|
+
catch (error) {
|
|
1644
|
+
for (const steps of stepLists)
|
|
1645
|
+
blocked(steps, error);
|
|
1646
|
+
}
|
|
1647
|
+
if (main && !sameIdentity(main, promoted)) {
|
|
1648
|
+
for (const steps of stepLists)
|
|
1649
|
+
blocked(steps, new Error("Main changed or became dirty after promotion; cleanup is blocked."));
|
|
1650
|
+
}
|
|
1651
|
+
else if (main) {
|
|
1613
1652
|
for (const stage of generation.stages) {
|
|
1614
1653
|
const task = changesetTaskState(state, stage.taskId);
|
|
1615
1654
|
const attempt = task.attempts[stage.attempt - 1];
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
candidate.worker = "release_pending";
|
|
1622
|
-
await this.saveProductive(handle);
|
|
1623
|
-
if (attempt.termination?.status !== "terminated") {
|
|
1624
|
-
if (attempt.termination) {
|
|
1625
|
-
const result = await this.callProductive(handle, scope, (context) => this.hostRuntime.reconcileWorkerTermination({
|
|
1626
|
-
task: changesetTaskRequest(state, task.taskId), attempt,
|
|
1627
|
-
workerId: attempt.termination.workerId, candidate: stage.source,
|
|
1628
|
-
}, context));
|
|
1629
|
-
if (result.outcome !== "terminated")
|
|
1630
|
-
throw new Error("Worker termination cannot be proved; never replay termination.");
|
|
1631
|
-
attempt.termination = { status: "terminated", workerId: attempt.termination.workerId,
|
|
1632
|
-
candidate: stage.source, at: this.coordinatorRuntime.now() };
|
|
1633
|
-
await this.saveProductive(handle);
|
|
1634
|
-
}
|
|
1635
|
-
else if (!await this.terminateWithSafety(handle, task, attempt, stage.source, scope.signal)) {
|
|
1636
|
-
throw new Error("Worker termination was not proved.");
|
|
1637
|
-
}
|
|
1655
|
+
try {
|
|
1656
|
+
await this.cleanupPromotedStage(handle, generation, receipts, promoted, stage, task, attempt, scope);
|
|
1657
|
+
}
|
|
1658
|
+
catch (error) {
|
|
1659
|
+
blocked(attempt.cleanup, error);
|
|
1638
1660
|
}
|
|
1639
|
-
|
|
1640
|
-
|
|
1661
|
+
await this.saveProductive(handle);
|
|
1662
|
+
}
|
|
1663
|
+
try {
|
|
1664
|
+
for (const step of generationCleanup) {
|
|
1641
1665
|
if (step.status === "completed")
|
|
1642
1666
|
continue;
|
|
1643
1667
|
step.status = "running";
|
|
1644
1668
|
await this.saveProductive(handle);
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
}, context));
|
|
1649
|
-
if (result.outcome === "blocked")
|
|
1650
|
-
throw new Error(result.failure);
|
|
1651
|
-
}
|
|
1652
|
-
else {
|
|
1653
|
-
const result = await this.callProductive(handle, scope, (context) => this.integrationGit.cleanup(state.root, generation.worktree, worker, generation.expectedMain, receipts, promoted, kind, context.signal, generation.correction));
|
|
1654
|
-
if (result.outcome !== "ready")
|
|
1655
|
-
throw new Error(result.failure);
|
|
1656
|
-
}
|
|
1669
|
+
const result = await this.callProductive(handle, scope, (context) => this.integrationGit.cleanup(state.root, generation.worktree, generation.worktree, generation.expectedMain, receipts, promoted, step.kind, context.signal, generation.correction));
|
|
1670
|
+
if (result.outcome !== "ready")
|
|
1671
|
+
throw new Error(result.failure);
|
|
1657
1672
|
step.status = "completed";
|
|
1658
1673
|
delete step.failure;
|
|
1659
1674
|
await this.saveProductive(handle);
|
|
1660
1675
|
}
|
|
1661
|
-
candidate.worker = "released";
|
|
1662
|
-
// Same-attempt revisions share this exact worker checkout; release historical
|
|
1663
|
-
// rejected tips only after the physical checkout has been proved removed.
|
|
1664
|
-
for (const old of state.integration.candidates.filter((item) => item.decision === "rejected"
|
|
1665
|
-
&& item.taskId === stage.taskId && item.attempt === stage.attempt))
|
|
1666
|
-
old.worker = "released";
|
|
1667
|
-
await this.saveProductive(handle);
|
|
1668
1676
|
}
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
for (const kind of ["worktree", "branch"]) {
|
|
1672
|
-
const step = generation.cleanup.find((item) => item.kind === kind);
|
|
1673
|
-
if (step.status === "completed")
|
|
1674
|
-
continue;
|
|
1675
|
-
step.status = "running";
|
|
1676
|
-
await this.saveProductive(handle);
|
|
1677
|
-
const result = await this.callProductive(handle, scope, (context) => this.integrationGit.cleanup(state.root, generation.worktree, generation.worktree, generation.expectedMain, receipts, promoted, kind, context.signal, generation.correction));
|
|
1678
|
-
if (result.outcome !== "ready")
|
|
1679
|
-
throw new Error(result.failure);
|
|
1680
|
-
step.status = "completed";
|
|
1681
|
-
delete step.failure;
|
|
1682
|
-
await this.saveProductive(handle);
|
|
1677
|
+
catch (error) {
|
|
1678
|
+
blocked(generationCleanup, error);
|
|
1683
1679
|
}
|
|
1684
1680
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
?? generation.stages.flatMap((stage) => changesetTaskState(state, stage.taskId).attempts[stage.attempt - 1].cleanup)
|
|
1689
|
-
.find((step) => step.status === "running");
|
|
1690
|
-
if (pending)
|
|
1691
|
-
pending.failure = bounded(`Cleanup requires attention: ${errorText(error)}`);
|
|
1692
|
-
await this.saveProductive(handle);
|
|
1693
|
-
}
|
|
1694
|
-
if (state.tasks.every((task) => task.status === "completed" || task.status === "rejected")
|
|
1681
|
+
// Promotion remains proven; the request stays visible until every promoted resource is released.
|
|
1682
|
+
if (stepLists.every((steps) => steps.every((step) => step.status === "completed"))
|
|
1683
|
+
&& state.tasks.every((task) => task.status === "completed" || task.status === "rejected")
|
|
1695
1684
|
&& state.integration.candidates.every((candidate) => candidate.decision || generation.stages.some((stage) => stage.taskId === candidate.taskId && stage.attempt === candidate.attempt && sameIdentity(stage.source, candidate.tip)))) {
|
|
1696
1685
|
state.final = { status: "passed", identity: promoted, checks: generation.checks,
|
|
1697
1686
|
...(generation.review ? { review: generation.review } : {}) };
|
|
1698
1687
|
state.status = "completed";
|
|
1699
1688
|
state.accepted = true;
|
|
1700
1689
|
state.acceptedAt = this.coordinatorRuntime.now();
|
|
1690
|
+
}
|
|
1691
|
+
await this.saveProductive(handle);
|
|
1692
|
+
}
|
|
1693
|
+
async cleanupPromotedStage(handle, generation, receipts, promoted, stage, task, attempt, scope) {
|
|
1694
|
+
const state = handle.state;
|
|
1695
|
+
const candidate = state.integration.candidates.find((item) => item.taskId === stage.taskId && item.attempt === stage.attempt
|
|
1696
|
+
&& sameIdentity(item.tip, stage.source));
|
|
1697
|
+
const worker = allocationByKind(attempt, "worktree")?.worktree;
|
|
1698
|
+
if (!worker)
|
|
1699
|
+
throw new Error("Promoted worker lacks a proven checkout.");
|
|
1700
|
+
candidate.worker = "release_pending";
|
|
1701
|
+
await this.saveProductive(handle);
|
|
1702
|
+
if (attempt.termination?.status !== "terminated") {
|
|
1703
|
+
if (attempt.termination) {
|
|
1704
|
+
const result = await this.callProductive(handle, scope, (context) => this.hostRuntime.reconcileWorkerTermination({
|
|
1705
|
+
task: changesetTaskRequest(state, task.taskId), attempt,
|
|
1706
|
+
workerId: attempt.termination.workerId, candidate: stage.source,
|
|
1707
|
+
}, context));
|
|
1708
|
+
if (result.outcome !== "terminated")
|
|
1709
|
+
throw new Error("Worker termination cannot be proved; never replay termination.");
|
|
1710
|
+
attempt.termination = { status: "terminated", workerId: attempt.termination.workerId,
|
|
1711
|
+
candidate: stage.source, at: this.coordinatorRuntime.now() };
|
|
1712
|
+
await this.saveProductive(handle);
|
|
1713
|
+
}
|
|
1714
|
+
else if (!await this.terminateWithSafety(handle, task, attempt, stage.source, scope.signal)) {
|
|
1715
|
+
throw new Error(`Worker termination was not proved: ${attempt.termination.failure}`);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
for (const kind of CLEANUP_KINDS) {
|
|
1719
|
+
const step = attempt.cleanup.find((item) => item.kind === kind);
|
|
1720
|
+
if (step.status === "completed")
|
|
1721
|
+
continue;
|
|
1722
|
+
step.status = "running";
|
|
1723
|
+
await this.saveProductive(handle);
|
|
1724
|
+
if (kind === "worker_tab" || kind === "workspace") {
|
|
1725
|
+
const result = await this.callProductive(handle, scope, (context) => this.hostRuntime.cleanupHost({
|
|
1726
|
+
requestId: state.request.id, kind, task: changesetTaskRequest(state, task.taskId), attempt,
|
|
1727
|
+
}, context));
|
|
1728
|
+
if (result.outcome === "blocked")
|
|
1729
|
+
throw new Error(result.failure);
|
|
1730
|
+
}
|
|
1731
|
+
else {
|
|
1732
|
+
const result = await this.callProductive(handle, scope, (context) => this.integrationGit.cleanup(state.root, generation.worktree, worker, generation.expectedMain, receipts, promoted, kind, context.signal, generation.correction));
|
|
1733
|
+
if (result.outcome !== "ready")
|
|
1734
|
+
throw new Error(result.failure);
|
|
1735
|
+
}
|
|
1736
|
+
step.status = "completed";
|
|
1737
|
+
delete step.failure;
|
|
1701
1738
|
await this.saveProductive(handle);
|
|
1702
1739
|
}
|
|
1740
|
+
candidate.worker = "released";
|
|
1741
|
+
// Same-attempt revisions share this exact worker checkout; release historical
|
|
1742
|
+
// rejected tips only after the physical checkout has been proved removed.
|
|
1743
|
+
for (const old of state.integration.candidates.filter((item) => item.decision === "rejected"
|
|
1744
|
+
&& item.taskId === stage.taskId && item.attempt === stage.attempt))
|
|
1745
|
+
old.worker = "released";
|
|
1703
1746
|
}
|
|
1704
1747
|
async requireExactIntegration(handle, generation, receipts, scope, allowDirtyMain = false) {
|
|
1705
1748
|
const root = handle.state.root;
|
|
@@ -2736,7 +2779,6 @@ export class IsolatedRunner {
|
|
|
2736
2779
|
if (result.outcome !== "terminated") {
|
|
2737
2780
|
const failure = boundedRuntimeText(result.failure, "Worker termination failure");
|
|
2738
2781
|
attempt.termination = { status: "unknown", workerId, candidate, failure };
|
|
2739
|
-
this.attention(task, `Worker termination is unproved: ${failure}`);
|
|
2740
2782
|
return false;
|
|
2741
2783
|
}
|
|
2742
2784
|
attempt.termination = { status: "terminated", workerId, candidate, at: this.coordinatorRuntime.now() };
|
|
@@ -2745,7 +2787,6 @@ export class IsolatedRunner {
|
|
|
2745
2787
|
catch (error) {
|
|
2746
2788
|
this.rethrowStopped(error);
|
|
2747
2789
|
attempt.termination = { status: "unknown", workerId, candidate, failure: errorText(error) };
|
|
2748
|
-
this.attention(task, `Worker termination is unproved: ${errorText(error)}`);
|
|
2749
2790
|
return false;
|
|
2750
2791
|
}
|
|
2751
2792
|
finally {
|
|
@@ -2753,17 +2794,19 @@ export class IsolatedRunner {
|
|
|
2753
2794
|
await save();
|
|
2754
2795
|
}
|
|
2755
2796
|
}
|
|
2756
|
-
async runCheckBatch(handle, checks, candidate, phase, scope, taskId, root = handle.state.root) {
|
|
2797
|
+
async runCheckBatch(handle, checks, candidate, phase, scope, taskId, root = handle.state.root, reuse = []) {
|
|
2757
2798
|
const attempt = taskId ? latestAttempt(changesetTaskState(handle.state, taskId)) : undefined;
|
|
2799
|
+
// Run every non-reused check, even none, so the runtime still proves the post-check identity.
|
|
2800
|
+
const run = checks.filter((_, index) => !reuse[index]);
|
|
2758
2801
|
const result = await this.callProductive(handle, scope, async (context) => await this.gitRuntime.runChecks({
|
|
2759
2802
|
root,
|
|
2760
2803
|
scope: phase === "final" ? "final" : "task",
|
|
2761
2804
|
...(taskId ? { taskId, attempt } : {}),
|
|
2762
|
-
checks,
|
|
2805
|
+
checks: run,
|
|
2763
2806
|
candidate,
|
|
2764
2807
|
}, context));
|
|
2765
2808
|
const identityAfter = runtimeIdentity(result.identityAfter, "Post-check identity");
|
|
2766
|
-
if (!exactCommandResults(result.results,
|
|
2809
|
+
if (!exactCommandResults(result.results, run)) {
|
|
2767
2810
|
throw new Error("Check runtime did not return one result with the exact command and argv for every declared check.");
|
|
2768
2811
|
}
|
|
2769
2812
|
if (result.results.some((item) => !Number.isSafeInteger(item.code)
|
|
@@ -2771,23 +2814,29 @@ export class IsolatedRunner {
|
|
|
2771
2814
|
|| typeof item.stdout !== "string" || typeof item.stderr !== "string")) {
|
|
2772
2815
|
throw new Error("Check runtime returned malformed result evidence.");
|
|
2773
2816
|
}
|
|
2774
|
-
|
|
2817
|
+
let ran = 0;
|
|
2818
|
+
const results = checks.map((check, index) => {
|
|
2819
|
+
const reusedFrom = reuse[index];
|
|
2820
|
+
return reusedFrom ? { ...check, code: 0, killed: false, stdout: "", stderr: "" } : result.results[ran++];
|
|
2821
|
+
});
|
|
2822
|
+
const commandsPassed = results.every((item) => item.code === 0 && !item.killed);
|
|
2775
2823
|
const identityMatches = sameIdentity(identityAfter, candidate);
|
|
2776
2824
|
const passed = identityMatches && commandsPassed;
|
|
2777
|
-
let diagnosticIndex = passed ? -1 :
|
|
2825
|
+
let diagnosticIndex = passed ? -1 : results.findIndex((item) => item.code !== 0 || item.killed);
|
|
2778
2826
|
if (diagnosticIndex < 0 && commandsPassed && !identityMatches)
|
|
2779
|
-
diagnosticIndex =
|
|
2827
|
+
diagnosticIndex = results.length - 1;
|
|
2780
2828
|
const evidence = {
|
|
2781
2829
|
phase,
|
|
2782
2830
|
candidate,
|
|
2783
2831
|
identityAfter,
|
|
2784
|
-
results:
|
|
2832
|
+
results: results.map((item, index) => ({
|
|
2785
2833
|
command: item.command,
|
|
2786
2834
|
args: [...item.args],
|
|
2787
2835
|
code: item.code,
|
|
2788
2836
|
killed: item.killed ?? false,
|
|
2789
2837
|
stdout: index === diagnosticIndex ? bounded(item.stdout) : "",
|
|
2790
2838
|
stderr: index === diagnosticIndex ? bounded(item.stderr) : "",
|
|
2839
|
+
...(reuse[index] ? { reusedFrom: reuse[index] } : {}),
|
|
2791
2840
|
})),
|
|
2792
2841
|
passed,
|
|
2793
2842
|
at: this.coordinatorRuntime.now(),
|
|
@@ -3291,6 +3340,7 @@ export class IsolatedRunner {
|
|
|
3291
3340
|
...(state.integration.generations.at(-1).failure ? [`Integration: ${state.integration.generations.at(-1).failure}`] : []),
|
|
3292
3341
|
...(state.integration.generations.at(-1).promotion?.failure ? [`Promotion: ${state.integration.generations.at(-1).promotion.failure}`] : []),
|
|
3293
3342
|
...(state.integration.generations.at(-1).checks?.passed === false ? ["Combined checks failed; Main was not changed."] : []),
|
|
3343
|
+
...reusedCheckLines(state.integration.generations.at(-1).checks),
|
|
3294
3344
|
...(state.integration.generations.at(-1).status === "promoted" ? ["Promotion proven; worker cleanup is pending or completed. Use subagent_integrate cleanup with the exact tip if interrupted."] : []),
|
|
3295
3345
|
] : []),
|
|
3296
3346
|
...(state.integration.generations.some((item) => item.status === "superseded" && item.worktree
|
package/dist/schema.d.ts
CHANGED
|
@@ -10,6 +10,11 @@ export declare const CheckCommandSchema: Type.TObject<{
|
|
|
10
10
|
command: Type.TString;
|
|
11
11
|
args: Type.TArray<Type.TString>;
|
|
12
12
|
}>;
|
|
13
|
+
/** One display form for a check command; quoted args keep argv boundaries visible. */
|
|
14
|
+
export declare function formatCheckCommand({ command, args }: {
|
|
15
|
+
command: string;
|
|
16
|
+
args: readonly string[];
|
|
17
|
+
}): string;
|
|
13
18
|
export declare const JudgmentSchema: Type.TObject<{
|
|
14
19
|
role: Type.TString;
|
|
15
20
|
modelClass: Type.TUnion<[Type.TLiteral<"fast">, Type.TLiteral<"balanced">, Type.TLiteral<"frontier">, Type.TLiteral<"fav">]>;
|
|
@@ -291,6 +296,7 @@ export type HostAllocationPlan = WorkspaceAllocationPlan | WorkerTabAllocationPl
|
|
|
291
296
|
export type PromptRecord = Static<typeof PromptRecordSchema>;
|
|
292
297
|
export type CommandEvidence = Static<typeof CommandEvidenceSchema>;
|
|
293
298
|
export type CheckBatchEvidence = Static<typeof CheckBatchEvidenceSchema>;
|
|
299
|
+
export type ReusedCheckSource = NonNullable<CommandEvidence["reusedFrom"]>;
|
|
294
300
|
export type ReviewEvidence = Static<typeof ReviewEvidenceSchema>;
|
|
295
301
|
export type WorkerTermination = Static<typeof WorkerTerminationSchema>;
|
|
296
302
|
export declare const CLEANUP_KINDS: readonly ["worker_tab", "workspace", "worktree", "branch"];
|
|
@@ -430,6 +436,16 @@ declare const CommandEvidenceSchema: Type.TObject<{
|
|
|
430
436
|
killed: Type.TBoolean;
|
|
431
437
|
stdout: Type.TString;
|
|
432
438
|
stderr: Type.TString;
|
|
439
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
440
|
+
taskId: Type.TString;
|
|
441
|
+
attempt: Type.TInteger;
|
|
442
|
+
candidate: Type.TObject<{
|
|
443
|
+
branch: Type.TString;
|
|
444
|
+
head: Type.TString;
|
|
445
|
+
index: Type.TString;
|
|
446
|
+
tree: Type.TString;
|
|
447
|
+
}>;
|
|
448
|
+
}>>;
|
|
433
449
|
}>;
|
|
434
450
|
declare const CheckBatchEvidenceSchema: Type.TObject<{
|
|
435
451
|
phase: Type.TUnion<[Type.TLiteral<"preliminary">, Type.TLiteral<"final">]>;
|
|
@@ -452,6 +468,16 @@ declare const CheckBatchEvidenceSchema: Type.TObject<{
|
|
|
452
468
|
killed: Type.TBoolean;
|
|
453
469
|
stdout: Type.TString;
|
|
454
470
|
stderr: Type.TString;
|
|
471
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
472
|
+
taskId: Type.TString;
|
|
473
|
+
attempt: Type.TInteger;
|
|
474
|
+
candidate: Type.TObject<{
|
|
475
|
+
branch: Type.TString;
|
|
476
|
+
head: Type.TString;
|
|
477
|
+
index: Type.TString;
|
|
478
|
+
tree: Type.TString;
|
|
479
|
+
}>;
|
|
480
|
+
}>>;
|
|
455
481
|
}>>;
|
|
456
482
|
passed: Type.TBoolean;
|
|
457
483
|
at: Type.TInteger;
|
|
@@ -619,6 +645,16 @@ declare const TaskAttemptSchema: Type.TObject<{
|
|
|
619
645
|
killed: Type.TBoolean;
|
|
620
646
|
stdout: Type.TString;
|
|
621
647
|
stderr: Type.TString;
|
|
648
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
649
|
+
taskId: Type.TString;
|
|
650
|
+
attempt: Type.TInteger;
|
|
651
|
+
candidate: Type.TObject<{
|
|
652
|
+
branch: Type.TString;
|
|
653
|
+
head: Type.TString;
|
|
654
|
+
index: Type.TString;
|
|
655
|
+
tree: Type.TString;
|
|
656
|
+
}>;
|
|
657
|
+
}>>;
|
|
622
658
|
}>>;
|
|
623
659
|
passed: Type.TBoolean;
|
|
624
660
|
at: Type.TInteger;
|
|
@@ -807,6 +843,16 @@ declare const ChangesetTaskStateSchema: Type.TObject<{
|
|
|
807
843
|
killed: Type.TBoolean;
|
|
808
844
|
stdout: Type.TString;
|
|
809
845
|
stderr: Type.TString;
|
|
846
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
847
|
+
taskId: Type.TString;
|
|
848
|
+
attempt: Type.TInteger;
|
|
849
|
+
candidate: Type.TObject<{
|
|
850
|
+
branch: Type.TString;
|
|
851
|
+
head: Type.TString;
|
|
852
|
+
index: Type.TString;
|
|
853
|
+
tree: Type.TString;
|
|
854
|
+
}>;
|
|
855
|
+
}>>;
|
|
810
856
|
}>>;
|
|
811
857
|
passed: Type.TBoolean;
|
|
812
858
|
at: Type.TInteger;
|
|
@@ -1086,6 +1132,16 @@ declare const TaskStateSchema: Type.TUnion<[Type.TObject<{
|
|
|
1086
1132
|
killed: Type.TBoolean;
|
|
1087
1133
|
stdout: Type.TString;
|
|
1088
1134
|
stderr: Type.TString;
|
|
1135
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
1136
|
+
taskId: Type.TString;
|
|
1137
|
+
attempt: Type.TInteger;
|
|
1138
|
+
candidate: Type.TObject<{
|
|
1139
|
+
branch: Type.TString;
|
|
1140
|
+
head: Type.TString;
|
|
1141
|
+
index: Type.TString;
|
|
1142
|
+
tree: Type.TString;
|
|
1143
|
+
}>;
|
|
1144
|
+
}>>;
|
|
1089
1145
|
}>>;
|
|
1090
1146
|
passed: Type.TBoolean;
|
|
1091
1147
|
at: Type.TInteger;
|
|
@@ -1206,6 +1262,16 @@ declare const IntegrationCandidateSchema: Type.TObject<{
|
|
|
1206
1262
|
killed: Type.TBoolean;
|
|
1207
1263
|
stdout: Type.TString;
|
|
1208
1264
|
stderr: Type.TString;
|
|
1265
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
1266
|
+
taskId: Type.TString;
|
|
1267
|
+
attempt: Type.TInteger;
|
|
1268
|
+
candidate: Type.TObject<{
|
|
1269
|
+
branch: Type.TString;
|
|
1270
|
+
head: Type.TString;
|
|
1271
|
+
index: Type.TString;
|
|
1272
|
+
tree: Type.TString;
|
|
1273
|
+
}>;
|
|
1274
|
+
}>>;
|
|
1209
1275
|
}>>;
|
|
1210
1276
|
passed: Type.TBoolean;
|
|
1211
1277
|
at: Type.TInteger;
|
|
@@ -1350,6 +1416,16 @@ declare const IntegrationGenerationSchema: Type.TObject<{
|
|
|
1350
1416
|
killed: Type.TBoolean;
|
|
1351
1417
|
stdout: Type.TString;
|
|
1352
1418
|
stderr: Type.TString;
|
|
1419
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
1420
|
+
taskId: Type.TString;
|
|
1421
|
+
attempt: Type.TInteger;
|
|
1422
|
+
candidate: Type.TObject<{
|
|
1423
|
+
branch: Type.TString;
|
|
1424
|
+
head: Type.TString;
|
|
1425
|
+
index: Type.TString;
|
|
1426
|
+
tree: Type.TString;
|
|
1427
|
+
}>;
|
|
1428
|
+
}>>;
|
|
1353
1429
|
}>>;
|
|
1354
1430
|
passed: Type.TBoolean;
|
|
1355
1431
|
at: Type.TInteger;
|
|
@@ -1470,6 +1546,16 @@ export declare const IntegrationStateSchema: Type.TObject<{
|
|
|
1470
1546
|
killed: Type.TBoolean;
|
|
1471
1547
|
stdout: Type.TString;
|
|
1472
1548
|
stderr: Type.TString;
|
|
1549
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
1550
|
+
taskId: Type.TString;
|
|
1551
|
+
attempt: Type.TInteger;
|
|
1552
|
+
candidate: Type.TObject<{
|
|
1553
|
+
branch: Type.TString;
|
|
1554
|
+
head: Type.TString;
|
|
1555
|
+
index: Type.TString;
|
|
1556
|
+
tree: Type.TString;
|
|
1557
|
+
}>;
|
|
1558
|
+
}>>;
|
|
1473
1559
|
}>>;
|
|
1474
1560
|
passed: Type.TBoolean;
|
|
1475
1561
|
at: Type.TInteger;
|
|
@@ -1590,6 +1676,16 @@ export declare const IntegrationStateSchema: Type.TObject<{
|
|
|
1590
1676
|
killed: Type.TBoolean;
|
|
1591
1677
|
stdout: Type.TString;
|
|
1592
1678
|
stderr: Type.TString;
|
|
1679
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
1680
|
+
taskId: Type.TString;
|
|
1681
|
+
attempt: Type.TInteger;
|
|
1682
|
+
candidate: Type.TObject<{
|
|
1683
|
+
branch: Type.TString;
|
|
1684
|
+
head: Type.TString;
|
|
1685
|
+
index: Type.TString;
|
|
1686
|
+
tree: Type.TString;
|
|
1687
|
+
}>;
|
|
1688
|
+
}>>;
|
|
1593
1689
|
}>>;
|
|
1594
1690
|
passed: Type.TBoolean;
|
|
1595
1691
|
at: Type.TInteger;
|
|
@@ -1918,6 +2014,16 @@ declare const RunStateSchema: Type.TObject<{
|
|
|
1918
2014
|
killed: Type.TBoolean;
|
|
1919
2015
|
stdout: Type.TString;
|
|
1920
2016
|
stderr: Type.TString;
|
|
2017
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
2018
|
+
taskId: Type.TString;
|
|
2019
|
+
attempt: Type.TInteger;
|
|
2020
|
+
candidate: Type.TObject<{
|
|
2021
|
+
branch: Type.TString;
|
|
2022
|
+
head: Type.TString;
|
|
2023
|
+
index: Type.TString;
|
|
2024
|
+
tree: Type.TString;
|
|
2025
|
+
}>;
|
|
2026
|
+
}>>;
|
|
1921
2027
|
}>>;
|
|
1922
2028
|
passed: Type.TBoolean;
|
|
1923
2029
|
at: Type.TInteger;
|
|
@@ -2031,6 +2137,16 @@ declare const RunStateSchema: Type.TObject<{
|
|
|
2031
2137
|
killed: Type.TBoolean;
|
|
2032
2138
|
stdout: Type.TString;
|
|
2033
2139
|
stderr: Type.TString;
|
|
2140
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
2141
|
+
taskId: Type.TString;
|
|
2142
|
+
attempt: Type.TInteger;
|
|
2143
|
+
candidate: Type.TObject<{
|
|
2144
|
+
branch: Type.TString;
|
|
2145
|
+
head: Type.TString;
|
|
2146
|
+
index: Type.TString;
|
|
2147
|
+
tree: Type.TString;
|
|
2148
|
+
}>;
|
|
2149
|
+
}>>;
|
|
2034
2150
|
}>>;
|
|
2035
2151
|
passed: Type.TBoolean;
|
|
2036
2152
|
at: Type.TInteger;
|
|
@@ -2151,6 +2267,16 @@ declare const RunStateSchema: Type.TObject<{
|
|
|
2151
2267
|
killed: Type.TBoolean;
|
|
2152
2268
|
stdout: Type.TString;
|
|
2153
2269
|
stderr: Type.TString;
|
|
2270
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
2271
|
+
taskId: Type.TString;
|
|
2272
|
+
attempt: Type.TInteger;
|
|
2273
|
+
candidate: Type.TObject<{
|
|
2274
|
+
branch: Type.TString;
|
|
2275
|
+
head: Type.TString;
|
|
2276
|
+
index: Type.TString;
|
|
2277
|
+
tree: Type.TString;
|
|
2278
|
+
}>;
|
|
2279
|
+
}>>;
|
|
2154
2280
|
}>>;
|
|
2155
2281
|
passed: Type.TBoolean;
|
|
2156
2282
|
at: Type.TInteger;
|
|
@@ -2281,6 +2407,16 @@ declare const RunStateSchema: Type.TObject<{
|
|
|
2281
2407
|
killed: Type.TBoolean;
|
|
2282
2408
|
stdout: Type.TString;
|
|
2283
2409
|
stderr: Type.TString;
|
|
2410
|
+
reusedFrom: Type.TOptional<Type.TObject<{
|
|
2411
|
+
taskId: Type.TString;
|
|
2412
|
+
attempt: Type.TInteger;
|
|
2413
|
+
candidate: Type.TObject<{
|
|
2414
|
+
branch: Type.TString;
|
|
2415
|
+
head: Type.TString;
|
|
2416
|
+
index: Type.TString;
|
|
2417
|
+
tree: Type.TString;
|
|
2418
|
+
}>;
|
|
2419
|
+
}>>;
|
|
2284
2420
|
}>>;
|
|
2285
2421
|
passed: Type.TBoolean;
|
|
2286
2422
|
at: Type.TInteger;
|
|
@@ -2445,11 +2581,16 @@ export declare function parseIdOnly(value: unknown): IdOnly;
|
|
|
2445
2581
|
export declare function parseResumeRequest(value: unknown): ResumeRequest;
|
|
2446
2582
|
export declare function sameIdentity(left: WorkspaceIdentity, right: WorkspaceIdentity): boolean;
|
|
2447
2583
|
export declare function isCleanCommitted(identity: WorkspaceIdentity): boolean;
|
|
2448
|
-
|
|
2584
|
+
/** Reused results pass only when they still cite one of `sources` exactly; no sources accept no reuse. */
|
|
2585
|
+
export declare function checkBatchPasses(evidence: CheckBatchEvidence | undefined, checks: readonly CheckCommand[], candidate: WorkspaceIdentity, sources?: readonly IntegrationCandidate[]): boolean;
|
|
2586
|
+
/** Candidates exactly staged in this generation; only they can supply reused final results. */
|
|
2587
|
+
export declare function stagedCheckSources(generation: IntegrationGeneration, candidates: readonly IntegrationCandidate[]): IntegrationCandidate[];
|
|
2588
|
+
/** A passed preliminary result stands in for a final check only on the same tree with identical command and argv. */
|
|
2589
|
+
export declare function reusableCheckSource(check: CheckCommand, tip: WorkspaceIdentity, sources: readonly IntegrationCandidate[]): ReusedCheckSource | undefined;
|
|
2449
2590
|
export declare function reviewEvidencePasses(evidence: ReviewEvidence | undefined, phase: ReviewEvidence["phase"], criterion: string, base: WorkspaceIdentity, tip: WorkspaceIdentity): boolean;
|
|
2450
2591
|
/** Check the persisted integration projection against the request before recovery or promotion. */
|
|
2451
2592
|
export declare function parseIntegrationState(value: unknown, request: ExecuteRequest): IntegrationState;
|
|
2452
2593
|
/** Passing final evidence is bound to the exact assembled identity, not any worker tip. */
|
|
2453
|
-
export declare function integrationGenerationPasses(generation: IntegrationGeneration, request: ExecuteRequest): boolean;
|
|
2594
|
+
export declare function integrationGenerationPasses(generation: IntegrationGeneration, request: ExecuteRequest, candidates: readonly IntegrationCandidate[]): boolean;
|
|
2454
2595
|
export declare function parseRunState(value: unknown): RunState;
|
|
2455
2596
|
export {};
|