@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 +13 -5
- 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 +16 -0
- package/dist/runner.js +42 -16
- package/dist/schema.d.ts +143 -2
- package/dist/schema.js +38 -6
- package/docs/orchestration.md +36 -7
- package/extensions/isolated.ts +56 -3
- package/extensions/result-transport.ts +55 -2
- package/extensions/subagent-command.ts +32 -18
- package/extensions/subagent.ts +89 -23
- package/extensions/workflow.ts +144 -4
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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
|
|
package/dist/direct-herdr.d.ts
CHANGED
|
@@ -8,10 +8,10 @@ export interface DirectHandle {
|
|
|
8
8
|
sessionFile: string;
|
|
9
9
|
prompt: string;
|
|
10
10
|
answer(maxBytes: number): Promise<string>;
|
|
11
|
-
|
|
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
|
|
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, "
|
|
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"> & {
|
package/dist/direct-herdr.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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?: {
|
package/dist/integration-git.js
CHANGED
|
@@ -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
|
-
|
|
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." };
|
package/dist/launch-runtime.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/launch-runtime.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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 :
|
|
2825
|
+
let diagnosticIndex = passed ? -1 : results.findIndex((item) => item.code !== 0 || item.killed);
|
|
2802
2826
|
if (diagnosticIndex < 0 && commandsPassed && !identityMatches)
|
|
2803
|
-
diagnosticIndex =
|
|
2827
|
+
diagnosticIndex = results.length - 1;
|
|
2804
2828
|
const evidence = {
|
|
2805
2829
|
phase,
|
|
2806
2830
|
candidate,
|
|
2807
2831
|
identityAfter,
|
|
2808
|
-
results:
|
|
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
|