@bacnh85/pi-subagent 0.19.2 → 0.20.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/CHANGELOG.md +27 -0
- package/README.md +25 -0
- package/extensions/auto-review.ts +282 -0
- package/extensions/index.ts +31 -0
- package/extensions/roles.ts +10 -0
- package/package.json +10 -9
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,32 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.20.0 (2026-09-07)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Auto-review** (`subagent.autoReview: true` in settings.json, default off) —
|
|
8
|
+
after a user-initiated turn that made ≥3 file-mutation tool calls
|
|
9
|
+
(`edit`/`write`/`apply_patch`/`str_replace_editor`) in an interactive (TUI)
|
|
10
|
+
session, the read-only `reviewer` agent is dispatched automatically as a
|
|
11
|
+
background task reviewing the current uncommitted diff of the files the turn
|
|
12
|
+
touched (new/untracked files are read directly); its findings wake the parent
|
|
13
|
+
via the normal background follow-up turn, so an independent review happens
|
|
14
|
+
after every real coding turn without asking. Precedence: global
|
|
15
|
+
settings.json → trusted repo `.pi/settings.json` overlay. Guards keep it
|
|
16
|
+
bounded: turns woken by auto-injected messages are
|
|
17
|
+
skipped (custom wake-ups by role, and pi-advisor blocker/concern steers by
|
|
18
|
+
their fixed `Advisor review (` content prefixes since those are plain user
|
|
19
|
+
messages), max 3 dispatches per session, never while another background task
|
|
20
|
+
runs, cursor tracked while the setting is off so enabling mid-session never
|
|
21
|
+
replays history, `session_start` (startup and reload) reseeds it, and a
|
|
22
|
+
fresh session's first coding turn is reviewed from entry zero. Subagent
|
|
23
|
+
catalog prompt now also states when NOT to delegate (single-file small
|
|
24
|
+
edits, quick greps → inline).
|
|
25
|
+
|
|
26
|
+
## 0.19.3 (2026-09-05)
|
|
27
|
+
|
|
28
|
+
- Widen Pi SDK peer range to `>=0.80.0 <0.86.0` and bump devDep to `^0.85.0` for Pi 0.85.0 compatibility (no breaking changes; peer cap widening only).
|
|
29
|
+
|
|
3
30
|
## 0.19.2 (2026-09-02)
|
|
4
31
|
|
|
5
32
|
### Fixed
|
package/README.md
CHANGED
|
@@ -66,6 +66,31 @@ subagent({ operation: "cancel", taskId: "bg-..." }) // abort a running task
|
|
|
66
66
|
|
|
67
67
|
You will be notified on completion — do not poll or sleep.
|
|
68
68
|
|
|
69
|
+
## Auto-review
|
|
70
|
+
|
|
71
|
+
Opt-in workflow automation (`subagent.autoReview: true` in settings.json,
|
|
72
|
+
default off): after a **user-initiated** turn that made ≥3 file-mutation tool
|
|
73
|
+
calls (`edit`, `write`, `apply_patch`, `str_replace_editor`) in an interactive
|
|
74
|
+
(TUI) session, the read-only `reviewer` agent is dispatched automatically as a
|
|
75
|
+
background task reviewing the current uncommitted diff of the files the turn
|
|
76
|
+
touched (new/untracked files are read directly by the reviewer). Its findings
|
|
77
|
+
arrive as a background follow-up turn, so an independent review follows every
|
|
78
|
+
real coding turn without asking. Precedence: global settings.json → trusted
|
|
79
|
+
repo `.pi/settings.json` overlay.
|
|
80
|
+
|
|
81
|
+
Guards keep it bounded:
|
|
82
|
+
|
|
83
|
+
- Turns woken by auto-injected messages never trigger a review — custom
|
|
84
|
+
wake-ups (`pi-subagent-complete`) by role, and pi-advisor blocker/concern
|
|
85
|
+
steers by their fixed `Advisor review (` content prefixes (those are plain
|
|
86
|
+
user messages) — so review→fix→review ping-pong can't start.
|
|
87
|
+
- Max 3 auto-reviews per session; never while another background task runs.
|
|
88
|
+
- The cursor tracks the transcript tail while the setting is off, so enabling
|
|
89
|
+
mid-session never replays accumulated history; `session_start` (startup and
|
|
90
|
+
reload) reseeds it, and a fresh session's first coding turn is reviewed from
|
|
91
|
+
entry zero. Reviewer timeout is 10 minutes; failures are non-blocking. Set
|
|
92
|
+
`PI_SUBAGENT_AUTOREVIEW_DEBUG=1` to trace dispatch decisions on stderr.
|
|
93
|
+
|
|
69
94
|
## History
|
|
70
95
|
|
|
71
96
|
Every completed task (foreground and background) is recorded to
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-review: after a user-initiated turn that mutated files, dispatch the
|
|
3
|
+
* read-only `reviewer` agent on the turn's diff as a background task. Its
|
|
4
|
+
* findings wake the parent via the normal background follow-up turn.
|
|
5
|
+
*
|
|
6
|
+
* Enabled via `subagent.autoReview: true` in settings.json (default off).
|
|
7
|
+
* Guards keep it quiet: user-initiated turns only (auto-injected wake-ups are
|
|
8
|
+
* excluded — see ADVISOR_PREFIXES), a per-session dispatch cap, and never two
|
|
9
|
+
* background tasks at once.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { readSubagentSection, readSubagentSectionFrom } from "./roles.ts";
|
|
16
|
+
import { discoverAgents } from "./agents.ts";
|
|
17
|
+
import { runNamedAgent } from "./service.ts";
|
|
18
|
+
import { startBackgroundTask, type BackgroundDeps } from "./background.ts";
|
|
19
|
+
import type { threadStore as ThreadStoreType } from "./threads.ts";
|
|
20
|
+
import type { SubAgentResult } from "./runner.ts";
|
|
21
|
+
|
|
22
|
+
/** File-mutating tool names counted toward the dispatch threshold. */
|
|
23
|
+
const MUTATION_TOOLS = new Set(["edit", "write", "apply_patch", "str_replace_editor"]);
|
|
24
|
+
/** Minimum mutation tool calls in one turn before a review is worth dispatching. */
|
|
25
|
+
export const MIN_MUTATIONS = 3;
|
|
26
|
+
/** Per-session dispatch cap — bounds model cost and any review→fix→review loop. */
|
|
27
|
+
export const MAX_PER_SESSION = 3;
|
|
28
|
+
/** Reviewer inactivity timeout for the bounded diff review (ms). */
|
|
29
|
+
export const REVIEW_TIMEOUT_MS = 10 * 60 * 1_000;
|
|
30
|
+
/** Max diff text embedded in the task (reviewer can `read` files for more). */
|
|
31
|
+
const MAX_EMBED_BYTES = 24 * 1024;
|
|
32
|
+
|
|
33
|
+
// pi-advisor steers blockers/concerns via pi.sendUserMessage — plain user-role
|
|
34
|
+
// messages with NO customType (fixed templates in pi-advisor's lib/watcher.ts).
|
|
35
|
+
// A turn woken by them is not user-initiated and must not re-trigger review.
|
|
36
|
+
const ADVISOR_PREFIXES = [
|
|
37
|
+
"Advisor review (nit",
|
|
38
|
+
"Advisor review (concern",
|
|
39
|
+
"Advisor review (blocker",
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/** Effective `subagent.autoReview`: layered ctx settings (when the SDK exposes
|
|
43
|
+
* them) → trusted repo `.pi/settings.json` → global settings.json. Mirrors
|
|
44
|
+
* readSubagentRoles precedence. */
|
|
45
|
+
export function readAutoReviewEnabled(ctx?: ExtensionContext, globalSection: Record<string, unknown> = readSubagentSection()): boolean {
|
|
46
|
+
let enabled = globalSection.autoReview === true;
|
|
47
|
+
try {
|
|
48
|
+
if (ctx?.isProjectTrusted?.()) {
|
|
49
|
+
const project = readSubagentSectionFrom(join(ctx.cwd, ".pi", "settings.json"));
|
|
50
|
+
if (typeof project.autoReview === "boolean") enabled = project.autoReview;
|
|
51
|
+
}
|
|
52
|
+
} catch { /* untrusted ctx or unreadable file — global only */ }
|
|
53
|
+
const layered = (ctx as unknown as { settings?: { subagent?: { autoReview?: unknown } } } | undefined)?.settings?.subagent?.autoReview;
|
|
54
|
+
if (typeof layered === "boolean") return layered;
|
|
55
|
+
return enabled;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface AutoReviewState {
|
|
59
|
+
/** Last transcript entry id classified; undefined until the first settle reseeds it. */
|
|
60
|
+
cursor: string | undefined;
|
|
61
|
+
/** Auto-reviews dispatched this session. */
|
|
62
|
+
dispatched: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createAutoReviewState(): AutoReviewState {
|
|
66
|
+
return { cursor: undefined, dispatched: 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface TurnAnalysis {
|
|
70
|
+
mutationCount: number;
|
|
71
|
+
files: string[];
|
|
72
|
+
/** True only when a real user prompt (not an advisor steer / custom wake-up) drove the turn. */
|
|
73
|
+
userInitiated: boolean;
|
|
74
|
+
cursor: string | undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function userEntryText(entry: any): string {
|
|
78
|
+
const content = entry?.message?.content;
|
|
79
|
+
if (!Array.isArray(content)) return "";
|
|
80
|
+
return content
|
|
81
|
+
.filter((part: any) => part?.type === "text")
|
|
82
|
+
.map((part: any) => String(part?.text ?? ""))
|
|
83
|
+
.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function extractMutatedPaths(part: any): string[] {
|
|
87
|
+
const args = part?.arguments;
|
|
88
|
+
if (typeof args?.path === "string" && args.path) return [args.path];
|
|
89
|
+
if (typeof args?.file_path === "string" && args.file_path) return [args.file_path];
|
|
90
|
+
if (typeof args?.patch === "string") {
|
|
91
|
+
return [...args.patch.matchAll(/^\*\*\* (?:Update|Add|Delete) File: (.+)$/gm)].map((m) => m[1]!.trim());
|
|
92
|
+
}
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Classify one settled turn's entries (those after `sinceId`): mutation tool
|
|
98
|
+
* calls, changed files, and whether a genuine user prompt drove the turn.
|
|
99
|
+
* Cursor entry itself is excluded (pi-advisor toolCallCount pattern).
|
|
100
|
+
*/
|
|
101
|
+
export function analyzeTurn(entries: any[], sinceId: string | undefined): TurnAnalysis {
|
|
102
|
+
// Flip counting at the cursor entry but classify from the NEXT entry on —
|
|
103
|
+
// strictly-after semantics, matching pi-advisor's toolCallCount.
|
|
104
|
+
let counting = sinceId === undefined;
|
|
105
|
+
let mutationCount = 0;
|
|
106
|
+
const files = new Set<string>();
|
|
107
|
+
let userInitiated = false;
|
|
108
|
+
let cursor = sinceId;
|
|
109
|
+
for (const entry of entries) {
|
|
110
|
+
if (counting) {
|
|
111
|
+
if (typeof entry?.id === "string") cursor = entry.id;
|
|
112
|
+
if (entry?.type === "message") {
|
|
113
|
+
const role = entry.message?.role;
|
|
114
|
+
if (role === "user") {
|
|
115
|
+
const text = userEntryText(entry);
|
|
116
|
+
if (text && !ADVISOR_PREFIXES.some((p) => text.startsWith(p))) userInitiated = true;
|
|
117
|
+
} else if (role === "assistant" && Array.isArray(entry.message?.content)) {
|
|
118
|
+
for (const part of entry.message.content) {
|
|
119
|
+
if (part?.type !== "toolCall" || !MUTATION_TOOLS.has(part?.name)) continue;
|
|
120
|
+
mutationCount++;
|
|
121
|
+
for (const file of extractMutatedPaths(part)) files.add(file);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} else if (entry?.id === sinceId) {
|
|
126
|
+
counting = true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { mutationCount, files: [...files], userInitiated, cursor };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** First-call reseed: point the cursor at the transcript tail so a mid-session
|
|
133
|
+
* enable never replays history (pi-advisor reseedCursor pattern). */
|
|
134
|
+
export function latestEntryId(entries: any[]): string | undefined {
|
|
135
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
136
|
+
if (typeof entries[i]?.id === "string") return entries[i].id;
|
|
137
|
+
}
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function git(cwd: string, args: string[], maxBytes: number): Promise<string> {
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
execFile("git", ["-C", cwd, ...args], {
|
|
144
|
+
timeout: 10_000,
|
|
145
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
146
|
+
}, (err, stdout) => {
|
|
147
|
+
if (err) return resolve("");
|
|
148
|
+
const out = String(stdout);
|
|
149
|
+
resolve(out.length > maxBytes ? out.slice(0, maxBytes) + "\n… (truncated)" : out);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Scoped diff + porcelain status for the turn's changed files. Never rejects. */
|
|
155
|
+
export async function captureDiff(cwd: string, files: string[]): Promise<{ diff: string; status: string }> {
|
|
156
|
+
return {
|
|
157
|
+
diff: files.length > 0 ? await git(cwd, ["diff", "HEAD", "--", ...files], MAX_EMBED_BYTES) : "",
|
|
158
|
+
status: await git(cwd, ["status", "--porcelain"], 4_096),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function buildReviewTask(files: string[], diff: string, status: string): string {
|
|
163
|
+
return [
|
|
164
|
+
"Review this change for correctness, security, regressions, and missing tests. Only the files below are in scope — do not attempt a broader review.",
|
|
165
|
+
"",
|
|
166
|
+
"Changed files:",
|
|
167
|
+
...files.map((file) => `- ${file}`),
|
|
168
|
+
"",
|
|
169
|
+
status ? `Git status:\n${status}\n` : "",
|
|
170
|
+
diff ? `Diff (may be truncated):\n${diff}` : "No tracked diff captured — read the changed files directly.",
|
|
171
|
+
"",
|
|
172
|
+
"Output contract (hard limits):",
|
|
173
|
+
"- Max 5 findings, one line each: `path:line — issue — evidence`.",
|
|
174
|
+
"- No praise, no style noise, no restating the diff.",
|
|
175
|
+
"- If nothing warrants a finding, reply with exactly: REVIEW: CLEAN",
|
|
176
|
+
"- Max 30 lines total.",
|
|
177
|
+
]
|
|
178
|
+
.filter((line) => line !== "")
|
|
179
|
+
.join("\n");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* runOne adapter so the auto-review dispatch can reuse startBackgroundTask
|
|
184
|
+
* (threads, history, status/cancel, completion delivery) without the tool
|
|
185
|
+
* path's execute-scoped runOne. Always read-only; reviewer needs no bash.
|
|
186
|
+
*/
|
|
187
|
+
export function makeHookRunOne(bundledAgentsDir: string, ctx: ExtensionContext): BackgroundDeps["runOne"] {
|
|
188
|
+
return async (agentName, task, _cwd, signal, timeoutMs, onProgress, onActivity) => {
|
|
189
|
+
const agent = discoverAgents(ctx.cwd, "user", bundledAgentsDir).agents.find((a) => a.name === agentName);
|
|
190
|
+
if (!agent) {
|
|
191
|
+
const errorResult: SubAgentResult = {
|
|
192
|
+
agent: agentName,
|
|
193
|
+
task,
|
|
194
|
+
exitCode: 1,
|
|
195
|
+
status: "error",
|
|
196
|
+
stopReason: "error",
|
|
197
|
+
messages: [],
|
|
198
|
+
stderr: `Unknown agent: "${agentName}".`,
|
|
199
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
200
|
+
errorMessage: `Unknown agent: "${agentName}"`,
|
|
201
|
+
};
|
|
202
|
+
return errorResult;
|
|
203
|
+
}
|
|
204
|
+
// ponytail: hook dispatches are always read-only regardless of the agent file
|
|
205
|
+
return await runNamedAgent({
|
|
206
|
+
agent: { ...agent, sandbox: "read-only" },
|
|
207
|
+
task,
|
|
208
|
+
cwd: ctx.cwd,
|
|
209
|
+
ctx,
|
|
210
|
+
timeout: timeoutMs,
|
|
211
|
+
signal,
|
|
212
|
+
readOnly: true,
|
|
213
|
+
onMessage: onProgress,
|
|
214
|
+
onProgress: onActivity,
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// agent_settled glue (extracted for testability — dispatch/isRunning injectable)
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
export interface AutoReviewSettleDeps {
|
|
224
|
+
pi: ExtensionAPI;
|
|
225
|
+
ctx: ExtensionContext;
|
|
226
|
+
state: AutoReviewState;
|
|
227
|
+
bundledAgentsDir: string;
|
|
228
|
+
threadStore: typeof ThreadStoreType;
|
|
229
|
+
/** Theme color for the reviewer thread (index.ts owns the name→color map). */
|
|
230
|
+
agentColor: string | undefined;
|
|
231
|
+
/** startBackgroundTask; injectable so tests can capture dispatches. */
|
|
232
|
+
dispatch: typeof startBackgroundTask;
|
|
233
|
+
/** True when any background task is running; injectable for tests. */
|
|
234
|
+
isRunning: () => boolean;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** One agent_settled tick: analyze the settled turn and maybe dispatch the reviewer. Returns true when a review was dispatched. */
|
|
238
|
+
export async function handleAutoReviewSettle(deps: AutoReviewSettleDeps): Promise<boolean> {
|
|
239
|
+
const { ctx, state } = deps;
|
|
240
|
+
const debug = process.env.PI_SUBAGENT_AUTOREVIEW_DEBUG === "1";
|
|
241
|
+
try {
|
|
242
|
+
const entries = ctx.sessionManager.getEntries() as any[];
|
|
243
|
+
if (!readAutoReviewEnabled(ctx)) {
|
|
244
|
+
// Keep the cursor fresh while disabled so enabling mid-session never
|
|
245
|
+
// replays accumulated history as one pseudo-turn.
|
|
246
|
+
state.cursor = latestEntryId(entries);
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
249
|
+
// Headless one-shot modes can't surface the follow-up turn and shouldn't
|
|
250
|
+
// linger on a detached reviewer — track the cursor, dispatch nothing.
|
|
251
|
+
if (ctx.mode !== "tui") {
|
|
252
|
+
state.cursor = latestEntryId(entries);
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
const analysis = analyzeTurn(entries, state.cursor);
|
|
256
|
+
// Write back the transcript tail unconditionally: if the cursor entry was
|
|
257
|
+
// dropped (compaction/pruning), counting never flips and analyzeTurn would
|
|
258
|
+
// return the stale id forever — a silent permanent disable (pi-advisor
|
|
259
|
+
// reseeds to latestEntryId for the same reason).
|
|
260
|
+
state.cursor = latestEntryId(entries);
|
|
261
|
+
if (debug) console.error(`[auto-review] mutations=${analysis.mutationCount} userInitiated=${analysis.userInitiated} dispatched=${state.dispatched}`);
|
|
262
|
+
if (!analysis.userInitiated) return false; // advisor steers / custom wake-ups must not loop
|
|
263
|
+
if (analysis.mutationCount < MIN_MUTATIONS) return false; // trivial edits stay un-reviewed
|
|
264
|
+
if (analysis.files.length === 0) return false; // delete-only/mutation-without-path turns have nothing to scope
|
|
265
|
+
if (state.dispatched >= MAX_PER_SESSION) return false;
|
|
266
|
+
if (deps.isRunning()) return false;
|
|
267
|
+
const { diff, status } = await captureDiff(ctx.cwd, analysis.files);
|
|
268
|
+
deps.dispatch({
|
|
269
|
+
agent: "reviewer",
|
|
270
|
+
task: buildReviewTask(analysis.files, diff, status),
|
|
271
|
+
timeout: REVIEW_TIMEOUT_MS,
|
|
272
|
+
agentColor: deps.agentColor,
|
|
273
|
+
deps: { pi: deps.pi, ctx, runOne: makeHookRunOne(deps.bundledAgentsDir, ctx), threadStore: deps.threadStore },
|
|
274
|
+
});
|
|
275
|
+
state.dispatched++;
|
|
276
|
+
if (debug) console.error("[auto-review] reviewer dispatched (background)");
|
|
277
|
+
return true;
|
|
278
|
+
} catch {
|
|
279
|
+
// Auto-review must never break the main loop (same contract as pi-advisor's watcher).
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -64,6 +64,11 @@ import { type SubagentThread, threadStore } from "./threads.ts";
|
|
|
64
64
|
import { SUBAGENT_REQUEST_EVENT, runNamedAgent, type SubagentRunRequest } from "./service.ts";
|
|
65
65
|
import { resolveModel, runWithModelFallback } from "./model.ts";
|
|
66
66
|
import { DEFAULT_ROLES, describeAgentModels, readSubagentRoles, readSubagentRolesGlobal, resolveAgentModelChain, type RolesConfig } from "./roles.ts";
|
|
67
|
+
import {
|
|
68
|
+
createAutoReviewState,
|
|
69
|
+
handleAutoReviewSettle,
|
|
70
|
+
latestEntryId,
|
|
71
|
+
} from "./auto-review.ts";
|
|
67
72
|
import { ThreadViewer, type ThreadViewerCallbacks } from "./thread-viewer.ts";
|
|
68
73
|
import { createTaskWidgetController, renderLiveThreadLine, type TaskWidgetController } from "./widget.ts";
|
|
69
74
|
import {
|
|
@@ -185,6 +190,7 @@ interface SubagentDetails {
|
|
|
185
190
|
|
|
186
191
|
export default function (pi: ExtensionAPI) {
|
|
187
192
|
let currentCtx: ExtensionContext | undefined;
|
|
193
|
+
let autoReviewState = createAutoReviewState();
|
|
188
194
|
|
|
189
195
|
// Live progress widget — fed by threadStore subscriptions (per SDK event).
|
|
190
196
|
const widget: TaskWidgetController = createTaskWidgetController(
|
|
@@ -198,6 +204,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
198
204
|
if (event.reason === "reload") invalidateAgentCache();
|
|
199
205
|
threadStore.clear();
|
|
200
206
|
trustedProjectAgentDirs.clear();
|
|
207
|
+
autoReviewState = createAutoReviewState();
|
|
208
|
+
// Reseed before any turn runs so the session's FIRST coding turn is
|
|
209
|
+
// reviewed too (reseed-on-first-settle would silently consume it).
|
|
210
|
+
try {
|
|
211
|
+
if (ctx) autoReviewState.cursor = latestEntryId(ctx.sessionManager.getEntries() as any[]);
|
|
212
|
+
} catch { /* no session yet — analyzeTurn handles undefined cursor */ }
|
|
201
213
|
// Clear any widget from a prior session.
|
|
202
214
|
widget.clearWidgetIfIdle();
|
|
203
215
|
// Mark prior-session running tasks as interrupted (we can't resume them),
|
|
@@ -252,6 +264,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
252
264
|
"Prefer **scout** and **tester** for cheap routine work. " +
|
|
253
265
|
"Prefer **worker** or **general-purpose** for normal coding. " +
|
|
254
266
|
"Prefer **planner** and **reviewer** for consequential reasoning. " +
|
|
267
|
+
"Delegate only when isolation/parallelism/specialization pays off — do NOT delegate single-file small edits or quick greps; do those inline. " +
|
|
255
268
|
"Modes: single, parallel (max 8 tasks, 4 concurrent), chain.",
|
|
256
269
|
};
|
|
257
270
|
});
|
|
@@ -298,6 +311,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
298
311
|
});
|
|
299
312
|
});
|
|
300
313
|
|
|
314
|
+
// Auto-review: after a user-initiated turn that mutated files, dispatch the
|
|
315
|
+
// read-only reviewer on the current uncommitted diff of the turn's touched
|
|
316
|
+
// files as a background task. Config: `subagent.autoReview`.
|
|
317
|
+
pi.on("agent_settled", async (_event, ctx) => {
|
|
318
|
+
if (!ctx) return;
|
|
319
|
+
const dispatched = await handleAutoReviewSettle({
|
|
320
|
+
pi,
|
|
321
|
+
ctx,
|
|
322
|
+
state: autoReviewState,
|
|
323
|
+
bundledAgentsDir,
|
|
324
|
+
threadStore,
|
|
325
|
+
agentColor: agentToThemeColor("reviewer"),
|
|
326
|
+
dispatch: startBackgroundTask,
|
|
327
|
+
isRunning: () => getAllBackgroundTasks().some((t) => t.status === "running"),
|
|
328
|
+
});
|
|
329
|
+
if (dispatched && ctx.mode === "tui") widget.ensureWidget(ctx);
|
|
330
|
+
});
|
|
331
|
+
|
|
301
332
|
// Register renderer for background-task completion (follow-up turn).
|
|
302
333
|
pi.registerMessageRenderer?.("pi-subagent-complete", (message, _opts, theme) => {
|
|
303
334
|
const d = (message.details ?? {}) as {
|
package/extensions/roles.ts
CHANGED
|
@@ -140,6 +140,16 @@ export function readSubagentRoles(ctx?: ExtensionContext): RolesConfig {
|
|
|
140
140
|
return cfg;
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
/** Read the `subagent` section of the user's global settings.json (empty when absent). */
|
|
144
|
+
export function readSubagentSection(): Record<string, unknown> {
|
|
145
|
+
return readSubagentSectionFrom(join(agentDir(), "settings.json"));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Read the `subagent` section of an arbitrary settings.json (empty when absent). */
|
|
149
|
+
export function readSubagentSectionFrom(settingsPath: string): Record<string, unknown> {
|
|
150
|
+
return subagentSection(readJson(settingsPath));
|
|
151
|
+
}
|
|
152
|
+
|
|
143
153
|
/** Global-only variant used by the panel so a save never persists repo
|
|
144
154
|
* `.pi/settings.json` overlay values into the user's global settings. */
|
|
145
155
|
export function readSubagentRolesGlobal(): RolesConfig {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "In-process subagents for Pi with isolated SDK sessions, parallel and chained delegation, and inspectable threads.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"CHANGELOG.md",
|
|
33
33
|
"agents/",
|
|
34
34
|
"extensions/index.ts",
|
|
35
|
+
"extensions/auto-review.ts",
|
|
35
36
|
"extensions/agents.ts",
|
|
36
37
|
"extensions/model.ts",
|
|
37
38
|
"extensions/roles.ts",
|
|
@@ -62,20 +63,20 @@
|
|
|
62
63
|
"check": "npm run typecheck && npm test"
|
|
63
64
|
},
|
|
64
65
|
"peerDependencies": {
|
|
65
|
-
"@earendil-works/pi-agent-core": ">=0.80.0 <0.
|
|
66
|
-
"@earendil-works/pi-ai": ">=0.80.0 <0.
|
|
67
|
-
"@earendil-works/pi-coding-agent": ">=0.80.0 <0.
|
|
68
|
-
"@earendil-works/pi-tui": ">=0.80.0 <0.
|
|
66
|
+
"@earendil-works/pi-agent-core": ">=0.80.0 <0.86.0",
|
|
67
|
+
"@earendil-works/pi-ai": ">=0.80.0 <0.86.0",
|
|
68
|
+
"@earendil-works/pi-coding-agent": ">=0.80.0 <0.86.0",
|
|
69
|
+
"@earendil-works/pi-tui": ">=0.80.0 <0.86.0",
|
|
69
70
|
"typebox": ">=1.3.0 <2.0.0"
|
|
70
71
|
},
|
|
71
72
|
"dependencies": {
|
|
72
73
|
"@bacnh85/pi-config-panel": "^0.1.0"
|
|
73
74
|
},
|
|
74
75
|
"devDependencies": {
|
|
75
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
76
|
-
"@earendil-works/pi-ai": "^0.
|
|
77
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
78
|
-
"@earendil-works/pi-tui": "^0.
|
|
76
|
+
"@earendil-works/pi-agent-core": "^0.85.0",
|
|
77
|
+
"@earendil-works/pi-ai": "^0.85.0",
|
|
78
|
+
"@earendil-works/pi-coding-agent": "^0.85.0",
|
|
79
|
+
"@earendil-works/pi-tui": "^0.85.0",
|
|
79
80
|
"@types/mocha": "^10.0.10",
|
|
80
81
|
"@types/node": "^20.19.43",
|
|
81
82
|
"mocha": "^11.8.0",
|