@ferris1225/pi-subagents 4.1.2 → 4.1.4
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 +584 -506
- package/agents/cleaner.md +4 -4
- package/agents/documenter.md +46 -44
- package/agents/explorer.md +15 -11
- package/agents/reviewer.md +7 -4
- package/agents/worker.md +7 -5
- package/package.json +55 -55
- package/src/agents.ts +42 -1
- package/src/config.ts +5 -5
- package/src/dispatch.ts +647 -637
- package/src/fixloop.ts +90 -127
- package/src/monitor.ts +97 -27
- package/src/prompt.ts +3 -3
- package/src/rpc-run.ts +6 -3
- package/src/runtime.ts +5 -0
- package/src/setup.ts +151 -136
- package/src/spawn.ts +8 -2
- package/src/thread-lifecycle.ts +46 -14
- package/src/tools.ts +44 -30
- package/src/widget.ts +65 -19
package/src/fixloop.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Managed workflow policy and handoff formatting.
|
|
3
3
|
*
|
|
4
|
-
* Successful top-level writers
|
|
5
|
-
*
|
|
6
|
-
* documentation sync
|
|
7
|
-
*
|
|
8
|
-
* reviewer
|
|
9
|
-
*
|
|
4
|
+
* Successful top-level writers continue through an independent code review
|
|
5
|
+
* gate; bounded worker → reviewer fix rounds close its findings; one final
|
|
6
|
+
* documentation sync runs after the gate settles, so code fixes never
|
|
7
|
+
* invalidate an earlier docs pass and reviewers stay focused on code. A direct
|
|
8
|
+
* passing reviewer gets that same single final documentation sync instead of a
|
|
9
|
+
* second gate; a direct failing reviewer uses the same fix rounds plus the
|
|
10
|
+
* final sync. Internal steps are launched by dispatch directly, so they never
|
|
11
|
+
* re-enter this top-level policy or wake the main agent mid-chain.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
import { isWriteCapableAgent, type AgentConfig } from "./agents.ts";
|
|
13
15
|
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
14
|
-
import {
|
|
16
|
+
import { formatUsageCompact, sumUsage } from "./monitor.ts";
|
|
15
17
|
import type { SubagentsConfig } from "./config.ts";
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -107,7 +109,9 @@ export function getManagedWorkflowPlan(
|
|
|
107
109
|
if (result.agent !== "reviewer") return undefined;
|
|
108
110
|
|
|
109
111
|
const verdict = reviewVerdict(getResultOutput(result));
|
|
110
|
-
|
|
112
|
+
// The pass stands as the code gate; documenter then syncs docs once and the
|
|
113
|
+
// workflow delivers without a second gate.
|
|
114
|
+
if (verdict === "pass" && availability.documenter) {
|
|
111
115
|
return { kind: "review-pass-sync", initialRelation: "pre-documentation review" };
|
|
112
116
|
}
|
|
113
117
|
if (verdict === "fail" && availability.worker && shouldTriggerFixLoop(result, config)) {
|
|
@@ -135,77 +139,56 @@ export function buildFixTaskBrief(reviewerResult: SingleResult, round: number, m
|
|
|
135
139
|
`Fix EVERY finding in the reviewer's findings list — there is no severity triage; all of them get fixed.`,
|
|
136
140
|
`If a finding is factually wrong or clearly out of scope, say so explicitly instead of fixing it.`,
|
|
137
141
|
`Do NOT refactor unrelated code beyond what the findings require.`,
|
|
138
|
-
`Do NOT commit, push, publish, tag, or release; do not bump versions. The parent chain still owns
|
|
142
|
+
`Do NOT commit, push, publish, tag, or release; do not bump versions. The parent chain still owns re-review and the final documentation sync.`,
|
|
139
143
|
`After editing, run the project's format/build/tests when they exist and report`,
|
|
140
144
|
`exactly what you changed (paths + short rationale) so a reviewer can verify.`,
|
|
141
145
|
remaining > 0
|
|
142
146
|
? `A reviewer will re-review your changes automatically after you finish.`
|
|
143
|
-
: `This is the last auto-fix round;
|
|
147
|
+
: `This is the last auto-fix round; the workflow runs any enabled final documentation sync and then delivers.`,
|
|
144
148
|
].join("\n");
|
|
145
149
|
}
|
|
146
150
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
151
|
+
/** Build the single documentation handoff that runs after the review gate
|
|
152
|
+
* settles. The last writer's report (top-level writer or final fix-round
|
|
153
|
+
* worker) and the terminal gate review are leads; the pending diff stays
|
|
154
|
+
* authoritative. At most one of the two is undefined in every managed flow. */
|
|
155
|
+
export function buildFinalDocumenterBrief(
|
|
156
|
+
lastWriterResult?: SingleResult,
|
|
157
|
+
finalReviewResult?: SingleResult,
|
|
158
|
+
): string {
|
|
159
|
+
const reportSections = [
|
|
160
|
+
...(lastWriterResult
|
|
161
|
+
? [
|
|
162
|
+
`The last writer (${lastWriterResult.agent}) reported:`,
|
|
163
|
+
`---`,
|
|
164
|
+
getResultOutput(lastWriterResult),
|
|
165
|
+
`---`,
|
|
166
|
+
``,
|
|
167
|
+
]
|
|
168
|
+
: []),
|
|
169
|
+
...(finalReviewResult
|
|
170
|
+
? [
|
|
171
|
+
`The final gate review reported:`,
|
|
172
|
+
`---`,
|
|
173
|
+
getResultOutput(finalReviewResult),
|
|
174
|
+
`---`,
|
|
175
|
+
``,
|
|
176
|
+
]
|
|
177
|
+
: []),
|
|
178
|
+
];
|
|
161
179
|
return [
|
|
162
|
-
|
|
180
|
+
`Final documentation sync: the review gate settled and you are the last managed stage before delivery.`,
|
|
163
181
|
``,
|
|
164
182
|
...reportSections,
|
|
165
|
-
`Inspect the actual git diff (the complete pending diff) and relevant implementation; the
|
|
166
|
-
`
|
|
183
|
+
`Inspect the actual git diff (the complete pending diff) and relevant implementation; the reports are only leads.`,
|
|
184
|
+
`Apply every documentation note the reviews recorded, then synchronize stale README/docs, examples, API comments, docstrings, and explanatory comments with the behavior that will be committed.`,
|
|
167
185
|
`Change documentation surfaces only; never alter runtime behavior or tests to make prose true.`,
|
|
168
186
|
`Make zero edits when the diff creates no documentation drift.`,
|
|
169
|
-
`Do NOT commit, push, publish, tag, or release; do not bump versions.
|
|
187
|
+
`Do NOT commit, push, publish, tag, or release; do not bump versions. The parent workflow delivers directly after you; no fresh reviewer runs.`,
|
|
170
188
|
`Report exact documentation/comment paths changed, or state explicitly that no sync was needed.`,
|
|
171
189
|
].join("\n");
|
|
172
190
|
}
|
|
173
191
|
|
|
174
|
-
/** Build the pre-commit documentation handoff after one auto-fix worker. */
|
|
175
|
-
export function buildDocumenterTaskBrief(
|
|
176
|
-
workerResult: SingleResult,
|
|
177
|
-
round: number,
|
|
178
|
-
reviewerResult?: SingleResult,
|
|
179
|
-
): string {
|
|
180
|
-
return buildDocumentationBrief({
|
|
181
|
-
title: `Documentation sync after auto-fix round ${round}.`,
|
|
182
|
-
reports: [
|
|
183
|
-
...(reviewerResult ? [{ label: "The triggering reviewer reported", result: reviewerResult }] : []),
|
|
184
|
-
{ label: "The worker reported", result: workerResult },
|
|
185
|
-
],
|
|
186
|
-
closing: "a fresh reviewer gate runs after you.",
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/** Build the automatic documentation stage after a successful top-level writer. */
|
|
191
|
-
export function buildPostWriterDocumenterBrief(writerResult: SingleResult): string {
|
|
192
|
-
return buildDocumentationBrief({
|
|
193
|
-
title: `Documentation sync after successful top-level ${writerResult.agent}.`,
|
|
194
|
-
reports: [{ label: `The ${writerResult.agent} reported`, result: writerResult }],
|
|
195
|
-
closing: "the managed workflow owns any final reviewer and delivery.",
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/** A direct passing review cannot be the final gate while documenter is enabled:
|
|
200
|
-
* the preliminary report is context, but the actual pending diff is authoritative. */
|
|
201
|
-
export function buildReviewPassDocumenterBrief(reviewerResult: SingleResult): string {
|
|
202
|
-
return buildDocumentationBrief({
|
|
203
|
-
title: "Documentation sync required before accepting a direct passing review.",
|
|
204
|
-
reports: [{ label: "The preliminary reviewer reported", result: reviewerResult }],
|
|
205
|
-
closing: "the preliminary pass is not final and a fresh reviewer gate runs after you.",
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
192
|
/**
|
|
210
193
|
* One step of an auto-fix chain as delivered: the run id (so the condensed
|
|
211
194
|
* summary can point at per-run detail via subagent_status), the result, and
|
|
@@ -224,16 +207,6 @@ export interface ManagedWorkflowOutcome {
|
|
|
224
207
|
steps: ChainStep[];
|
|
225
208
|
}
|
|
226
209
|
|
|
227
|
-
/** Max distinguishing fragments kept in a one-line chain summary. */
|
|
228
|
-
export const CHAIN_SUMMARY_FRAGMENTS_MAX = 3;
|
|
229
|
-
|
|
230
|
-
/** The most telling fragments (paths, quoted phrases, symbols) of a run's final
|
|
231
|
-
* output: for a worker these are the paths it changed, for a reviewer the
|
|
232
|
-
* issues it found. Capped so summaries stay one line. */
|
|
233
|
-
export function chainKeyFragments(result: SingleResult): string[] {
|
|
234
|
-
return extractKeyFragments(getResultOutput(result)).slice(0, CHAIN_SUMMARY_FRAGMENTS_MAX);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
210
|
function workflowResultStatus(result: SingleResult): string {
|
|
238
211
|
if (isFailedResult(result)) return "failed";
|
|
239
212
|
if (result.agent === "reviewer") {
|
|
@@ -244,15 +217,8 @@ function workflowResultStatus(result: SingleResult): string {
|
|
|
244
217
|
}
|
|
245
218
|
|
|
246
219
|
function workflowStepLine(step: ChainStep): string {
|
|
247
|
-
const fragments = chainKeyFragments(step.result);
|
|
248
|
-
const writer = step.result.agent === "worker" || step.result.agent === "cleaner" || step.result.agent === "documenter";
|
|
249
|
-
const suffix = fragments.length > 0
|
|
250
|
-
? writer
|
|
251
|
-
? ` — changed: ${fragments.join(" · ")}`
|
|
252
|
-
: ` — ${fragments.join(" · ")}`
|
|
253
|
-
: "";
|
|
254
220
|
const id = step.runId !== undefined ? `#${step.runId} ` : "";
|
|
255
|
-
return `- ${id}${step.result.agent} · ${step.relation} · ${workflowResultStatus(step.result)}
|
|
221
|
+
return `- ${id}${step.result.agent} · ${step.relation} · ${workflowResultStatus(step.result)}`;
|
|
256
222
|
}
|
|
257
223
|
|
|
258
224
|
function appendWorkflowFooter(lines: string[], steps: readonly ChainStep[]): void {
|
|
@@ -260,7 +226,13 @@ function appendWorkflowFooter(lines: string[], steps: readonly ChainStep[]): voi
|
|
|
260
226
|
const usage = formatUsageCompact(total);
|
|
261
227
|
lines.push("", `Totals: ${steps.length} run${steps.length === 1 ? "" : "s"}${usage ? ` · ${usage}` : ""}`);
|
|
262
228
|
const ids = steps.filter((step) => step.runId !== undefined).map((step) => `#${step.runId}`);
|
|
263
|
-
lines.push(`
|
|
229
|
+
lines.push(`Per-run details: subagent_status ${ids.join(" ")}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function formatWorkflowSummary(title: string, steps: readonly ChainStep[]): string {
|
|
233
|
+
const lines = [title, "", ...steps.map(workflowStepLine)];
|
|
234
|
+
appendWorkflowFooter(lines, steps);
|
|
235
|
+
return lines.join("\n");
|
|
264
236
|
}
|
|
265
237
|
|
|
266
238
|
/** Condensed compatibility summary for a direct REVIEW_FAIL auto-fix chain. */
|
|
@@ -269,13 +241,10 @@ export function formatChainSummary(
|
|
|
269
241
|
terminalResult: SingleResult = steps[steps.length - 1]!.result,
|
|
270
242
|
): string {
|
|
271
243
|
const rounds = steps.filter((step) => step.relation.startsWith("fix round")).length;
|
|
272
|
-
|
|
244
|
+
return formatWorkflowSummary(
|
|
273
245
|
`## Auto-fix chain: ${Math.max(1, rounds)} round${rounds === 1 ? "" : "s"} — final ${workflowResultStatus(terminalResult)}`,
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
];
|
|
277
|
-
appendWorkflowFooter(lines, steps);
|
|
278
|
-
return lines.join("\n");
|
|
246
|
+
steps,
|
|
247
|
+
);
|
|
279
248
|
}
|
|
280
249
|
|
|
281
250
|
/** One clear final delivery for all newly managed writer/documenter workflows. */
|
|
@@ -286,42 +255,42 @@ export function formatManagedWorkflowSummary(
|
|
|
286
255
|
const route = steps.map((step) => step.result.agent).join(" → ");
|
|
287
256
|
const fixRounds = steps.filter((step) => step.relation.startsWith("fix round")).length;
|
|
288
257
|
const roundNote = fixRounds > 0 ? ` · ${fixRounds} fix round${fixRounds === 1 ? "" : "s"}` : "";
|
|
289
|
-
|
|
258
|
+
return formatWorkflowSummary(
|
|
290
259
|
`## Managed workflow: ${route}${roundNote} — final ${workflowResultStatus(terminalResult)}`,
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
260
|
+
steps,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface GateBriefOptions {
|
|
265
|
+
/** A final documenter runs after the gate settles. Documentation drift is
|
|
266
|
+
* then routed to it as non-gating notes instead of failing the gate. */
|
|
267
|
+
documenterPending: boolean;
|
|
296
268
|
}
|
|
297
269
|
|
|
298
|
-
/** Build the
|
|
299
|
-
*
|
|
300
|
-
*
|
|
270
|
+
/** Build the code gate that runs directly after a top-level writer, before any
|
|
271
|
+
* documentation. Reports carry intent; the actual pending diff remains
|
|
272
|
+
* authoritative. */
|
|
301
273
|
export function buildFinalReviewBrief(
|
|
302
274
|
initialResult: SingleResult,
|
|
303
|
-
|
|
275
|
+
options: GateBriefOptions,
|
|
304
276
|
): string {
|
|
305
|
-
const documenterSection = documenterResult
|
|
306
|
-
? [
|
|
307
|
-
``,
|
|
308
|
-
`The documenter's full sync report:`,
|
|
309
|
-
`---`,
|
|
310
|
-
getResultOutput(documenterResult),
|
|
311
|
-
`---`,
|
|
312
|
-
]
|
|
313
|
-
: [];
|
|
314
277
|
return [
|
|
315
|
-
`Fresh
|
|
278
|
+
`Fresh code gate for a managed ${initialResult.agent} workflow.`,
|
|
316
279
|
``,
|
|
317
280
|
`The top-level ${initialResult.agent}'s full report:`,
|
|
318
281
|
`---`,
|
|
319
282
|
getResultOutput(initialResult),
|
|
320
283
|
`---`,
|
|
321
|
-
...documenterSection,
|
|
322
284
|
``,
|
|
323
|
-
`Run \`git status\` and \`git diff\` and inspect the actual pending code
|
|
324
|
-
`Remain read-only. Verify correctness, regressions,
|
|
285
|
+
`Run \`git status\` and \`git diff\` and inspect the actual pending code; the report is context, not proof.`,
|
|
286
|
+
`Remain read-only. Verify correctness, regressions, and tests.`,
|
|
287
|
+
...(options.documenterPending
|
|
288
|
+
? [
|
|
289
|
+
`Documentation sync runs AFTER this gate, so documentation drift is not a gate finding: record needed`,
|
|
290
|
+
`documentation updates as a separate short "## Documentation notes" list for the final documenter,`,
|
|
291
|
+
`and fail the gate only for code or test findings.`,
|
|
292
|
+
]
|
|
293
|
+
: []),
|
|
325
294
|
`This is an acceptance gate, not an advisory audit. End with exactly one standalone machine verdict line:`,
|
|
326
295
|
`VERDICT: REVIEW_PASS when no finding remains, otherwise VERDICT: REVIEW_FAIL.`,
|
|
327
296
|
].join("\n");
|
|
@@ -329,29 +298,19 @@ export function buildFinalReviewBrief(
|
|
|
329
298
|
|
|
330
299
|
/**
|
|
331
300
|
* The re-review brief handed to the reviewer after a worker fix round. Includes
|
|
332
|
-
* the prior review
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
*
|
|
336
|
-
* a verified resolution.
|
|
301
|
+
* the prior review and worker report so the reviewer can adjudicate rejections
|
|
302
|
+
* instead of restating findings. The convergence contract keeps rounds from
|
|
303
|
+
* ping-ponging: rule on the open findings once, add only defects this round's
|
|
304
|
+
* edits introduced, never re-open a verified resolution.
|
|
337
305
|
*/
|
|
338
306
|
export function buildReReviewBrief(
|
|
339
307
|
reviewerResult: SingleResult,
|
|
340
308
|
round: number,
|
|
341
309
|
workerResult: SingleResult,
|
|
342
|
-
|
|
310
|
+
options: GateBriefOptions = { documenterPending: false },
|
|
343
311
|
): string {
|
|
344
312
|
const review = getResultOutput(reviewerResult);
|
|
345
313
|
const workerReport = getResultOutput(workerResult);
|
|
346
|
-
const documenterSection = documenterResult
|
|
347
|
-
? [
|
|
348
|
-
``,
|
|
349
|
-
`The documenter's pre-commit sync report:`,
|
|
350
|
-
`---`,
|
|
351
|
-
getResultOutput(documenterResult),
|
|
352
|
-
`---`,
|
|
353
|
-
]
|
|
354
|
-
: [];
|
|
355
314
|
return [
|
|
356
315
|
`Re-review after auto-fix round ${round}.`,
|
|
357
316
|
``,
|
|
@@ -364,7 +323,6 @@ export function buildReReviewBrief(
|
|
|
364
323
|
`---`,
|
|
365
324
|
workerReport,
|
|
366
325
|
`---`,
|
|
367
|
-
...documenterSection,
|
|
368
326
|
``,
|
|
369
327
|
`Rule on EVERY previous finding: resolved, or still open. A finding the worker rejected must be`,
|
|
370
328
|
`adjudicated ONCE — accept the rejection unless you can concretely refute the worker's reasoning;`,
|
|
@@ -372,6 +330,11 @@ export function buildReReviewBrief(
|
|
|
372
330
|
`Run \`git diff\` to see what changed, then add NEW findings only when they are defects this round's`,
|
|
373
331
|
`edits introduced or exposed (or a load-bearing issue the earlier review genuinely missed).`,
|
|
374
332
|
`Do NOT re-open a finding you verified as resolved.`,
|
|
333
|
+
...(options.documenterPending
|
|
334
|
+
? [
|
|
335
|
+
`Carry any unresolved "## Documentation notes" forward verbatim; the final documenter applies them after the chain settles.`,
|
|
336
|
+
]
|
|
337
|
+
: []),
|
|
375
338
|
`REQUEST_CHANGES only while an open finding remains; otherwise APPROVE.`,
|
|
376
339
|
`End with your machine-readable verdict line as usual (VERDICT: REVIEW_PASS / REVIEW_FAIL).`,
|
|
377
340
|
].join("\n");
|
package/src/monitor.ts
CHANGED
|
@@ -19,6 +19,7 @@ import type { IsolationMode, WorktreeFinalizationStatus } from "./worktree.ts";
|
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
|
|
21
21
|
export type RunStatus = "queued" | "running" | "steering" | "interrupting" | "parked" | "done" | "failed";
|
|
22
|
+
export type ContinuationKind = "resume-retained" | "resume-appended" | "fork-retained" | "fork-appended" | "retarget";
|
|
22
23
|
|
|
23
24
|
export function isRunActiveStatus(status: RunStatus): boolean {
|
|
24
25
|
return status === "queued" || status === "running" || status === "steering" || status === "interrupting";
|
|
@@ -45,16 +46,25 @@ export interface RunView {
|
|
|
45
46
|
usage: UsageStats;
|
|
46
47
|
/** Concise current activity ("thinking", "read src/index.ts"); last writer wins. */
|
|
47
48
|
activity?: string;
|
|
48
|
-
/** Epoch ms when
|
|
49
|
+
/** Epoch ms when this logical run first started executing. */
|
|
49
50
|
startedAt?: number;
|
|
50
|
-
/** Epoch ms when the
|
|
51
|
+
/** Epoch ms when the current active segment started. */
|
|
52
|
+
activeSince?: number;
|
|
53
|
+
/** Cumulative active execution time from closed segments; parked time is excluded. */
|
|
54
|
+
elapsedMs: number;
|
|
55
|
+
/** Epoch ms when the latest active segment stopped. */
|
|
51
56
|
endedAt?: number;
|
|
57
|
+
/** Why this generation reused retained context, shown in the widget/status. */
|
|
58
|
+
continuationKind?: ContinuationKind;
|
|
52
59
|
/** When set, this is an internal managed-workflow step. */
|
|
53
60
|
groupId?: string;
|
|
54
61
|
/** Human-readable role within a chain, e.g. "fix round 1" or "re-review round 1". */
|
|
55
62
|
relationLabel?: string;
|
|
56
63
|
/** Stable owning run whose row represents the whole managed workflow. */
|
|
57
64
|
parentRunId?: number;
|
|
65
|
+
/** This stable top-level row currently owns a multi-stage managed workflow.
|
|
66
|
+
* Its elapsed time is workflow-wide; active child rows own stage telemetry. */
|
|
67
|
+
managedWorkflow?: boolean;
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
/** Optional metadata for documenter/reviewer/fix children of a stable parent run. */
|
|
@@ -64,6 +74,7 @@ export interface RunChainMeta {
|
|
|
64
74
|
parentRunId?: number;
|
|
65
75
|
isolation?: IsolationMode;
|
|
66
76
|
forkedFromRunId?: number;
|
|
77
|
+
continuationKind?: ContinuationKind;
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
// ---------------------------------------------------------------------------
|
|
@@ -276,10 +287,32 @@ export function formatDuration(ms: number): string {
|
|
|
276
287
|
return `${hours}h${String(minutes % 60).padStart(2, "0")}m`;
|
|
277
288
|
}
|
|
278
289
|
|
|
279
|
-
/**
|
|
290
|
+
/** Cumulative active time across generations; parked gaps never count. */
|
|
291
|
+
export function elapsedMilliseconds(run: RunView, now: number = Date.now()): number {
|
|
292
|
+
let elapsed = run.elapsedMs ?? 0;
|
|
293
|
+
if (run.activeSince !== undefined) elapsed += Math.max(0, now - run.activeSince);
|
|
294
|
+
// Keep formatting tolerant of older/synthetic RunView values that predate
|
|
295
|
+
// segmented timing and carry only startedAt/endedAt.
|
|
296
|
+
if (elapsed === 0 && run.startedAt !== undefined && run.activeSince === undefined) {
|
|
297
|
+
elapsed = Math.max(0, (run.endedAt ?? now) - run.startedAt);
|
|
298
|
+
}
|
|
299
|
+
return elapsed;
|
|
300
|
+
}
|
|
301
|
+
|
|
280
302
|
export function formatElapsed(run: RunView, now: number = Date.now()): string {
|
|
281
|
-
if (run.startedAt === undefined) return "";
|
|
282
|
-
return formatDuration((run
|
|
303
|
+
if (run.startedAt === undefined && run.elapsedMs <= 0) return "";
|
|
304
|
+
return formatDuration(elapsedMilliseconds(run, now));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function continuationLabel(kind: ContinuationKind | undefined, sourceRunId?: number): string | undefined {
|
|
308
|
+
switch (kind) {
|
|
309
|
+
case "resume-retained": return "resume: current objective";
|
|
310
|
+
case "resume-appended": return "resume: appended objective";
|
|
311
|
+
case "fork-retained": return `fork${sourceRunId === undefined ? "" : ` #${sourceRunId}`}: current objective`;
|
|
312
|
+
case "fork-appended": return `fork${sourceRunId === undefined ? "" : ` #${sourceRunId}`}: appended objective`;
|
|
313
|
+
case "retarget": return "retarget: replacement objective";
|
|
314
|
+
default: return undefined;
|
|
315
|
+
}
|
|
283
316
|
}
|
|
284
317
|
|
|
285
318
|
/** Max length of the argument target inside a formatted activity line. */
|
|
@@ -409,11 +442,13 @@ export class MonitorStore {
|
|
|
409
442
|
thinking,
|
|
410
443
|
status: "queued",
|
|
411
444
|
usage: emptyUsage(),
|
|
445
|
+
elapsedMs: 0,
|
|
412
446
|
...(meta?.groupId ? { groupId: meta.groupId } : {}),
|
|
413
447
|
...(meta?.relationLabel ? { relationLabel: meta.relationLabel } : {}),
|
|
414
448
|
...(meta?.parentRunId !== undefined ? { parentRunId: meta.parentRunId } : {}),
|
|
415
449
|
...(meta?.isolation ? { isolation: meta.isolation, integrationStatus: meta.isolation === "worktree" ? "pending" : undefined } : {}),
|
|
416
450
|
...(meta?.forkedFromRunId !== undefined ? { forkedFromRunId: meta.forkedFromRunId } : {}),
|
|
451
|
+
...(meta?.continuationKind ? { continuationKind: meta.continuationKind } : {}),
|
|
417
452
|
});
|
|
418
453
|
this.notify();
|
|
419
454
|
return id;
|
|
@@ -422,17 +457,33 @@ export class MonitorStore {
|
|
|
422
457
|
setStatus(id: number, status: RunStatus): void {
|
|
423
458
|
const run = this.find(id);
|
|
424
459
|
if (!run) return;
|
|
460
|
+
const previousStatus = run.status;
|
|
461
|
+
const wasExecuting = previousStatus === "running" || previousStatus === "steering" || previousStatus === "interrupting";
|
|
462
|
+
const isExecuting = status === "running" || status === "steering" || status === "interrupting";
|
|
463
|
+
const now = Date.now();
|
|
425
464
|
run.status = status;
|
|
426
|
-
if (
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
run.
|
|
465
|
+
if (isExecuting && !wasExecuting) {
|
|
466
|
+
run.startedAt ??= now;
|
|
467
|
+
run.activeSince = now;
|
|
468
|
+
run.endedAt = undefined;
|
|
469
|
+
} else if (!isExecuting && wasExecuting && run.activeSince !== undefined) {
|
|
470
|
+
run.elapsedMs += Math.max(0, now - run.activeSince);
|
|
471
|
+
run.activeSince = undefined;
|
|
433
472
|
}
|
|
473
|
+
if ((status === "parked" || status === "done" || status === "failed") && run.endedAt === undefined) {
|
|
474
|
+
run.endedAt = now;
|
|
475
|
+
}
|
|
476
|
+
this.notify();
|
|
477
|
+
}
|
|
478
|
+
/** Switch a stable top-level row from one model run to workflow ownership.
|
|
479
|
+
* The original role remains for identity; child rows show stage telemetry. */
|
|
480
|
+
setManagedWorkflow(id: number, active: boolean): void {
|
|
481
|
+
const run = this.find(id);
|
|
482
|
+
if (!run) return;
|
|
483
|
+
run.managedWorkflow = active || undefined;
|
|
434
484
|
this.notify();
|
|
435
485
|
}
|
|
486
|
+
|
|
436
487
|
setUsage(id: number, usage: UsageStats, model?: string): void {
|
|
437
488
|
const run = this.find(id);
|
|
438
489
|
if (!run) return;
|
|
@@ -503,26 +554,38 @@ export class MonitorStore {
|
|
|
503
554
|
this.notify();
|
|
504
555
|
}
|
|
505
556
|
|
|
506
|
-
/**
|
|
507
|
-
|
|
508
|
-
setAgent(id: number, agent: string): void {
|
|
557
|
+
/** Update the objective shown for a queued retarget or resumed generation. */
|
|
558
|
+
setTask(id: number, task: string): void {
|
|
509
559
|
const run = this.find(id);
|
|
510
560
|
if (!run) return;
|
|
511
|
-
run.
|
|
561
|
+
run.task = task;
|
|
562
|
+
run.label = runLabel(task);
|
|
512
563
|
this.notify();
|
|
513
564
|
}
|
|
514
565
|
|
|
515
|
-
|
|
516
|
-
setTask(id: number, task: string): void {
|
|
566
|
+
setContinuationKind(id: number, kind: ContinuationKind): void {
|
|
517
567
|
const run = this.find(id);
|
|
518
568
|
if (!run) return;
|
|
519
|
-
run.
|
|
520
|
-
run.label = runLabel(task);
|
|
569
|
+
run.continuationKind = kind;
|
|
521
570
|
this.notify();
|
|
522
571
|
}
|
|
523
572
|
|
|
524
|
-
|
|
525
|
-
|
|
573
|
+
getElapsedMs(id: number, now: number = Date.now()): number | undefined {
|
|
574
|
+
const run = this.find(id);
|
|
575
|
+
return run ? elapsedMilliseconds(run, now) : undefined;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** Reuse a stable logical run id for a resumed generation without discarding
|
|
579
|
+
* active time accumulated by earlier generations. */
|
|
580
|
+
restartRun(
|
|
581
|
+
id: number,
|
|
582
|
+
agent: string,
|
|
583
|
+
task: string,
|
|
584
|
+
model?: string,
|
|
585
|
+
thinking?: string,
|
|
586
|
+
isolation?: IsolationMode,
|
|
587
|
+
meta?: { elapsedMs?: number; continuationKind?: ContinuationKind },
|
|
588
|
+
): void {
|
|
526
589
|
const run = this.find(id);
|
|
527
590
|
if (!run) {
|
|
528
591
|
this.runs.push({
|
|
@@ -535,6 +598,8 @@ export class MonitorStore {
|
|
|
535
598
|
...(isolation ? { isolation, integrationStatus: isolation === "worktree" ? "pending" as const : undefined } : {}),
|
|
536
599
|
status: "queued",
|
|
537
600
|
usage: emptyUsage(),
|
|
601
|
+
elapsedMs: meta?.elapsedMs ?? 0,
|
|
602
|
+
continuationKind: meta?.continuationKind,
|
|
538
603
|
});
|
|
539
604
|
this.notify();
|
|
540
605
|
return;
|
|
@@ -549,8 +614,11 @@ export class MonitorStore {
|
|
|
549
614
|
run.status = "queued";
|
|
550
615
|
run.usage = emptyUsage();
|
|
551
616
|
run.activity = undefined;
|
|
552
|
-
run.
|
|
617
|
+
run.managedWorkflow = undefined;
|
|
618
|
+
run.activeSince = undefined;
|
|
553
619
|
run.endedAt = undefined;
|
|
620
|
+
run.elapsedMs = Math.max(run.elapsedMs, meta?.elapsedMs ?? 0);
|
|
621
|
+
run.continuationKind = meta?.continuationKind;
|
|
554
622
|
this.notify();
|
|
555
623
|
}
|
|
556
624
|
|
|
@@ -589,12 +657,14 @@ export class MonitorStore {
|
|
|
589
657
|
|
|
590
658
|
summarize(run: RunView): string {
|
|
591
659
|
const usage = formatUsageCompact(run.usage);
|
|
592
|
-
const parts = [run.agent];
|
|
660
|
+
const parts = [run.managedWorkflow ? `${run.agent} workflow` : run.agent];
|
|
661
|
+
const continuation = continuationLabel(run.continuationKind, run.forkedFromRunId);
|
|
662
|
+
if (continuation) parts.push(continuation);
|
|
593
663
|
if (run.relationLabel) parts.push(run.relationLabel);
|
|
594
|
-
if (run.model) parts.push(run.model);
|
|
595
|
-
if (run.thinking) parts.push(`thinking ${run.thinking}`);
|
|
664
|
+
if (!run.managedWorkflow && run.model) parts.push(run.model);
|
|
665
|
+
if (!run.managedWorkflow && run.thinking) parts.push(`thinking ${run.thinking}`);
|
|
596
666
|
if (run.isolation === "worktree") parts.push(`worktree ${run.integrationStatus ?? "active"}`);
|
|
597
|
-
if (usage) parts.push(usage);
|
|
667
|
+
if (!run.managedWorkflow && usage) parts.push(usage);
|
|
598
668
|
const elapsed = formatElapsed(run);
|
|
599
669
|
if (elapsed) parts.push(elapsed);
|
|
600
670
|
return parts.join(" · ");
|
package/src/prompt.ts
CHANGED
|
@@ -34,8 +34,8 @@ export function buildDelegationDirective(
|
|
|
34
34
|
...(hasDocumenter ? ["documenter"] : []),
|
|
35
35
|
];
|
|
36
36
|
const automaticWriterRoute = [
|
|
37
|
-
...(hasDocumenter ? ["documenter"] : []),
|
|
38
37
|
...(hasReviewer ? ["reviewer"] : []),
|
|
38
|
+
...(hasDocumenter ? ["documenter"] : []),
|
|
39
39
|
].join(" → ");
|
|
40
40
|
const namedWorktreeTargets = [
|
|
41
41
|
...(hasWorker ? ["worker"] : []),
|
|
@@ -65,7 +65,7 @@ export function buildDelegationDirective(
|
|
|
65
65
|
: []),
|
|
66
66
|
...(hasDocumenter
|
|
67
67
|
? [
|
|
68
|
-
`Use \`documenter\` directly for explicit whole-codebase maintenance or standalone documentation work.${codeWriterNames.length > 0 ? ` Successful ${codeWriterNames.join("/")} runs already auto-sync the actual diff; never dispatch a duplicate.` : ""} Zero edits is valid and broad mode is never inferred. It changes docs/comments only and never runtime behavior, versions, release state
|
|
68
|
+
`Use \`documenter\` directly for explicit whole-codebase maintenance or standalone documentation work.${codeWriterNames.length > 0 ? ` Successful ${codeWriterNames.join("/")} runs already auto-sync the actual diff once after the review gate; never dispatch a duplicate.` : ""} Zero edits is valid and broad mode is never inferred. It changes docs/comments only and never runtime behavior, versions, or release state.`,
|
|
69
69
|
]
|
|
70
70
|
: []),
|
|
71
71
|
...(hasReviewer
|
|
@@ -103,7 +103,7 @@ export function buildDelegationDirective(
|
|
|
103
103
|
? [
|
|
104
104
|
...(hasDocumenter
|
|
105
105
|
? [
|
|
106
|
-
`A direct REVIEW_PASS is
|
|
106
|
+
`A direct REVIEW_PASS is final for code: runtime runs the final documentation sync once and delivers. A direct REVIEW_FAIL ${autoFixEnabled ? "keeps auto-fix; maxFixRounds limits worker fixes only, not the final documentation sync." : "cannot start fixes while worker/fix rounds are disabled."}`,
|
|
107
107
|
]
|
|
108
108
|
: []),
|
|
109
109
|
"Resolve every gate finding; do not bypass the configured auto-fix/re-review cap. A reviewer report without a standalone VERDICT is advisory and cannot trigger writes.",
|
package/src/rpc-run.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { tmpdir } from "node:os";
|
|
|
14
14
|
import { basename, join } from "node:path";
|
|
15
15
|
import { StringDecoder } from "node:string_decoder";
|
|
16
16
|
import type { Message } from "@earendil-works/pi-ai";
|
|
17
|
-
import type
|
|
17
|
+
import { SUBAGENT_TOOL_NAMES, type AgentConfig } from "./agents.ts";
|
|
18
18
|
import type { ThinkingLevel } from "./config.ts";
|
|
19
19
|
import type { IsolationMode, WorktreeFinalizationStatus } from "./worktree.ts";
|
|
20
20
|
|
|
@@ -469,7 +469,7 @@ export interface RunRpcAttemptOptions {
|
|
|
469
469
|
/** Run one persistent RPC child until a stable `agent_settled` or control action. */
|
|
470
470
|
export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise<RpcSingleResult> {
|
|
471
471
|
const { agent, agentName, task, thinkingLevel, idleTimeoutMs, signal, onLive, control } = options;
|
|
472
|
-
const args: string[] = ["--mode", "rpc", "--exclude-tools", "
|
|
472
|
+
const args: string[] = ["--mode", "rpc", "--exclude-tools", SUBAGENT_TOOL_NAMES.join(",")];
|
|
473
473
|
if (options.sessionDir && options.sessionId) {
|
|
474
474
|
args.push("--session-dir", options.sessionDir);
|
|
475
475
|
args.push(sessionExists(options.sessionDir, options.sessionId) ? "--session" : "--session-id", options.sessionId);
|
|
@@ -478,7 +478,10 @@ export async function runRpcAgentAttempt(options: RunRpcAttemptOptions): Promise
|
|
|
478
478
|
}
|
|
479
479
|
if (agent.model) args.push("--model", agent.model);
|
|
480
480
|
args.push("--thinking", thinkingLevel);
|
|
481
|
-
if (agent.tools
|
|
481
|
+
if (agent.tools) {
|
|
482
|
+
if (agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
|
|
483
|
+
else args.push("--no-tools");
|
|
484
|
+
}
|
|
482
485
|
|
|
483
486
|
let tmpPromptDir: string | null = null;
|
|
484
487
|
let tmpPromptPath: string | null = null;
|
package/src/runtime.ts
CHANGED
|
@@ -67,6 +67,8 @@ export interface SubagentThread {
|
|
|
67
67
|
lifecycleOperation?: ThreadLifecycleOperation;
|
|
68
68
|
sessionId?: string;
|
|
69
69
|
sessionDir?: string;
|
|
70
|
+
/** Active execution time accumulated across retained resume generations. */
|
|
71
|
+
elapsedMs: number;
|
|
70
72
|
/** Most recent generation result, retained for parked destructive-stop output. */
|
|
71
73
|
lastResult?: SingleResult;
|
|
72
74
|
/** A destructive stop retires context even if the active child settles later. */
|
|
@@ -92,6 +94,8 @@ export interface SubagentThread {
|
|
|
92
94
|
export interface SubagentRuntime {
|
|
93
95
|
configPath: string;
|
|
94
96
|
backgroundQueue: BackgroundTaskQueue;
|
|
97
|
+
/** Live parent tool names from ExtensionAPI, read again for each child launch. */
|
|
98
|
+
getActiveTools: () => string[];
|
|
95
99
|
/** False after session_shutdown; guards delivery and queue work. */
|
|
96
100
|
sessionActive: boolean;
|
|
97
101
|
/** Deliver a batch of completion messages to the main window, waking it only
|
|
@@ -128,6 +132,7 @@ export function createRuntime(pi: ExtensionAPI, configPath: string): SubagentRun
|
|
|
128
132
|
const runtime: SubagentRuntime = {
|
|
129
133
|
configPath,
|
|
130
134
|
backgroundQueue,
|
|
135
|
+
getActiveTools: () => pi.getActiveTools(),
|
|
131
136
|
sessionActive: true,
|
|
132
137
|
sendCompletionGroup: (items) => {
|
|
133
138
|
if (!runtime.sessionActive || items.length === 0) return;
|