@ferris1225/pi-subagents 2.0.3 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -19
- package/agents/reviewer.md +7 -9
- package/package.json +1 -1
- package/src/announcements.ts +70 -70
- package/src/completion.ts +10 -2
- package/src/dispatch.ts +30 -997
- package/src/fixloop.ts +24 -21
- package/src/format.ts +174 -177
- package/src/index.ts +3 -2
- package/src/monitor.ts +19 -0
- package/src/prompt.ts +2 -2
- package/src/rpc-run.ts +1125 -1125
- package/src/thread-lifecycle.ts +1063 -0
- package/src/tools.ts +1 -0
- package/src/widget.ts +186 -144
- package/src/worktree.ts +687 -687
package/README.md
CHANGED
|
@@ -32,15 +32,23 @@ The common quality loop now runs end to end without waking the main agent betwee
|
|
|
32
32
|
steps:
|
|
33
33
|
|
|
34
34
|
```text
|
|
35
|
-
reviewer (find
|
|
35
|
+
reviewer (find issues) → worker (fix every finding) → reviewer (verify) → final PASS/FAIL
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
Reviews use a single flat findings list — no severity triage. Every reported
|
|
39
|
+
finding is fixed before the change is accepted, and each re-review converges on
|
|
40
|
+
an open-finding set: the worker's explicit rejections are adjudicated once, only
|
|
41
|
+
defects a fix round introduced or exposed are added, and resolved items never
|
|
42
|
+
re-open. `maxFixRounds` stays the hard cap, so a chain always settles and wakes
|
|
43
|
+
the main agent with the full picture.
|
|
44
|
+
|
|
38
45
|
Cleanup stays a separate lifecycle: explicit cleanup intent can dispatch the
|
|
39
46
|
evidence-first `cleaner`; any edits still go through the independent `reviewer`
|
|
40
47
|
gate.
|
|
41
48
|
|
|
42
|
-
Each chain is delivered as one concise completion group
|
|
43
|
-
|
|
49
|
+
Each chain is delivered as one concise completion group whose footer totals the
|
|
50
|
+
aggregate token usage and cost of every included run, while full per-run reports
|
|
51
|
+
remain available through `subagent_status`. Its parent stays `running`
|
|
44
52
|
until the whole chain settles; completed internal rounds leave active status
|
|
45
53
|
immediately, so no `done` row keeps accumulating elapsed time. Selected-to-main
|
|
46
54
|
model handoffs keep the same retained context, and isolated parallel workers use
|
|
@@ -63,12 +71,16 @@ index.
|
|
|
63
71
|
"go check" step. `subagent_wait` is a **non-blocking** in-turn lookup by default
|
|
64
72
|
(pass `timeoutMs` to block); `subagent_status` inspects runs; `subagent_stop`
|
|
65
73
|
cancels one and delivers its partial output.
|
|
66
|
-
- **Active-only live widget** — each queued or running sub-agent gets one compact
|
|
67
|
-
width-aware primary line with task, effective model/thinking, and elapsed time;
|
|
68
|
-
|
|
74
|
+
- **Active-only live widget, as a tree** — each queued or running sub-agent gets one compact
|
|
75
|
+
width-aware primary line with task, effective model/thinking, and elapsed time; current
|
|
76
|
+
activity appears only when present on an indented second line. Auto-fix rounds nest under
|
|
77
|
+
the triggering reviewer row that owns the chain, so it is always visible who dispatched
|
|
78
|
+
what; no run ids appear here — the tree and the task label identify each row:
|
|
69
79
|
```text
|
|
70
|
-
|
|
71
|
-
|
|
80
|
+
● reviewer · review diff of src/foo.ts · claude-sonnet-4-5/high · 42s
|
|
81
|
+
├ ● worker · fix round 1 · src/foo.ts · claude-sonnet-4-5/high · 10s
|
|
82
|
+
│ grep cacheKey
|
|
83
|
+
└ ○ reviewer · re-review round 1 · claude-sonnet-4-5/high · 3s
|
|
72
84
|
```
|
|
73
85
|
Long tasks and activity paths truncate first (preserving a useful path tail when
|
|
74
86
|
possible), groups have no blank rows, and settled/parked runs disappear immediately.
|
|
@@ -83,7 +95,10 @@ index.
|
|
|
83
95
|
- **A quality gate that closes the loop** — when a reviewer returns `REVIEW_FAIL`,
|
|
84
96
|
the extension dispatches a worker briefed with the concrete findings, then a
|
|
85
97
|
re-review, up to `maxFixRounds` times — and only then wakes the main agent.
|
|
86
|
-
Every
|
|
98
|
+
Every reported finding gets fixed (no severity triage), and re-reviews converge
|
|
99
|
+
on an open-finding set instead of ping-ponging: worker rejections are adjudicated
|
|
100
|
+
once, only defects the fix round introduced are added, and resolved items never
|
|
101
|
+
re-open. Every round stays in the triggering reviewer's cwd, and chains that target the
|
|
87
102
|
same repository are serialized so shared-checkout edits cannot race.
|
|
88
103
|
- **Direct fallback with real thinking capabilities** — each agent has at most
|
|
89
104
|
one selected model. An unavailable selection, rate limit, invalid key, quota,
|
|
@@ -98,9 +113,9 @@ index.
|
|
|
98
113
|
`subagent_control` can steer active work, retarget it after a stable abort,
|
|
99
114
|
park/resume it under the same run id, or fork a parked/settled checkpoint into
|
|
100
115
|
a new independent run. Concurrent resume calls are serialized.
|
|
101
|
-
- **Concise but honest completions** —
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
- **Concise but honest completions** — group completions end with aggregate token
|
|
117
|
+
and cost totals across every included run; failed-tool diagnostics stay out of the
|
|
118
|
+
delivered message and remain one `subagent_status` call away. Actual process,
|
|
104
119
|
model, and integration failures still surface as failures.
|
|
105
120
|
- **Parallel fan-out with filesystem isolation** — independent tasks run up to a
|
|
106
121
|
configurable limit (default 4). Parallel workers default to detached Git
|
|
@@ -123,8 +138,9 @@ an undifferentiated child-agent launcher:
|
|
|
123
138
|
independent reviewer gate;
|
|
124
139
|
- isolated, retained threads that can be steered, parked, resumed, retargeted, or
|
|
125
140
|
forked under stable run ids;
|
|
126
|
-
- the reviewer → worker auto-fix → reviewer loop
|
|
127
|
-
|
|
141
|
+
- the reviewer → worker auto-fix → reviewer loop, fixing every finding under a
|
|
142
|
+
convergence contract with a hard round cap;
|
|
143
|
+
- failed-tool diagnostics available by run id through `subagent_status`;
|
|
128
144
|
- direct selected→main fallback plus capability-aware Auto thinking;
|
|
129
145
|
- detached Git worktree isolation for parallel workers and opt-in write-capable
|
|
130
146
|
cleaner runs.
|
|
@@ -226,7 +242,8 @@ because they may need to inspect the same images.
|
|
|
226
242
|
### Controlling and stopping
|
|
227
243
|
|
|
228
244
|
Dispatch confirmations, tool result rows, and completion blocks all show the
|
|
229
|
-
stable `#id`, so a thread remains directly controllable after its live UI is gone
|
|
245
|
+
stable `#id`, so a thread remains directly controllable after its live UI is gone
|
|
246
|
+
(the widget itself identifies rows by tree position and task instead of ids).
|
|
230
247
|
|
|
231
248
|
- `subagent_control` — `steer`, `retarget`, `park`, `resume`, or `fork` a logical
|
|
232
249
|
thread by stable run id. Resume accepts an optional replacement objective;
|
|
@@ -317,7 +334,7 @@ direct-file settings.
|
|
|
317
334
|
| `proactiveInjection` | Whether to add the delegation directive to the main system prompt. |
|
|
318
335
|
| `agentScope` | `user`, `project`, or `both`; controls which user/project agent directories are discovered. |
|
|
319
336
|
| `maxConcurrency` | Max sub-agent processes running at once (1–16, default 4), and the max tasks one parallel `subagent` call accepts. Extra work waits in the queue. |
|
|
320
|
-
| `maxFixRounds` | Auto-fix rounds when a reviewer returns `REVIEW_FAIL` (default 2; `0` disables the loop). |
|
|
337
|
+
| `maxFixRounds` | Auto-fix rounds when a reviewer returns `REVIEW_FAIL` (default 2; `0` disables the loop). Hard cap: the chain always settles, delivers its condensed summary, and wakes the main agent. |
|
|
321
338
|
| `idleTimeoutSec` | Idle watchdog: a sub-agent whose stdout goes silent for this long is terminated; a selected model then hands to current main. `0` disables it. Default 90. |
|
|
322
339
|
|
|
323
340
|
### Model routing and thinking
|
|
@@ -429,12 +446,13 @@ npm run check
|
|
|
429
446
|
npm test
|
|
430
447
|
```
|
|
431
448
|
|
|
432
|
-
The source is modular: `dispatch.ts` (dispatch
|
|
449
|
+
The source is modular: `dispatch.ts` (public dispatch contract + auto-fix),
|
|
450
|
+
`thread-lifecycle.ts` (queued generations, resume/fork, and isolation settlement),
|
|
433
451
|
`rpc-run.ts` / `spawn.ts` (persistent child transport + selected→main handoff),
|
|
434
452
|
`worktree.ts` / `session-fork.ts` (filesystem/session branching), `tools.ts`
|
|
435
453
|
(wait/status/control/stop), `widget.ts` (active-only TUI status), `announcements.ts`
|
|
436
|
-
(recovery and feature notices), and `runtime.ts` (session-scoped ownership). No runtime
|
|
437
|
-
dependencies.
|
|
454
|
+
(recovery and feature notices), and `runtime.ts` (session-scoped ownership). No runtime
|
|
455
|
+
dependencies beyond pi peer dependencies.
|
|
438
456
|
|
|
439
457
|
## License
|
|
440
458
|
|
package/agents/reviewer.md
CHANGED
|
@@ -42,25 +42,23 @@ Understand the context first, then verify: the fix addresses the root cause, cha
|
|
|
42
42
|
- Concurrency: shared mutable state, locks held across await, races.
|
|
43
43
|
- Encoding/Unicode: assuming `char*`/files/CLI text is UTF-8; wrong `A` vs `W` Win32 APIs; boundary conversions.
|
|
44
44
|
- Resource leaks; violations of the project's stated conventions.
|
|
45
|
-
-
|
|
45
|
+
- Every finding you report gets fixed by the auto-fix loop — there are no severity tiers and no optional nits. Report genuine defects and risks only, each defensible with file:line evidence; leave out style preferences and anything you would merely "suggest". Order findings most important first.
|
|
46
46
|
|
|
47
47
|
## Collaboration
|
|
48
48
|
- Independent of `worker` by design — your verdict is the gate before commit. Fix nothing yourself; report so the caller can dispatch a worker.
|
|
49
|
+
- Re-reviews converge on an open-finding set: rule on each previously reported finding once (adjudicating the worker's explicit rejections — uphold only with a concrete refutation), add only defects the fix round introduced or exposed, and never re-open an item you verified resolved. Rounds are hard-capped, so padding a re-review with restated findings just burns them.
|
|
49
50
|
|
|
50
51
|
## Output format
|
|
51
52
|
## Files Reviewed
|
|
52
53
|
- `path/to/file.ts`
|
|
53
|
-
##
|
|
54
|
+
## Findings
|
|
54
55
|
- `file.ts:42` — concrete issue and why it breaks.
|
|
55
|
-
|
|
56
|
-
- `file.ts:10` — issue and suggested direction.
|
|
57
|
-
## Suggestions (consider)
|
|
58
|
-
- Optional improvements.
|
|
56
|
+
(Write "None" when you found nothing; an empty findings list is a valid, honest result.)
|
|
59
57
|
## Verdict
|
|
60
|
-
One of: APPROVE /
|
|
61
|
-
End with exactly one machine-readable line: `VERDICT: REVIEW_PASS` for APPROVE
|
|
58
|
+
One of: APPROVE / REQUEST_CHANGES, plus a 2-3 sentence rationale.
|
|
59
|
+
End with exactly one machine-readable line: `VERDICT: REVIEW_PASS` for APPROVE; `VERDICT: REVIEW_FAIL` for REQUEST_CHANGES.
|
|
62
60
|
|
|
63
|
-
REQUEST_CHANGES
|
|
61
|
+
REQUEST_CHANGES whenever even one finding remains. On this extension REQUEST_CHANGES automatically starts an auto-fix loop: a worker is briefed with your findings, fixes every one of them, and you re-review, up to the configured round limit. APPROVE means the findings list is empty — or, on a re-review, that every previously reported finding is resolved. Never quietly wave an issue through, and never invent findings to hedge: report each genuine issue once, accurately.
|
|
64
62
|
|
|
65
63
|
## Quality standards
|
|
66
64
|
Specific file paths and line numbers. No vague feedback. A clean report means you looked hard, not that you found nothing to say.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Controllable background sub-agent threads for pi: specialized roles, capability-aware thinking, direct main-model fallback, auto-fix chains, and Git worktree isolation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/announcements.ts
CHANGED
|
@@ -1,70 +1,70 @@
|
|
|
1
|
-
/** Session-start recovery and one-time feature announcements. */
|
|
2
|
-
|
|
3
|
-
import { stat } from "node:fs/promises";
|
|
4
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { loadConfig, saveConfig } from "./config.ts";
|
|
6
|
-
import { announceRecoveryRecords } from "./recovery.ts";
|
|
7
|
-
import type { SubagentRuntime } from "./runtime.ts";
|
|
8
|
-
import { pruneResultArtifacts } from "./spawn.ts";
|
|
9
|
-
import { installActiveRunsWidget } from "./widget.ts";
|
|
10
|
-
|
|
11
|
-
const ANNOUNCEMENTS: Array<{
|
|
12
|
-
key: string;
|
|
13
|
-
condition: (config: Awaited<ReturnType<typeof loadConfig>>) => boolean;
|
|
14
|
-
message: string;
|
|
15
|
-
}> = [
|
|
16
|
-
{
|
|
17
|
-
key: "visionModel",
|
|
18
|
-
condition: (config) => config.visionModel === undefined,
|
|
19
|
-
message:
|
|
20
|
-
"pi-subagents: new — a vision-capable model can now handle image tasks (screenshots, mockups, designs). Run /subagents-setup to configure it; until set, vision tasks use the main session's current model.",
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
key: "cleanerAgent",
|
|
24
|
-
condition: (config) => !config.enabledAgents.includes("cleaner"),
|
|
25
|
-
message:
|
|
26
|
-
"pi-subagents: new built-in cleaner agent is available for evidence-first code cleanup. Run /subagents-setup to enable it; your existing enabledAgents selection was left unchanged.",
|
|
27
|
-
},
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
async function announceNewFeatures(
|
|
31
|
-
ctx: { ui: { notify: (message: string, kind: "info" | "warning" | "error") => void } },
|
|
32
|
-
runtime: SubagentRuntime,
|
|
33
|
-
): Promise<void> {
|
|
34
|
-
try {
|
|
35
|
-
let configExists = true;
|
|
36
|
-
try {
|
|
37
|
-
await stat(runtime.configPath);
|
|
38
|
-
} catch {
|
|
39
|
-
configExists = false;
|
|
40
|
-
}
|
|
41
|
-
if (!configExists) return;
|
|
42
|
-
|
|
43
|
-
const config = await loadConfig(runtime.configPath);
|
|
44
|
-
const pending = ANNOUNCEMENTS.filter(
|
|
45
|
-
(announcement) =>
|
|
46
|
-
announcement.condition(config) && !config.announcedFeatures.includes(announcement.key),
|
|
47
|
-
);
|
|
48
|
-
if (pending.length === 0) return;
|
|
49
|
-
await saveConfig(
|
|
50
|
-
{
|
|
51
|
-
...config,
|
|
52
|
-
announcedFeatures: [...config.announcedFeatures, ...pending.map((announcement) => announcement.key)],
|
|
53
|
-
},
|
|
54
|
-
runtime.configPath,
|
|
55
|
-
);
|
|
56
|
-
for (const announcement of pending) ctx.ui.notify(announcement.message, "info");
|
|
57
|
-
} catch {
|
|
58
|
-
/* announcement failures are non-fatal */
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function registerAnnouncements(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
63
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
64
|
-
pruneResultArtifacts();
|
|
65
|
-
await announceRecoveryRecords(runtime.configPath, ctx);
|
|
66
|
-
if (ctx.mode !== "tui") return;
|
|
67
|
-
installActiveRunsWidget(ctx);
|
|
68
|
-
await announceNewFeatures(ctx, runtime);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
1
|
+
/** Session-start recovery and one-time feature announcements. */
|
|
2
|
+
|
|
3
|
+
import { stat } from "node:fs/promises";
|
|
4
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import { loadConfig, saveConfig } from "./config.ts";
|
|
6
|
+
import { announceRecoveryRecords } from "./recovery.ts";
|
|
7
|
+
import type { SubagentRuntime } from "./runtime.ts";
|
|
8
|
+
import { pruneResultArtifacts } from "./spawn.ts";
|
|
9
|
+
import { installActiveRunsWidget } from "./widget.ts";
|
|
10
|
+
|
|
11
|
+
const ANNOUNCEMENTS: Array<{
|
|
12
|
+
key: string;
|
|
13
|
+
condition: (config: Awaited<ReturnType<typeof loadConfig>>) => boolean;
|
|
14
|
+
message: string;
|
|
15
|
+
}> = [
|
|
16
|
+
{
|
|
17
|
+
key: "visionModel",
|
|
18
|
+
condition: (config) => config.visionModel === undefined,
|
|
19
|
+
message:
|
|
20
|
+
"pi-subagents: new — a vision-capable model can now handle image tasks (screenshots, mockups, designs). Run /subagents-setup to configure it; until set, vision tasks use the main session's current model.",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: "cleanerAgent",
|
|
24
|
+
condition: (config) => !config.enabledAgents.includes("cleaner"),
|
|
25
|
+
message:
|
|
26
|
+
"pi-subagents: new built-in cleaner agent is available for evidence-first code cleanup. Run /subagents-setup to enable it; your existing enabledAgents selection was left unchanged.",
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
async function announceNewFeatures(
|
|
31
|
+
ctx: { ui: { notify: (message: string, kind: "info" | "warning" | "error") => void } },
|
|
32
|
+
runtime: SubagentRuntime,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
try {
|
|
35
|
+
let configExists = true;
|
|
36
|
+
try {
|
|
37
|
+
await stat(runtime.configPath);
|
|
38
|
+
} catch {
|
|
39
|
+
configExists = false;
|
|
40
|
+
}
|
|
41
|
+
if (!configExists) return;
|
|
42
|
+
|
|
43
|
+
const config = await loadConfig(runtime.configPath);
|
|
44
|
+
const pending = ANNOUNCEMENTS.filter(
|
|
45
|
+
(announcement) =>
|
|
46
|
+
announcement.condition(config) && !config.announcedFeatures.includes(announcement.key),
|
|
47
|
+
);
|
|
48
|
+
if (pending.length === 0) return;
|
|
49
|
+
await saveConfig(
|
|
50
|
+
{
|
|
51
|
+
...config,
|
|
52
|
+
announcedFeatures: [...config.announcedFeatures, ...pending.map((announcement) => announcement.key)],
|
|
53
|
+
},
|
|
54
|
+
runtime.configPath,
|
|
55
|
+
);
|
|
56
|
+
for (const announcement of pending) ctx.ui.notify(announcement.message, "info");
|
|
57
|
+
} catch {
|
|
58
|
+
/* announcement failures are non-fatal */
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function registerAnnouncements(pi: ExtensionAPI, runtime: SubagentRuntime): void {
|
|
63
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
64
|
+
pruneResultArtifacts();
|
|
65
|
+
await announceRecoveryRecords(runtime.configPath, ctx);
|
|
66
|
+
if (ctx.mode !== "tui") return;
|
|
67
|
+
installActiveRunsWidget(ctx);
|
|
68
|
+
await announceNewFeatures(ctx, runtime);
|
|
69
|
+
});
|
|
70
|
+
}
|
package/src/completion.ts
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { getResultOutput, isFailedResult, reviewVerdict, type SingleResult } from "./spawn.ts";
|
|
11
|
+
import { formatUsageCompact, sumUsage } from "./monitor.ts";
|
|
12
|
+
import type { UsageStats } from "./rpc-run.ts";
|
|
11
13
|
|
|
12
14
|
export interface CompletionBatchTimings {
|
|
13
15
|
debounceMs: number;
|
|
@@ -98,14 +100,20 @@ export interface CompletionMessageItem {
|
|
|
98
100
|
agent: string;
|
|
99
101
|
block: string;
|
|
100
102
|
triggerTurn: boolean;
|
|
103
|
+
/** Final usage of the underlying run (or chain); aggregated into the group totals. */
|
|
104
|
+
usage?: UsageStats;
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
/** Keep the established single-result shape; add a
|
|
107
|
+
/** Keep the established single-result shape; add a group header and an aggregate
|
|
108
|
+
* token/cost footer only for real groups. */
|
|
104
109
|
export function formatCompletionMessage(items: readonly CompletionMessageItem[]): string {
|
|
105
110
|
if (items.length === 0) return "";
|
|
106
111
|
if (items.length === 1) return items[0].block;
|
|
107
112
|
const agents = items.map((item) => item.agent).join(", ");
|
|
108
|
-
|
|
113
|
+
const withUsage = items.filter((item) => item.usage !== undefined);
|
|
114
|
+
const totals = withUsage.length > 0 ? formatUsageCompact(sumUsage(withUsage.map((item) => item.usage!))) : "";
|
|
115
|
+
const footer = totals ? `\n\nTotals: ${items.length} runs · ${totals}` : "";
|
|
116
|
+
return `### Subagents completed (${items.length}): ${agents}\n\n${items.map((item) => item.block).join("\n\n")}${footer}`;
|
|
109
117
|
}
|
|
110
118
|
|
|
111
119
|
/** A grouped completion wakes the main agent when any member requires a turn. */
|