@henryqw/pi-subagent 27.5.4 → 27.6.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/README.md CHANGED
@@ -33,7 +33,7 @@ Commands and UI are for you. Tools and the packaged skill are for Main, except `
33
33
  | `/subagent roles` | command | List Role sources, resources, resolved model and thinking level, route errors, and file errors. Read-only; requires no Herdr pane. |
34
34
  | `/subagent prune` | command | Review remaining worker and integration branches and worktrees. Remove merged resources after confirmation. Requires a running Herdr server; without interactive UI, only reports. |
35
35
  | `/subagent recover` | command | Classify interrupted isolated work in the canonical repository. Send Main request IDs, blockers, and next actions. Requires no UI or Herdr pane. |
36
- | `delegate_task` | tool | Start authorized shared-checkout work or a durable isolated checked graph. |
36
+ | `delegate_task` | tool | Start authorized shared-checkout work or a durable isolated checked graph. With `dryRun: true`, check the request and show the resolved plan without a launch. |
37
37
  | `subagent_status` | tool | Read saved isolated state, exact identities, failed review verdicts, and allowed continuation. Does not replay work. Supports structured output for `codemode`. |
38
38
  | `subagent_resume` | tool | Perform the reported `retry`, `verify`, or `finalize` continuation, not staging or integration. Same-worker correction retries accept optional `instruction` text. |
39
39
  | `subagent_stage` | tool | Stage, resolve, reject, or revise an exact candidate. Rejecting a staged candidate freezes its generation. |
@@ -41,7 +41,7 @@ Commands and UI are for you. Tools and the packaged skill are for Main, except `
41
41
  | `subagent_abort` | tool | Terminate an isolated request without discarding retained work. Retained candidates and integration work require release or proof of absence. |
42
42
  | `git_read` | tool | Let Role-declared children inspect bounded local Git evidence without Bash, tests, network access, or writes. Not available to Main. |
43
43
  | `pi-subagent` | skill | Guide Main through authorization, delegation, checks, integration, and recovery. |
44
- | Direct status widget (`D`) | ui | Show direct references, names, Role badges, status, routes, elapsed time, measured task tokens, and tool activity in the TUI. |
44
+ | Direct status widget (`D`) | ui | Show direct references, names, Role badges, status, routes, elapsed time, measured task tokens, cost, and tool activity in the TUI. |
45
45
  | Isolated status widget (`I`) | ui | Show recorded task state, Role/model-class badges, and retained workspaces in the TUI. Readiness does not mean promotion. |
46
46
 
47
47
  `delegate_task`, `subagent_resume`, `subagent_stage`, `subagent_integrate`, and `subagent_abort` are model-only. Main must make these decisions visibly; `codemode` scripts can call only `subagent_status`.
@@ -82,7 +82,11 @@ Tasks in write requests must return one JSON completion object:
82
82
 
83
83
  `outcome` accepts `succeeded`, `failed`, or `blocked`; `answer` must be non-empty. Plain text, malformed completion, blocked work, or failure stops later dispatch. Success is the worker's report, not independent validation. Checkout admission depends on proved termination, not the reported outcome.
84
84
 
