@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/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
- async execute(value, cwd, outerSignal) {
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 checks = await this.runCheckBatch(handle, request.finalChecks, action.expectedTip, "final", scope, undefined, generation.worktree.path);
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
- const main = await this.callProductive(handle, scope, (context) => this.gitRuntime.inspectMain({ root: state.root }, context));
1611
- if (!sameIdentity(main, promoted))
1612
- throw new Error("Main changed or became dirty after promotion; cleanup is blocked.");
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
- const candidate = state.integration.candidates.find((item) => item.taskId === stage.taskId && item.attempt === stage.attempt
1617
- && sameIdentity(item.tip, stage.source));
1618
- const worker = allocationByKind(attempt, "worktree")?.worktree;
1619
- if (!worker)
1620
- throw new Error("Promoted worker lacks a proven checkout.");
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
- for (const kind of CLEANUP_KINDS) {
1640
- const step = attempt.cleanup.find((item) => item.kind === kind);
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
- if (kind === "worker_tab" || kind === "workspace") {
1646
- const result = await this.callProductive(handle, scope, (context) => this.hostRuntime.cleanupHost({
1647
- requestId: state.request.id, kind, task: changesetTaskRequest(state, task.taskId), attempt,
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
- generation.cleanup ??= [{ kind: "worktree", status: "pending" }, { kind: "branch", status: "pending" }];
1670
- await this.saveProductive(handle);
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
- catch (error) {
1686
- generation.failure = undefined; // Promotion remains proven; cleanup evidence stays inspectable.
1687
- const pending = generation.cleanup?.find((step) => step.status === "running")
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, checks)) {
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
- const commandsPassed = result.results.every((item) => item.code === 0 && !item.killed);
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 : result.results.findIndex((item) => item.code !== 0 || item.killed);
2825
+ let diagnosticIndex = passed ? -1 : results.findIndex((item) => item.code !== 0 || item.killed);
2778
2826
  if (diagnosticIndex < 0 && commandsPassed && !identityMatches)
2779
- diagnosticIndex = result.results.length - 1;
2827
+ diagnosticIndex = results.length - 1;
2780
2828
  const evidence = {
2781
2829
  phase,
2782
2830
  candidate,
2783
2831
  identityAfter,
2784
- results: result.results.map((item, index) => ({
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
- export declare function checkBatchPasses(evidence: CheckBatchEvidence | undefined, checks: readonly CheckCommand[], candidate: WorkspaceIdentity): boolean;
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 {};