85
- The launch returns a task ID and first Herdr tab, not the answer. One follow-up delivers results when the workflow finishes. Busy Main receives it after its current turn; idle Main starts a turn. Failed partial output stays marked as partial, never success. Worker output carries no user authority.
85
+ A write task can declare `"paths": ["src/**"]` with repo-relative globs. For each task in a write request, the result lists new commits and changed paths, and it flags changed paths outside `paths`. This is a report only, not enforcement, and it cannot exclude changes from other processes. For a read request, the result warns `checkout changed during task; result may be stale` when the checkout changed while the task ran. Outside a Git repository, the extension makes no report. See [Checkout change reports](./docs/orchestration.md#checkout-change-reports).
86
+
87
+ To check a request before a launch, add `"dryRun": true`. The dry run does the launch checks and shows each task's Role, model, thinking level, access, and dependencies. It starts no worker and opens no tab. It also reserves no checkout and sends no follow-up. A failed check gives the same error as a launch. For the full list of checks, see [Dry run](./docs/orchestration.md#dry-run).
88
+
89
+ The launch returns a task ID and first Herdr tab, not the answer. One follow-up delivers results when the workflow finishes. The results show the tokens and cost of each worker and the total cost of the request. Busy Main receives it after its current turn; idle Main starts a turn. Failed partial output stays marked as partial, never success. Worker output carries no user authority.
86
90
 
87
91
  To continue a finished worker, add `"continueFrom": "D7.1"` with the same `role`. This opens a new tab with the saved conversation and a new reference. Continue the newest reference from the original checkout and workspace. For eligibility and stop behavior, see [Continue or stop one direct worker](./docs/orchestration.md#continue-or-stop-one-direct-worker).
88
92
 
@@ -125,13 +129,17 @@ Keep the durable ID; Main can work or end its turn without polling. Inspect read
125
129
 
126
130
  Readiness requires preliminary checks and any task `judgment`; it does not permit promotion. Checks and judgments bind to exact Git identities. Staging, combined validation, and promotion require unchanged, clean Main at the pinned identity. Changed identities, conflicts, or uncertain outcomes block promotion and preserve evidence. A definitive combined check or review failure permits one committed integration correction, recorded with `subagent_integrate correct`, and another validation.
127
131
 
132
+ Combined validation can reuse a passed preliminary check of a staged candidate. The candidate must have the same tree as the combined tip, and the check must have the identical command and argv. Then the final check does not run again. This usually occurs when you stage one candidate onto unchanged Main. Status shows each reused result and its candidate. Final judgment still runs.
133
+
134
+ Reuse assumes that checks use only tracked content. Ignored files, for example `node_modules`, can be different between worktrees.
135
+
128
136
  The extension sends follow-ups when a candidate becomes ready, a worker needs attention, and the wave finishes. A missed follow-up does not authorize replay. Read [Isolated contract](./docs/orchestration.md#isolated-contract) and [Exact integration actions](./docs/orchestration.md#exact-integration-actions) for graph, judgment, revision, refresh, and release details.
129
137
 
130
138
  ### Inspect work
131
139
 
132
- Open `/subagent` to browse direct work on this session branch and isolated requests in the current canonical Git checkout, including other sessions. **Refresh** reloads saved status without recovery or mutation. Invalid state IDs stay preserved with a warning. Direct recovery remains available outside Git or without isolated inventory.
140
+ Open `/subagent` to browse direct work on this session branch and isolated requests in the current canonical Git checkout, including other sessions. The menu groups rows under **Direct**, **Isolated**, and **Orphaned** headers. Each isolated row shows the request ID, the status, and a short goal summary. **Refresh** or a header reloads saved status without recovery or mutation. Invalid state IDs stay preserved with a warning. Direct recovery remains available outside Git or without isolated inventory.
133
141
 
134
- Requests whose parent checkout was removed show as **Isolated · orphaned** from any checkout of the same repository. Every isolated request, including an orphaned request, has **Close and delete worktrees (discard work)**. Select it to review owned resources and confirm deletion. **This permanently deletes committed, uncommitted, and ignored content in the listed checkouts.** Cancel makes no change.
142
+ Requests whose parent checkout was removed show in the **Orphaned · parent checkout removed** group from any checkout of the same repository. Every isolated request, including an orphaned request, has **Close and delete worktrees (discard work)**. Select it to review owned resources and confirm deletion. **This permanently deletes committed, uncommitted, and ignored content in the listed checkouts.** Cancel makes no change.
135
143
 
136
144
  Only shown resources can be removed; live jobs, uncertain ownership, or other Herdr panes can block removal. Agents and tools cannot trigger this action. See [Close and delete isolated worktrees](./docs/orchestration.md#close-and-delete-isolated-worktrees) for guards and partial cleanup.
137
145
 
@@ -8,10 +8,10 @@ export interface DirectHandle {
8
8
  sessionFile: string;
9
9
  prompt: string;
10
10
  answer(maxBytes: number): Promise<string>;
11
- usageTokens(): Promise<number | undefined>;
11
+ usage(): Promise<DirectUsage>;
12
12
  /** Bounded assistant text from this turn so far; failure evidence only, never completion. */
13
13
  partialText(maxChars: number): Promise<string | undefined>;
14
- /** Display-only activity from the last usageTokens() sample. */
14
+ /** Display-only activity from the last usage() sample. */
15
15
  readonly activity?: DirectToolActivity;
16
16
  cancel(): Promise<void>;
17
17
  }
@@ -19,6 +19,12 @@ export declare function exactDirectTerminalTurn(jsonl: string, prompt: string):
19
19
  /** Refuse continuation unless the saved session's exact recorded turn is terminal and no later user turn exists. */
20
20
  export declare function assertSettledDirectSession(sessionFile: string, turn: string): Promise<void>;
21
21
  export declare function exactDirectAnswer(jsonl: string, prompt: string, maxBytes?: number, requireSuccess?: boolean): string;
22
+ /** Measured from one session read; each field is absent when no turn record carried it. */
23
+ export interface DirectUsage {
24
+ tokens?: number;
25
+ /** Sum of assistant `usage.cost.total`. */
26
+ cost?: number;
27
+ }
22
28
  export interface DirectToolActivity {
23
29
  pending: string[];
24
30
  latest?: {
@@ -26,14 +32,13 @@ export interface DirectToolActivity {
26
32
  durationMs?: number;
27
33
  };
28
34
  }
29
- export declare function directSessionUsage(jsonl: string, prompt: string): {
30
- tokens?: number;
35
+ export declare function directSessionUsage(jsonl: string, prompt: string): DirectUsage & {
31
36
  activity?: DirectToolActivity;
32
37
  };
33
38
  /** The last `maxChars` of assistant text in the exact prompt's turn; ignores a partial trailing entry. */
34
39
  export declare function directPartialText(jsonl: string, prompt: string, maxChars: number): string | undefined;
35
40
  /** One reader per handle; only complete, valid lines advance the byte offset. */
36
- export declare function createDirectSessionReader(sessionFile: string, prompt: string): Pick<DirectHandle, "usageTokens" | "partialText" | "activity">;
41
+ export declare function createDirectSessionReader(sessionFile: string, prompt: string): Pick<DirectHandle, "usage" | "partialText" | "activity">;
37
42
  /** `turn` is the prompt's random turn identity; it proves which session turn a record owns.
38
43
  * `workspaceId` and `cwd` bind the record to its launching Herdr workspace and checkout. */
39
44
  export type DirectTab = Pick<DirectHandle, "name" | "tabId" | "paneId" | "sessionFile"> & {
@@ -126,6 +126,7 @@ class DirectSessionState {
126
126
  active = false;
127
127
  total = 0;
128
128
  observed = false;
129
+ cost;
129
130
  pending = new Map();
130
131
  latest;
131
132
  texts = [];
@@ -150,6 +151,7 @@ class DirectSessionState {
150
151
  if (this.active) {
151
152
  this.total = 0;
152
153
  this.observed = false;
154
+ this.cost = undefined;
153
155
  this.pending.clear();
154
156
  this.latest = undefined;
155
157
  this.texts = [];
@@ -163,6 +165,9 @@ class DirectSessionState {
163
165
  this.total += values.reduce((sum, value) => sum + value, 0);
164
166
  this.observed = true;
165
167
  }
168
+ const cost = usage.cost?.total;
169
+ if (typeof cost === "number" && Number.isFinite(cost) && cost >= 0)
170
+ this.cost = (this.cost ?? 0) + cost;
166
171
  }
167
172
  if (Array.isArray(message.content))
168
173
  for (const part of message.content) {
@@ -187,7 +192,7 @@ class DirectSessionState {
187
192
  return true;
188
193
  }
189
194
  usage() {
190
- return { tokens: this.observed ? this.total : undefined,
195
+ return { tokens: this.observed ? this.total : undefined, cost: this.cost,
191
196
  activity: this.pending.size || this.latest ? { pending: [...this.pending.values()], latest: this.latest } : undefined };
192
197
  }
193
198
  partialText(maxChars) {
@@ -258,9 +263,9 @@ export function createDirectSessionReader(sessionFile, prompt) {
258
263
  const sample = () => reading ??= read().finally(() => { reading = undefined; });
259
264
  return {
260
265
  get activity() { return lastUsage.activity; },
261
- async usageTokens() {
266
+ async usage() {
262
267
  lastUsage = await sample() ? state.usage() : {};
263
- return lastUsage.tokens;
268
+ return { tokens: lastUsage.tokens, cost: lastUsage.cost };
264
269
  },
265
270
  async partialText(maxChars) {
266
271
  return await sample() ? state.partialText(maxChars) : undefined;
@@ -426,7 +431,7 @@ export function createDirectHerdr(pi, cwd, idleMs) {
426
431
  return {
427
432
  name, tabId, paneId, sessionFile, prompt,
428
433
  get activity() { return session.activity; },
429
- usageTokens: session.usageTokens,
434
+ usage: session.usage,
430
435
  partialText: session.partialText,
431
436
  async answer(maxBytes) {
432
437
  const readAnswer = async () => {
@@ -1,5 +1,5 @@
1
1
  import { type WorktreeInfo } from "./worktree.ts";
2
- import { type CheckBatchEvidence, type CheckCommand, type ReviewEvidence, type WorkspaceIdentity } from "./schema.ts";
2
+ import { type CheckBatchEvidence, type CheckCommand, type IntegrationCandidate, type ReviewEvidence, type WorkspaceIdentity } from "./schema.ts";
3
3
  export type StageReceipt = {
4
4
  previous: WorkspaceIdentity;
5
5
  worker: WorkspaceIdentity;
@@ -42,6 +42,8 @@ export declare class IntegrationGit {
42
42
  stages: readonly StageReceipt[];
43
43
  checks: CheckBatchEvidence;
44
44
  commands: readonly CheckCommand[];
45
+ /** Staged candidates that may supply reused final results; omitted accepts no reuse. */
46
+ sources?: readonly IntegrationCandidate[];
45
47
  review?: ReviewEvidence;
46
48
  criterion?: string;
47
49
  correction?: {
@@ -317,7 +317,8 @@ export class IntegrationGit {
317
317
  return { outcome: "blocked", failure: "No worker stages to promote." };
318
318
  const tip = input.correction ? (await this.inspectCorrection(root, integration, base, stages, input.correction.from, input.correction.to, signal), input.correction.to)
319
319
  : await this.inspectCombined(root, integration, base, stages, signal);
320
- if (input.checks.phase !== "final" || !checkBatchPasses(input.checks, input.commands, tip)
320
+ const sources = (input.sources ?? []).filter((source) => stages.some((stage) => sameIdentity(stage.worker, source.tip)));
321
+ if (input.checks.phase !== "final" || !checkBatchPasses(input.checks, input.commands, tip, sources)
321
322
  || (input.criterion !== undefined && !reviewEvidencePasses(input.review, "final", input.criterion, base, tip))
322
323
  || (input.criterion === undefined && input.review !== undefined)) {
323
324
  return { outcome: "blocked", failure: "Exact combined-tip final checks/review evidence is missing or failed." };
@@ -1,5 +1,5 @@
1
1
  import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { CoordinatorRuntime, OperationContext, TransientLaunchHandle, VerifiedLaunch } from "./runner.ts";
2
+ import type { CoordinatorRuntime, OperationContext, ResolvedRoute, TransientLaunchHandle, VerifiedLaunch } from "./runner.ts";
3
3
  import { type ExecuteRequest, type ModelClass, type WorkspaceIdentity } from "./schema.ts";
4
4
  export declare const ISOLATED_MODEL_TASK: {
5
5
  readonly id: "pi-subagent/isolatedRoleLaunch";
@@ -43,9 +43,11 @@ export declare class RoleLaunchRuntime implements CoordinatorRuntime {
43
43
  preflight(input: {
44
44
  request: ExecuteRequest;
45
45
  cwd: string;
46
+ dryRun?: boolean;
46
47
  }, context: OperationContext): Promise<{
47
48
  root: string;
48
49
  main: WorkspaceIdentity;
50
+ routes: ResolvedRoute[];
49
51
  }>;
50
52
  acquireLaunch(role: string, modelClass: ModelClass, context: OperationContext): Promise<TransientLaunchHandle<VerifiedLaunch>>;
51
53
  }
@@ -150,7 +150,7 @@ export class RoleLaunchRuntime {
150
150
  if (!rootInfo.isDirectory())
151
151
  throw new Error("Pi Subagent root must be an existing local directory.");
152
152
  context.signal?.throwIfAborted();
153
- if (input.request.tasks.some((task) => task.kind === "changeset")) {
153
+ if (!input.dryRun && input.request.tasks.some((task) => task.kind === "changeset")) {
154
154
  await this.options.preflightHost?.({ request: input.request, cwd: input.cwd, root }, context);
155
155
  }
156
156
  const main = await this.options.inspectMainBase({ root }, context);
@@ -168,11 +168,14 @@ export class RoleLaunchRuntime {
168
168
  if (input.request.finalJudgment) {
169
169
  addRequired(input.request.finalJudgment.role, input.request.finalJudgment.modelClass);
170
170
  }
171
+ const routes = [];
171
172
  for (const [role, modelClasses] of required) {
172
- for (const modelClass of modelClasses)
173
- await this.prepareLaunch(role, modelClass, context);
173
+ for (const modelClass of modelClasses) {
174
+ const { launch } = await this.prepareLaunch(role, modelClass, context);
175
+ routes.push({ role, modelClass, model: launch.model, thinkingLevel: launch.thinkingLevel });
176
+ }
174
177
  }
175
- return { root, main };
178
+ return { root, main, routes };
176
179
  }
177
180
  async acquireLaunch(role, modelClass, context) {
178
181
  const prepared = await this.prepareLaunch(role, modelClass, context);
package/dist/runner.d.ts CHANGED
@@ -96,6 +96,14 @@ export interface VerifiedLaunch {
96
96
  readonly env: Readonly<Record<string, string>>;
97
97
  readonly tools: readonly string[];
98
98
  }
99
+ export type ResolvedRoute = Pick<VerifiedLaunch, "role" | "modelClass" | "model" | "thinkingLevel">;
100
+ /** A dry-run result: the normalized request and the resolved routes; no state exists for it. */
101
+ export interface ExecutePlan {
102
+ request: ExecuteRequest;
103
+ root: string;
104
+ main: WorkspaceIdentity;
105
+ routes: readonly ResolvedRoute[];
106
+ }
99
107
  export interface TransientLaunchHandle<Launch extends VerifiedLaunch = VerifiedLaunch> {
100
108
  readonly launch: Launch;
101
109
  cleanup(): Promise<void>;
@@ -109,12 +117,16 @@ export declare function withTransientLaunch<Launch extends VerifiedLaunch, Resul
109
117
  export interface CoordinatorRuntime {
110
118
  now(): number;
111
119
  randomToken(): string;
120
+ /** `dryRun` skips checks that call Herdr; every other check runs. */
112
121
  preflight(input: {
113
122
  request: ExecuteRequest;
114
123
  cwd: string;
124
+ dryRun?: boolean;
115
125
  }, context: OperationContext): Promise<{
116
126
  root: string;
117
127
  main: WorkspaceIdentity;
128
+ /** Resolved route of each distinct Role and model class, when the coordinator reports it. */
129
+ routes?: readonly ResolvedRoute[];
118
130
  }>;
119
131
  acquireLaunch(role: string, modelClass: ModelClass, context: OperationContext): Promise<TransientLaunchHandle<VerifiedLaunch>>;
120
132
  }
@@ -340,6 +352,10 @@ export declare class IsolatedRunner {
340
352
  private saveProductive;
341
353
  private callProductive;
342
354
  private rethrowStopped;
355
+ /** Every execute check before durable state; a dry run also skips the Herdr host preflight. */
356
+ private admitExecute;
357
+ /** Dry run of execute: no state, lock, lease, worktree, or Herdr call. */
358
+ plan(value: unknown, cwd: string, outerSignal?: AbortSignal): Promise<ExecutePlan>;
343
359
  execute(value: unknown, cwd: string, outerSignal?: AbortSignal): Promise<RunResponse>;
344
360
  resume(value: unknown, root: string, outerSignal?: AbortSignal): Promise<RunResponse>;
345
361
  /** Main selects an exact retained candidate; only the owned integration checkout is writable. */
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
  }
@@ -802,12 +807,12 @@ export class IsolatedRunner {
802
807
  if (error instanceof DurableRunStopped)
803
808
  throw error;
804
809
  }
805
- 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) {
806
812
  const request = parseExecuteRequest(value);
807
813
  const policy = Object.freeze({ ...this.currentPolicy() });
808
- const scope = new ProductiveScope(outerSignal);
809
814
  const canonicalCwd = realpathSync.native(cwd);
810
- 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));
811
816
  const root = realpathSync.native(prepared.root);
812
817
  if (root !== prepared.root)
813
818
  throw new Error("Preflight repository root must be canonical.");
@@ -824,6 +829,16 @@ export class IsolatedRunner {
824
829
  if (this.hasAnyActiveControl(root)) {
825
830
  throw new Error("Another Pi Subagent request has active changeset work in this repository.");
826
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);
827
842
  return await this.withProductiveRun(root, async (lifecycle) => {
828
843
  const createdAt = this.coordinatorRuntime.now();
829
844
  const state = {
@@ -1453,10 +1468,11 @@ export class IsolatedRunner {
1453
1468
  throw new Error("Combined tip changed the admitted canonical root test script.");
1454
1469
  }
1455
1470
  await this.requireExactIntegration(handle, generation, receipts, scope);
1456
- 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)));
1457
1473
  generation.checks = checks;
1458
1474
  await this.saveProductive(handle);
1459
- if (!checkBatchPasses(checks, request.finalChecks, action.expectedTip))
1475
+ if (!checkBatchPasses(checks, request.finalChecks, action.expectedTip, sources))
1460
1476
  throw new Error("Combined full-suite check failed or changed the integration tip.");
1461
1477
  await this.requireExactIntegration(handle, generation, receipts, scope);
1462
1478
  if (request.finalJudgment) {
@@ -1468,12 +1484,12 @@ export class IsolatedRunner {
1468
1484
  }
1469
1485
  }
1470
1486
  await this.requireExactIntegration(handle, generation, receipts, scope);
1471
- if (!integrationGenerationPasses(generation, request))
1487
+ if (!integrationGenerationPasses(generation, request, state.integration.candidates))
1472
1488
  throw new Error("Combined validation is incomplete.");
1473
1489
  generation.status = "ready";
1474
1490
  }
1475
1491
  catch (error) {
1476
- if (integrationGenerationPasses(generation, request)) {
1492
+ if (integrationGenerationPasses(generation, request, state.integration.candidates)) {
1477
1493
  delete generation.checks;
1478
1494
  delete generation.review; // Drift invalidates otherwise passing evidence.
1479
1495
  }
@@ -1483,7 +1499,7 @@ export class IsolatedRunner {
1483
1499
  await this.saveProductive(handle);
1484
1500
  return this.response(state);
1485
1501
  }
1486
- if (generation.status !== "ready" || !integrationGenerationPasses(generation, request)) {
1502
+ if (generation.status !== "ready" || !integrationGenerationPasses(generation, request, state.integration.candidates)) {
1487
1503
  throw new Error("Promotion requires exact successful combined validation.");
1488
1504
  }
1489
1505
  for (const definition of request.tasks.filter((task) => hasChangesetDependency(state, task.id))) {
@@ -1510,6 +1526,7 @@ export class IsolatedRunner {
1510
1526
  outcome = await scope.call((context) => this.integrationGit.promote({
1511
1527
  root, integration: generation.worktree, base: generation.expectedMain, stages: receipts,
1512
1528
  checks: generation.checks, commands: request.finalChecks,
1529
+ sources: stagedCheckSources(generation, state.integration.candidates),
1513
1530
  review: generation.review, criterion: request.finalJudgment?.criterion, correction: generation.correction,
1514
1531
  }, context.signal));
1515
1532
  }
@@ -2777,17 +2794,19 @@ export class IsolatedRunner {
2777
2794
  await save();
2778
2795
  }
2779
2796
  }
2780
- 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 = []) {
2781
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]);
2782
2801
  const result = await this.callProductive(handle, scope, async (context) => await this.gitRuntime.runChecks({
2783
2802
  root,
2784
2803
  scope: phase === "final" ? "final" : "task",
2785
2804
  ...(taskId ? { taskId, attempt } : {}),
2786
- checks,
2805
+ checks: run,
2787
2806
  candidate,
2788
2807
  }, context));
2789
2808
  const identityAfter = runtimeIdentity(result.identityAfter, "Post-check identity");
2790
- if (!exactCommandResults(result.results, checks)) {
2809
+ if (!exactCommandResults(result.results, run)) {
2791
2810
  throw new Error("Check runtime did not return one result with the exact command and argv for every declared check.");
2792
2811
  }
2793
2812
  if (result.results.some((item) => !Number.isSafeInteger(item.code)
@@ -2795,23 +2814,29 @@ export class IsolatedRunner {
2795
2814
  || typeof item.stdout !== "string" || typeof item.stderr !== "string")) {
2796
2815
  throw new Error("Check runtime returned malformed result evidence.");
2797
2816
  }
2798
- 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);
2799
2823
  const identityMatches = sameIdentity(identityAfter, candidate);
2800
2824
  const passed = identityMatches && commandsPassed;
2801
- 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);
2802
2826
  if (diagnosticIndex < 0 && commandsPassed && !identityMatches)
2803
- diagnosticIndex = result.results.length - 1;
2827
+ diagnosticIndex = results.length - 1;
2804
2828
  const evidence = {
2805
2829
  phase,
2806
2830
  candidate,
2807
2831
  identityAfter,
2808
- results: result.results.map((item, index) => ({
2832
+ results: results.map((item, index) => ({
2809
2833
  command: item.command,
2810
2834
  args: [...item.args],
2811
2835
  code: item.code,
2812
2836
  killed: item.killed ?? false,
2813
2837
  stdout: index === diagnosticIndex ? bounded(item.stdout) : "",
2814
2838
  stderr: index === diagnosticIndex ? bounded(item.stderr) : "",
2839
+ ...(reuse[index] ? { reusedFrom: reuse[index] } : {}),
2815
2840
  })),
2816
2841
  passed,
2817
2842
  at: this.coordinatorRuntime.now(),
@@ -3315,6 +3340,7 @@ export class IsolatedRunner {
3315
3340
  ...(state.integration.generations.at(-1).failure ? [`Integration: ${state.integration.generations.at(-1).failure}`] : []),
3316
3341
  ...(state.integration.generations.at(-1).promotion?.failure ? [`Promotion: ${state.integration.generations.at(-1).promotion.failure}`] : []),
3317
3342
  ...(state.integration.generations.at(-1).checks?.passed === false ? ["Combined checks failed; Main was not changed."] : []),
3343
+ ...reusedCheckLines(state.integration.generations.at(-1).checks),
3318
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."] : []),
3319
3345
  ] : []),
3320
3346
  ...(state.integration.generations.some((item) => item.status === "superseded" && item.worktree