@ferris1225/pi-subagents 0.7.0 → 0.9.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 +23 -9
- package/agents/explore.md +1 -1
- package/package.json +3 -3
- package/src/background.ts +11 -1
- package/src/config.ts +99 -15
- package/src/index.ts +64 -19
- package/src/monitor.ts +26 -1
- package/src/prompt.ts +0 -1
- package/src/setup.ts +68 -1
- package/src/spawn.ts +5 -4
- package/agents/plan.md +0 -41
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@ferris1225/pi-subagents)
|
|
5
5
|
[](./LICENSE)
|
|
6
6
|

|
|
7
|
-

|
|
8
8
|
|
|
9
9
|
Focused background delegation for [pi](https://pi.dev). `pi-subagents` adds a small set of
|
|
10
10
|
specialized agents that run in isolated child processes, report results back to the main
|
|
@@ -21,8 +21,8 @@ agent, and keep the workflow moving without manual polling.
|
|
|
21
21
|
- **Parallel fan-out** — run independent tasks together, with a bounded background queue.
|
|
22
22
|
- **Live progress** — a TUI widget shows each agent's status, activity, model, usage, and
|
|
23
23
|
elapsed time; completion also produces a concise notification.
|
|
24
|
-
- **Per-agent configuration** — enable agents, select models, set thinking strength,
|
|
25
|
-
choose discovery scope from `/subagents-setup`.
|
|
24
|
+
- **Per-agent configuration** — enable agents, select models, set thinking strength, tune
|
|
25
|
+
concurrency limits, and choose discovery scope from `/subagents-setup`.
|
|
26
26
|
- **Leaf processes** — child agents cannot access the `subagent` tool, so delegation cannot
|
|
27
27
|
recurse.
|
|
28
28
|
|
|
@@ -40,8 +40,7 @@ After installation, open the setup wizard in an interactive TUI session:
|
|
|
40
40
|
/subagents-setup
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
The default configuration enables `explore`, `worker`, and `reviewer`.
|
|
44
|
-
but opt-in.
|
|
43
|
+
The default configuration enables `explore`, `worker`, and `reviewer`.
|
|
45
44
|
|
|
46
45
|
## Included agents
|
|
47
46
|
|
|
@@ -50,7 +49,6 @@ but opt-in.
|
|
|
50
49
|
| `explore` | Yes | Read-only | Fast codebase reconnaissance and structured findings. |
|
|
51
50
|
| `worker` | Yes | Full | Implements, fixes, refactors, and tests a self-contained task. |
|
|
52
51
|
| `reviewer` | Yes | Read-only | Independent adversarial review of a diff before completion. |
|
|
53
|
-
| `plan` | No | Read-only | Produces a separate implementation plan when one is useful. |
|
|
54
52
|
|
|
55
53
|
Agents are Markdown files in `agents/`. Each file contains YAML frontmatter and a system
|
|
56
54
|
prompt. User and project scopes can override a built-in agent with the same name.
|
|
@@ -72,8 +70,9 @@ main agent
|
|
|
72
70
|
1. The main agent calls `subagent` with a self-contained brief.
|
|
73
71
|
2. The tool returns immediately and ends that foreground tool turn, leaving the editor ready
|
|
74
72
|
for input.
|
|
75
|
-
3. The child process works independently.
|
|
76
|
-
parallel request may contain up to eight tasks
|
|
73
|
+
3. The child process works independently. By default up to four queued runs execute at
|
|
74
|
+
once and a single parallel request may contain up to eight tasks; both limits are
|
|
75
|
+
configurable (`maxConcurrency` / `maxParallelTasks`).
|
|
77
76
|
4. On completion or failure, the extension sends a durable result message to the main
|
|
78
77
|
session. That message automatically wakes the main agent, or waits until its current turn
|
|
79
78
|
finishes.
|
|
@@ -131,7 +130,10 @@ Configuration is stored at `~/.pi/agent/pi-subagents.json`. The location follows
|
|
|
131
130
|
},
|
|
132
131
|
"thinkingLevel": "max",
|
|
133
132
|
"proactiveInjection": true,
|
|
134
|
-
"agentScope": "user"
|
|
133
|
+
"agentScope": "user",
|
|
134
|
+
"maxConcurrency": 4,
|
|
135
|
+
"maxParallelTasks": 8,
|
|
136
|
+
"maxSubagentDepth": 1
|
|
135
137
|
}
|
|
136
138
|
```
|
|
137
139
|
|
|
@@ -142,6 +144,18 @@ Configuration is stored at `~/.pi/agent/pi-subagents.json`. The location follows
|
|
|
142
144
|
| `thinkingLevel` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, or `max`. |
|
|
143
145
|
| `proactiveInjection` | Whether to add the delegation directive to the main system prompt. |
|
|
144
146
|
| `agentScope` | `user`, `project`, or `both`; controls which user/project agent directories are discovered. |
|
|
147
|
+
| `maxConcurrency` | How many sub-agent processes run at once (1–16, default 4). Extra work waits in the queue. |
|
|
148
|
+
| `maxParallelTasks` | Maximum tasks accepted by one parallel `subagent` call (1–32, default 8). |
|
|
149
|
+
| `maxSubagentDepth` | Depth at which the `subagent` tool is no longer registered (default 1: the main session delegates, children are leaf processes). `0` disables the tool entirely. Read once at extension load. |
|
|
150
|
+
|
|
151
|
+
### Configuration migration
|
|
152
|
+
|
|
153
|
+
The config file migrates itself on load — no manual steps after an upgrade:
|
|
154
|
+
|
|
155
|
+
- **Schema upgrades** — a config written by an older version (missing newer keys or
|
|
156
|
+
holding invalid values) is normalized and saved back with the new fields filled in.
|
|
157
|
+
- **Removed agents** — agents no longer shipped (e.g. the old `plan` agent) are stripped
|
|
158
|
+
from `enabledAgents` and `agentModels` automatically.
|
|
145
159
|
|
|
146
160
|
Model selection uses this precedence:
|
|
147
161
|
|
package/agents/explore.md
CHANGED
|
@@ -26,7 +26,7 @@ You are an explore agent: a fast, read-only reconnaissance specialist. You inves
|
|
|
26
26
|
- Thorough: trace dependencies across modules; check tests and types.
|
|
27
27
|
|
|
28
28
|
## Collaboration
|
|
29
|
-
- Your output feeds `
|
|
29
|
+
- Your output feeds `worker` (or the main agent directly). Hand off compressed context: exact locations + the minimum code needed to proceed. Flag anything ambiguous so the caller can decide.
|
|
30
30
|
|
|
31
31
|
## Output format
|
|
32
32
|
## Files Retrieved
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ferris1225/pi-subagents",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Focused sub-agent delegation for pi: explore /
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Focused sub-agent delegation for pi: explore / worker / reviewer agents in isolated context, with proactive dispatch injection and per-agent model selection.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"publishConfig": {
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"@earendil-works/pi-coding-agent": "^0.83.0",
|
|
44
44
|
"@earendil-works/pi-tui": "^0.83.0",
|
|
45
45
|
"@types/node": "^22.10.0",
|
|
46
|
-
"typebox": "^1.3.
|
|
46
|
+
"typebox": "^1.3.9",
|
|
47
47
|
"typescript": "^5.9.0",
|
|
48
48
|
"vitest": "^4.1.0"
|
|
49
49
|
},
|
package/src/background.ts
CHANGED
|
@@ -14,7 +14,7 @@ interface PendingTask {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export class BackgroundTaskQueue {
|
|
17
|
-
private
|
|
17
|
+
private concurrency: number;
|
|
18
18
|
private readonly pending: PendingTask[] = [];
|
|
19
19
|
private readonly active = new Set<AbortController>();
|
|
20
20
|
private stopped = false;
|
|
@@ -23,6 +23,16 @@ export class BackgroundTaskQueue {
|
|
|
23
23
|
this.concurrency = Math.max(1, concurrency);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Update the concurrency limit (e.g. after a config change). Raising it
|
|
28
|
+
* immediately starts more queued work; lowering it takes effect as running
|
|
29
|
+
* tasks finish — already-running tasks are never interrupted.
|
|
30
|
+
*/
|
|
31
|
+
setConcurrency(concurrency: number): void {
|
|
32
|
+
this.concurrency = Math.max(1, concurrency);
|
|
33
|
+
this.drain();
|
|
34
|
+
}
|
|
35
|
+
|
|
26
36
|
enqueue(task: BackgroundTask, onCancelled?: () => void): AbortController {
|
|
27
37
|
const controller = new AbortController();
|
|
28
38
|
if (this.stopped) {
|
package/src/config.ts
CHANGED
|
@@ -5,19 +5,31 @@
|
|
|
5
5
|
* and honors PI_CODING_AGENT_DIR). Parsing is defensive: invalid fields fall back
|
|
6
6
|
* to defaults instead of throwing, so a hand-edited or partially-written file can
|
|
7
7
|
* never break the extension at runtime.
|
|
8
|
+
*
|
|
9
|
+
* Schema upgrades happen transparently on load: a config written by an older
|
|
10
|
+
* version (missing newer keys, holding removed agents, or containing invalid
|
|
11
|
+
* values) is normalized and persisted back with the new fields filled in.
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
14
|
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
15
|
+
import { readFileSync } from "node:fs";
|
|
11
16
|
import { dirname, join } from "node:path";
|
|
12
17
|
import { getAgentDir, withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
13
18
|
|
|
14
19
|
/** Full catalog of agents shipped with the package (selectable in /subagents-setup). */
|
|
15
|
-
export const BUILTIN_AGENT_NAMES = ["explore", "
|
|
20
|
+
export const BUILTIN_AGENT_NAMES = ["explore", "worker", "reviewer"] as const;
|
|
16
21
|
export type BuiltinAgentName = (typeof BUILTIN_AGENT_NAMES)[number];
|
|
17
22
|
|
|
18
|
-
/** Agents enabled out of the box.
|
|
23
|
+
/** Agents enabled out of the box. */
|
|
19
24
|
export const DEFAULT_ENABLED_AGENTS: readonly string[] = ["explore", "worker", "reviewer"];
|
|
20
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Agents that used to ship but were removed. normalizeConfig strips them from
|
|
28
|
+
* enabledAgents/agentModels so upgraded installs clean their config automatically
|
|
29
|
+
* (the schema-upgrade save in loadConfig then persists the cleanup).
|
|
30
|
+
*/
|
|
31
|
+
export const REMOVED_AGENT_NAMES: readonly string[] = ["plan"];
|
|
32
|
+
|
|
21
33
|
export const AGENT_SCOPE_VALUES = ["user", "project", "both"] as const;
|
|
22
34
|
export type AgentScope = (typeof AGENT_SCOPE_VALUES)[number];
|
|
23
35
|
|
|
@@ -28,6 +40,22 @@ export const DEFAULT_THINKING_LEVEL: ThinkingLevel = "max";
|
|
|
28
40
|
|
|
29
41
|
export const CONFIG_FILE_NAME = "pi-subagents.json";
|
|
30
42
|
|
|
43
|
+
/** How many sub-agent processes may run at once. Default: 4. */
|
|
44
|
+
export const DEFAULT_MAX_CONCURRENCY = 4;
|
|
45
|
+
/** Upper bound accepted for maxConcurrency (defensive clamp). */
|
|
46
|
+
export const MAX_CONCURRENCY_LIMIT = 16;
|
|
47
|
+
/** How many tasks a single parallel `subagent` call may contain. Default: 8. */
|
|
48
|
+
export const DEFAULT_MAX_PARALLEL_TASKS = 8;
|
|
49
|
+
/** Upper bound accepted for maxParallelTasks (defensive clamp). */
|
|
50
|
+
export const MAX_PARALLEL_TASKS_LIMIT = 32;
|
|
51
|
+
/**
|
|
52
|
+
* Depth at which the subagent tool stops being available. 1 = the main session
|
|
53
|
+
* delegates and child processes are leaves; 0 disables the tool entirely.
|
|
54
|
+
*/
|
|
55
|
+
export const DEFAULT_MAX_SUBAGENT_DEPTH = 1;
|
|
56
|
+
/** Upper bound accepted for maxSubagentDepth (defensive clamp). */
|
|
57
|
+
export const MAX_SUBAGENT_DEPTH_LIMIT = 4;
|
|
58
|
+
|
|
31
59
|
export interface SubagentsConfig {
|
|
32
60
|
/** Agent names that are discoverable and injected. Default: explore, worker, reviewer. */
|
|
33
61
|
enabledAgents: string[];
|
|
@@ -39,6 +67,12 @@ export interface SubagentsConfig {
|
|
|
39
67
|
proactiveInjection: boolean;
|
|
40
68
|
/** Which agent directories to discover from. Default: "user". */
|
|
41
69
|
agentScope: AgentScope;
|
|
70
|
+
/** Max sub-agent processes running at once (extra work queues). Default: 4. */
|
|
71
|
+
maxConcurrency: number;
|
|
72
|
+
/** Max tasks accepted by one parallel `subagent` call. Default: 8. */
|
|
73
|
+
maxParallelTasks: number;
|
|
74
|
+
/** Depth at which the subagent tool is no longer registered. Default: 1. */
|
|
75
|
+
maxSubagentDepth: number;
|
|
42
76
|
}
|
|
43
77
|
|
|
44
78
|
export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
@@ -47,6 +81,9 @@ export const DEFAULT_CONFIG: SubagentsConfig = {
|
|
|
47
81
|
thinkingLevel: DEFAULT_THINKING_LEVEL,
|
|
48
82
|
proactiveInjection: true,
|
|
49
83
|
agentScope: "user",
|
|
84
|
+
maxConcurrency: DEFAULT_MAX_CONCURRENCY,
|
|
85
|
+
maxParallelTasks: DEFAULT_MAX_PARALLEL_TASKS,
|
|
86
|
+
maxSubagentDepth: DEFAULT_MAX_SUBAGENT_DEPTH,
|
|
50
87
|
};
|
|
51
88
|
|
|
52
89
|
export function getConfigPath(agentDir: string = getAgentDir()): string {
|
|
@@ -68,6 +105,12 @@ function isModelReference(value: unknown): value is string {
|
|
|
68
105
|
return slash > 0 && slash < normalized.length - 1 && !/\s/u.test(normalized);
|
|
69
106
|
}
|
|
70
107
|
|
|
108
|
+
/** Clamp a raw value to a positive integer within [1, upper]; undefined when invalid. */
|
|
109
|
+
function clampCount(value: unknown, upper: number): number | undefined {
|
|
110
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
111
|
+
return Math.max(1, Math.min(upper, Math.round(value)));
|
|
112
|
+
}
|
|
113
|
+
|
|
71
114
|
/**
|
|
72
115
|
* Merge a raw parsed JSON value over the defaults, dropping invalid fields.
|
|
73
116
|
* Exported for tests.
|
|
@@ -81,6 +124,9 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
81
124
|
thinkingLevel: DEFAULT_CONFIG.thinkingLevel,
|
|
82
125
|
proactiveInjection: DEFAULT_CONFIG.proactiveInjection,
|
|
83
126
|
agentScope: DEFAULT_CONFIG.agentScope,
|
|
127
|
+
maxConcurrency: DEFAULT_CONFIG.maxConcurrency,
|
|
128
|
+
maxParallelTasks: DEFAULT_CONFIG.maxParallelTasks,
|
|
129
|
+
maxSubagentDepth: DEFAULT_CONFIG.maxSubagentDepth,
|
|
84
130
|
};
|
|
85
131
|
|
|
86
132
|
if (Array.isArray(raw.enabledAgents)) {
|
|
@@ -88,11 +134,15 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
88
134
|
(name): name is string => typeof name === "string" && name.trim().length > 0,
|
|
89
135
|
);
|
|
90
136
|
// An explicitly empty array is honored (disables all agents); otherwise keep valid names.
|
|
91
|
-
|
|
137
|
+
// Agents removed from the package (e.g. plan) are stripped from upgraded configs.
|
|
138
|
+
config.enabledAgents = [...new Set(names.map((name) => name.trim()))].filter(
|
|
139
|
+
(name) => !REMOVED_AGENT_NAMES.includes(name),
|
|
140
|
+
);
|
|
92
141
|
}
|
|
93
142
|
|
|
94
143
|
if (isRecord(raw.agentModels)) {
|
|
95
144
|
for (const [key, value] of Object.entries(raw.agentModels)) {
|
|
145
|
+
if (REMOVED_AGENT_NAMES.includes(key.trim())) continue;
|
|
96
146
|
if (isModelReference(value)) config.agentModels[key.trim()] = value.trim();
|
|
97
147
|
}
|
|
98
148
|
}
|
|
@@ -109,33 +159,71 @@ export function normalizeConfig(raw: unknown): SubagentsConfig {
|
|
|
109
159
|
config.agentScope = raw.agentScope;
|
|
110
160
|
}
|
|
111
161
|
|
|
162
|
+
const maxConcurrency = clampCount(raw.maxConcurrency, MAX_CONCURRENCY_LIMIT);
|
|
163
|
+
if (maxConcurrency !== undefined) config.maxConcurrency = maxConcurrency;
|
|
164
|
+
|
|
165
|
+
const maxParallelTasks = clampCount(raw.maxParallelTasks, MAX_PARALLEL_TASKS_LIMIT);
|
|
166
|
+
if (maxParallelTasks !== undefined) config.maxParallelTasks = maxParallelTasks;
|
|
167
|
+
|
|
168
|
+
// 0 is meaningful here (disables the tool), so clamp to [0, limit] instead.
|
|
169
|
+
if (typeof raw.maxSubagentDepth === "number" && Number.isFinite(raw.maxSubagentDepth)) {
|
|
170
|
+
config.maxSubagentDepth = Math.max(0, Math.min(MAX_SUBAGENT_DEPTH_LIMIT, Math.round(raw.maxSubagentDepth)));
|
|
171
|
+
}
|
|
172
|
+
|
|
112
173
|
return config;
|
|
113
174
|
}
|
|
114
175
|
|
|
176
|
+
function defaultConfig(): SubagentsConfig {
|
|
177
|
+
return { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_CONFIG.enabledAgents], agentModels: {} };
|
|
178
|
+
}
|
|
179
|
+
|
|
115
180
|
/**
|
|
116
181
|
* Load config. A missing file is a normal state and yields the defaults (not an error).
|
|
117
182
|
* A corrupt file also falls back to defaults rather than throwing, so startup never breaks.
|
|
183
|
+
* A file from an older version (missing newer keys or holding removed agents) is
|
|
184
|
+
* normalized and persisted back, so the on-disk config stays current.
|
|
118
185
|
*/
|
|
119
186
|
export async function loadConfig(configPath: string = getConfigPath()): Promise<SubagentsConfig> {
|
|
120
187
|
let text: string;
|
|
121
188
|
try {
|
|
122
189
|
text = await readFile(configPath, "utf8");
|
|
123
|
-
} catch
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
// Unreadable for another reason: fall back to defaults but do not crash startup.
|
|
128
|
-
return { ...DEFAULT_CONFIG, enabledAgents: [...DEFAULT_CONFIG.enabledAgents] };
|
|
190
|
+
} catch {
|
|
191
|
+
// Missing or unreadable: fall back to defaults but do not crash startup.
|
|
192
|
+
return defaultConfig();
|
|
129
193
|
}
|
|
130
194
|
|
|
131
195
|
let parsed: unknown;
|
|
132
196
|
try {
|
|
133
197
|
parsed = JSON.parse(text);
|
|
134
198
|
} catch {
|
|
135
|
-
return
|
|
199
|
+
return defaultConfig();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const config = normalizeConfig(parsed);
|
|
203
|
+
|
|
204
|
+
// Schema upgrade: persist the normalized shape when the file gained fields
|
|
205
|
+
// (new version) or dropped invalid/removed ones.
|
|
206
|
+
if (JSON.stringify(config) !== JSON.stringify(parsed)) {
|
|
207
|
+
try {
|
|
208
|
+
await saveConfig(config, configPath);
|
|
209
|
+
} catch {
|
|
210
|
+
// Non-fatal: keep the in-memory config for this run.
|
|
211
|
+
}
|
|
136
212
|
}
|
|
137
213
|
|
|
138
|
-
return
|
|
214
|
+
return config;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Synchronous load for the extension's init-time decisions (e.g. the recursion
|
|
219
|
+
* guard). Runs before any async context is available; never migrates or saves.
|
|
220
|
+
*/
|
|
221
|
+
export function loadConfigSync(configPath: string = getConfigPath()): SubagentsConfig {
|
|
222
|
+
try {
|
|
223
|
+
return normalizeConfig(JSON.parse(readFileSync(configPath, "utf8")));
|
|
224
|
+
} catch {
|
|
225
|
+
return defaultConfig();
|
|
226
|
+
}
|
|
139
227
|
}
|
|
140
228
|
|
|
141
229
|
/**
|
|
@@ -159,10 +247,6 @@ export async function saveConfig(
|
|
|
159
247
|
});
|
|
160
248
|
}
|
|
161
249
|
|
|
162
|
-
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
163
|
-
return error instanceof Error;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
250
|
export function errorMessage(error: unknown): string {
|
|
167
251
|
return error instanceof Error ? error.message : String(error);
|
|
168
252
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* pi-subagents — focused sub-agent delegation for pi.
|
|
3
3
|
*
|
|
4
4
|
* Registers:
|
|
5
|
-
* - a `subagent` tool that runs explore/
|
|
5
|
+
* - a `subagent` tool that runs explore/worker/reviewer agents as isolated
|
|
6
6
|
* `pi` child processes (single or parallel),
|
|
7
7
|
* - a `/subagents-setup` command for selection-only configuration,
|
|
8
8
|
* - a `before_agent_start` hook that injects a delegation directive into the
|
|
@@ -18,14 +18,11 @@ import { Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
|
18
18
|
import { Type } from "typebox";
|
|
19
19
|
import { discoverAgents, type AgentConfig } from "./agents.ts";
|
|
20
20
|
import { BackgroundTaskQueue } from "./background.ts";
|
|
21
|
-
import { getConfigPath, loadConfig, saveConfig } from "./config.ts";
|
|
21
|
+
import { getConfigPath, loadConfig, loadConfigSync, saveConfig } from "./config.ts";
|
|
22
22
|
import { repairUnavailableModelOverrides } from "./models.ts";
|
|
23
23
|
import { buildDelegationDirective } from "./prompt.ts";
|
|
24
24
|
import { runSetup } from "./setup.ts";
|
|
25
25
|
import {
|
|
26
|
-
MAX_CONCURRENCY,
|
|
27
|
-
MAX_PARALLEL_TASKS,
|
|
28
|
-
MAX_SUBAGENT_DEPTH,
|
|
29
26
|
currentSubagentDepth,
|
|
30
27
|
getFinalOutput,
|
|
31
28
|
getResultOutput,
|
|
@@ -36,17 +33,24 @@ import {
|
|
|
36
33
|
type SubagentLiveEvent,
|
|
37
34
|
type UsageStats,
|
|
38
35
|
} from "./spawn.ts";
|
|
39
|
-
import { formatToolActivity, monitor, statusColor, statusIcon, statusLabel } from "./monitor.ts";
|
|
36
|
+
import { formatTaskSummary, formatToolActivity, monitor, statusColor, statusIcon, statusLabel } from "./monitor.ts";
|
|
37
|
+
|
|
38
|
+
const NON_BLANK_TASK_OPTIONS = { minLength: 1, pattern: "\\S" } as const;
|
|
40
39
|
|
|
41
40
|
const TaskItem = Type.Object({
|
|
42
41
|
agent: Type.String({ description: "Name of the agent to invoke" }),
|
|
43
|
-
task: Type.String({
|
|
42
|
+
task: Type.String({
|
|
43
|
+
...NON_BLANK_TASK_OPTIONS,
|
|
44
|
+
description: "Self-contained task to delegate (the agent has no memory of this conversation)",
|
|
45
|
+
}),
|
|
44
46
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
45
47
|
});
|
|
46
48
|
|
|
47
49
|
const SubagentParams = Type.Object({
|
|
48
50
|
agent: Type.Optional(Type.String({ description: "Name of the agent to invoke (single mode)" })),
|
|
49
|
-
task: Type.Optional(
|
|
51
|
+
task: Type.Optional(
|
|
52
|
+
Type.String({ ...NON_BLANK_TASK_OPTIONS, description: "Self-contained task to delegate (single mode)" }),
|
|
53
|
+
),
|
|
50
54
|
tasks: Type.Optional(Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" })),
|
|
51
55
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
|
|
52
56
|
});
|
|
@@ -112,15 +116,23 @@ function formatUsage(usage: UsageStats): string {
|
|
|
112
116
|
|
|
113
117
|
export default function (pi: ExtensionAPI): void {
|
|
114
118
|
const configPath = getConfigPath(getAgentDir());
|
|
115
|
-
|
|
119
|
+
// Init-time decisions need the config synchronously; the full (migrating)
|
|
120
|
+
// async load runs per tool call.
|
|
121
|
+
const initialConfig = loadConfigSync(configPath);
|
|
122
|
+
const backgroundQueue = new BackgroundTaskQueue(initialConfig.maxConcurrency);
|
|
116
123
|
let sessionActive = true;
|
|
117
124
|
|
|
118
|
-
// Recursion guard:
|
|
119
|
-
|
|
125
|
+
// Recursion guard: sub-agents at the configured depth are leaf processes and
|
|
126
|
+
// cannot delegate again. maxSubagentDepth 0 disables the tool entirely.
|
|
127
|
+
if (currentSubagentDepth() >= initialConfig.maxSubagentDepth) {
|
|
128
|
+
const reason =
|
|
129
|
+
initialConfig.maxSubagentDepth === 0
|
|
130
|
+
? "disabled by maxSubagentDepth 0 in pi-subagents.json"
|
|
131
|
+
: "disabled in nested sub-agent processes";
|
|
120
132
|
pi.registerCommand("subagents-setup", {
|
|
121
|
-
description:
|
|
133
|
+
description: `Configure pi-subagents (${reason})`,
|
|
122
134
|
handler: async (_args, ctx) => {
|
|
123
|
-
ctx.ui.notify(
|
|
135
|
+
ctx.ui.notify(`pi-subagents setup is unavailable here (${reason}).`, "warning");
|
|
124
136
|
},
|
|
125
137
|
});
|
|
126
138
|
return;
|
|
@@ -144,7 +156,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
144
156
|
label: "Subagent",
|
|
145
157
|
description: [
|
|
146
158
|
"Delegate a discrete, self-contained task to a specialized sub-agent running in an ISOLATED context window.",
|
|
147
|
-
"Agents: explore (read-only codebase recon),
|
|
159
|
+
"Agents: explore (read-only codebase recon), worker (implement/fix/refactor/test, full tools), reviewer (adversarial pre-commit review, read-only).",
|
|
148
160
|
"Modes: single ({agent, task}) or parallel ({tasks: [{agent, task}, ...]}).",
|
|
149
161
|
"It starts agents in the background and immediately returns control to the main window; completion messages automatically wake the main agent to continue.",
|
|
150
162
|
"Each agent has no memory of this conversation — brief it fully (goal, exact paths, constraints, expected output)."
|
|
@@ -164,6 +176,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
164
176
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
165
177
|
monitor.beginTurn();
|
|
166
178
|
let config = await loadConfig(configPath);
|
|
179
|
+
// Pick up concurrency changes from /subagents-setup without a restart.
|
|
180
|
+
backgroundQueue.setConcurrency(config.maxConcurrency);
|
|
167
181
|
const repairedModels = repairUnavailableModelOverrides(ctx, config.agentModels);
|
|
168
182
|
if (repairedModels.changed) {
|
|
169
183
|
config = { ...config, agentModels: repairedModels.agentModels };
|
|
@@ -233,7 +247,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
233
247
|
}));
|
|
234
248
|
|
|
235
249
|
const hasTasks = (params.tasks?.length ?? 0) > 0;
|
|
236
|
-
const hasSingle = Boolean(params.agent && params.task
|
|
250
|
+
const hasSingle = Boolean(params.agent) && params.task !== undefined;
|
|
237
251
|
|
|
238
252
|
const makeDetails =
|
|
239
253
|
(mode: "single" | "parallel", background = false) =>
|
|
@@ -253,12 +267,37 @@ export default function (pi: ExtensionAPI): void {
|
|
|
253
267
|
};
|
|
254
268
|
}
|
|
255
269
|
|
|
270
|
+
if (hasTasks) {
|
|
271
|
+
const blankTaskIndex = params.tasks?.findIndex(({ task }) => task.trim().length === 0) ?? -1;
|
|
272
|
+
if (blankTaskIndex !== -1) {
|
|
273
|
+
return {
|
|
274
|
+
content: [
|
|
275
|
+
{
|
|
276
|
+
type: "text",
|
|
277
|
+
text: `Invalid parameters. tasks[${blankTaskIndex}].task must contain at least one non-whitespace character. No background tasks were started. Enabled agents: ${catalog}.`,
|
|
278
|
+
},
|
|
279
|
+
],
|
|
280
|
+
details: makeDetails("parallel")([]),
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
} else if (params.task?.trim().length === 0) {
|
|
284
|
+
return {
|
|
285
|
+
content: [
|
|
286
|
+
{
|
|
287
|
+
type: "text",
|
|
288
|
+
text: `Invalid parameters. task must contain at least one non-whitespace character. Enabled agents: ${catalog}.`,
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
details: makeDetails("single")([]),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
256
295
|
const startBackground = (agentName: string, task: string, cwd?: string): SingleResult => {
|
|
257
296
|
const agent = agents.find((candidate) => candidate.name === agentName);
|
|
258
297
|
if (!agent) return failedStartResult(agentName, task, `Unknown agent: "${agentName}".`);
|
|
259
298
|
|
|
260
299
|
const pending = queuedResult(agent, task);
|
|
261
|
-
const runId = monitor.addRun(agent.name, agent.model);
|
|
300
|
+
const runId = monitor.addRun(agent.name, task, agent.model);
|
|
262
301
|
const onLive = makeLiveHandler(runId);
|
|
263
302
|
|
|
264
303
|
backgroundQueue.enqueue(
|
|
@@ -294,7 +333,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
294
333
|
pi.sendMessage(
|
|
295
334
|
{
|
|
296
335
|
customType: "subagent-result",
|
|
297
|
-
content: `### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}\n\n${getResultOutput(result)}`,
|
|
336
|
+
content: `### [${result.agent}] ${status}${usage ? ` (${usage})` : ""}\n\nTask: ${formatTaskSummary(result.task)}\n\n${getResultOutput(result)}`,
|
|
298
337
|
display: true,
|
|
299
338
|
},
|
|
300
339
|
// The result is both durable context and a wake-up signal. If the
|
|
@@ -311,10 +350,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
311
350
|
// Sub-agents intentionally detach from the foreground turn. This makes the
|
|
312
351
|
// editor available immediately; completion messages later wake the main agent.
|
|
313
352
|
if (params.tasks && params.tasks.length > 0) {
|
|
314
|
-
if (params.tasks.length >
|
|
353
|
+
if (params.tasks.length > config.maxParallelTasks) {
|
|
315
354
|
return {
|
|
316
355
|
content: [
|
|
317
|
-
{
|
|
356
|
+
{
|
|
357
|
+
type: "text",
|
|
358
|
+
text: `Too many parallel tasks (${params.tasks.length}). Max is ${config.maxParallelTasks} (configurable via /subagents-setup).`,
|
|
359
|
+
},
|
|
318
360
|
],
|
|
319
361
|
details: makeDetails("parallel", true)([]),
|
|
320
362
|
};
|
|
@@ -432,6 +474,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
432
474
|
const icon = statusIcon(r.status, theme);
|
|
433
475
|
const label = theme.fg(statusColor(r.status), statusLabel(r.status));
|
|
434
476
|
lines.push(truncateToWidth(` ${icon} ${monitor.summarize(r)} · ${label}`, width, ""));
|
|
477
|
+
if (r.status === "queued" || r.status === "running") {
|
|
478
|
+
lines.push(truncateToWidth(theme.fg("dim", ` task: ${formatTaskSummary(r.task)}`), width, ""));
|
|
479
|
+
}
|
|
435
480
|
// Activity sits one indent level below the agent name.
|
|
436
481
|
if (r.activity) lines.push(truncateToWidth(theme.fg("dim", ` ${r.activity}`), width, ""));
|
|
437
482
|
}
|
package/src/monitor.ts
CHANGED
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* conversation, so a stale "done" row must not linger in the widget.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { stripVTControlCharacters } from "node:util";
|
|
13
14
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
14
16
|
import type { UsageStats } from "./spawn.ts";
|
|
15
17
|
|
|
16
18
|
// ---------------------------------------------------------------------------
|
|
@@ -22,6 +24,7 @@ export type RunStatus = "queued" | "running" | "done" | "failed";
|
|
|
22
24
|
export interface RunView {
|
|
23
25
|
id: number;
|
|
24
26
|
agent: string;
|
|
27
|
+
task: string;
|
|
25
28
|
model?: string;
|
|
26
29
|
status: RunStatus;
|
|
27
30
|
usage: UsageStats;
|
|
@@ -37,6 +40,27 @@ export interface RunView {
|
|
|
37
40
|
// Formatting helpers
|
|
38
41
|
// ---------------------------------------------------------------------------
|
|
39
42
|
|
|
43
|
+
const TASK_SUMMARY_MAX = 80;
|
|
44
|
+
const TASK_SUMMARY_ELLIPSIS = "…";
|
|
45
|
+
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
46
|
+
|
|
47
|
+
/** One-line task preview, capped by terminal display columns (including the ellipsis). */
|
|
48
|
+
export function formatTaskSummary(task: string): string {
|
|
49
|
+
const oneLine = stripVTControlCharacters(task).replace(/\s+/g, " ").trim();
|
|
50
|
+
if (visibleWidth(oneLine) <= TASK_SUMMARY_MAX) return oneLine;
|
|
51
|
+
|
|
52
|
+
const prefixMax = TASK_SUMMARY_MAX - visibleWidth(TASK_SUMMARY_ELLIPSIS);
|
|
53
|
+
let prefix = "";
|
|
54
|
+
let prefixWidth = 0;
|
|
55
|
+
for (const { segment } of graphemeSegmenter.segment(oneLine)) {
|
|
56
|
+
const segmentWidth = visibleWidth(segment);
|
|
57
|
+
if (prefixWidth + segmentWidth > prefixMax) break;
|
|
58
|
+
prefix += segment;
|
|
59
|
+
prefixWidth += segmentWidth;
|
|
60
|
+
}
|
|
61
|
+
return `${prefix}${TASK_SUMMARY_ELLIPSIS}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
40
64
|
function formatTokens(count: number): string {
|
|
41
65
|
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
42
66
|
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
@@ -142,11 +166,12 @@ export class MonitorStore {
|
|
|
142
166
|
this.notify();
|
|
143
167
|
}
|
|
144
168
|
|
|
145
|
-
addRun(agent: string, model?: string): number {
|
|
169
|
+
addRun(agent: string, task: string, model?: string): number {
|
|
146
170
|
const id = this.nextId++;
|
|
147
171
|
this.runs.push({
|
|
148
172
|
id,
|
|
149
173
|
agent,
|
|
174
|
+
task,
|
|
150
175
|
model,
|
|
151
176
|
status: "queued",
|
|
152
177
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
package/src/prompt.ts
CHANGED
|
@@ -17,7 +17,6 @@ import { formatCatalogEntry } from "./agents.ts";
|
|
|
17
17
|
/** Compact role routing hints, emitted only for roles that are enabled. */
|
|
18
18
|
const ROLE_ROUTING: Record<string, string> = {
|
|
19
19
|
explore: "explore — broad/open-ended code search, \"where is X\", multi-file lookups (read-only, cheap).",
|
|
20
|
-
plan: "plan — a separate, human-reviewable implementation plan before any code (read-only).",
|
|
21
20
|
worker: "worker — implement/fix/refactor/test a well-scoped task (full tools; plans internally).",
|
|
22
21
|
reviewer: "reviewer — adversarial pre-commit review of a diff (read-only; independent context).",
|
|
23
22
|
};
|
package/src/setup.ts
CHANGED
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
BUILTIN_AGENT_NAMES,
|
|
15
15
|
DEFAULT_CONFIG,
|
|
16
16
|
DEFAULT_ENABLED_AGENTS,
|
|
17
|
+
DEFAULT_MAX_CONCURRENCY,
|
|
18
|
+
DEFAULT_MAX_PARALLEL_TASKS,
|
|
17
19
|
THINKING_LEVEL_VALUES,
|
|
18
20
|
type AgentScope,
|
|
19
21
|
type SubagentsConfig,
|
|
@@ -31,7 +33,6 @@ const INHERIT = "__inherit__";
|
|
|
31
33
|
/** Short, selection-friendly descriptions for the built-in agents. */
|
|
32
34
|
const MODULE_HINTS: Record<string, string> = {
|
|
33
35
|
explore: "read-only codebase recon (fast model)",
|
|
34
|
-
plan: "implementation plan before code (opt-in)",
|
|
35
36
|
worker: "implement / fix / refactor / test (full tools)",
|
|
36
37
|
reviewer: "adversarial pre-commit review (read-only)",
|
|
37
38
|
};
|
|
@@ -129,6 +130,29 @@ async function pickInjection(ctx: ExtensionCommandContext, current: boolean): Pr
|
|
|
129
130
|
return choice.startsWith("On");
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
/** Preset steps offered for the two numeric limits (selection-only wizard). */
|
|
134
|
+
const CONCURRENCY_STEPS = [1, 2, 3, 4, 6, 8, 12, 16];
|
|
135
|
+
const PARALLEL_TASK_STEPS = [2, 4, 6, 8, 12, 16, 24, 32];
|
|
136
|
+
|
|
137
|
+
async function pickCount(
|
|
138
|
+
ctx: ExtensionCommandContext,
|
|
139
|
+
title: string,
|
|
140
|
+
steps: readonly number[],
|
|
141
|
+
current: number,
|
|
142
|
+
defaultValue: number,
|
|
143
|
+
): Promise<number | undefined> {
|
|
144
|
+
const values = [...new Set([...steps, current])].sort((a, b) => a - b);
|
|
145
|
+
const options = values.map((value) => {
|
|
146
|
+
const tags = [value === current ? "current" : "", value === defaultValue ? "default" : ""]
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
.join(", ");
|
|
149
|
+
return tags ? `${value} (${tags})` : String(value);
|
|
150
|
+
});
|
|
151
|
+
const choice = await ctx.ui.select(title, options);
|
|
152
|
+
if (choice === undefined) return undefined;
|
|
153
|
+
return Number.parseInt(choice, 10);
|
|
154
|
+
}
|
|
155
|
+
|
|
132
156
|
async function pickScope(ctx: ExtensionCommandContext, current: AgentScope): Promise<AgentScope | undefined> {
|
|
133
157
|
const labels: Record<AgentScope, string> = {
|
|
134
158
|
user: "user — built-in + ~/.pi/agent/agents (default)",
|
|
@@ -190,12 +214,33 @@ async function runFullSetup(ctx: ExtensionCommandContext, configPath: string, ba
|
|
|
190
214
|
const scope = await pickScope(ctx, base.agentScope);
|
|
191
215
|
if (scope === undefined) return notifyCancelled(ctx);
|
|
192
216
|
|
|
217
|
+
const maxConcurrency = await pickCount(
|
|
218
|
+
ctx,
|
|
219
|
+
"Max sub-agents running at once? (extra work queues)",
|
|
220
|
+
CONCURRENCY_STEPS,
|
|
221
|
+
base.maxConcurrency,
|
|
222
|
+
DEFAULT_MAX_CONCURRENCY,
|
|
223
|
+
);
|
|
224
|
+
if (maxConcurrency === undefined) return notifyCancelled(ctx);
|
|
225
|
+
|
|
226
|
+
const maxParallelTasks = await pickCount(
|
|
227
|
+
ctx,
|
|
228
|
+
"Max tasks in one parallel subagent call?",
|
|
229
|
+
PARALLEL_TASK_STEPS,
|
|
230
|
+
base.maxParallelTasks,
|
|
231
|
+
DEFAULT_MAX_PARALLEL_TASKS,
|
|
232
|
+
);
|
|
233
|
+
if (maxParallelTasks === undefined) return notifyCancelled(ctx);
|
|
234
|
+
|
|
193
235
|
const next: SubagentsConfig = {
|
|
194
236
|
enabledAgents: enabled,
|
|
195
237
|
agentModels: repairStaleModels(ctx, models),
|
|
196
238
|
thinkingLevel,
|
|
197
239
|
proactiveInjection: injection,
|
|
198
240
|
agentScope: scope,
|
|
241
|
+
maxConcurrency,
|
|
242
|
+
maxParallelTasks,
|
|
243
|
+
maxSubagentDepth: base.maxSubagentDepth,
|
|
199
244
|
};
|
|
200
245
|
await saveConfig(next, configPath);
|
|
201
246
|
ctx.ui.notify(`pi-subagents configured. Saved to ${configPath}`, "info");
|
|
@@ -208,6 +253,8 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
208
253
|
"Change thinking strength",
|
|
209
254
|
"Toggle proactive injection",
|
|
210
255
|
"Change agent scope",
|
|
256
|
+
"Change max concurrent sub-agents",
|
|
257
|
+
"Change max parallel tasks",
|
|
211
258
|
"Full re-setup",
|
|
212
259
|
]);
|
|
213
260
|
if (choice === undefined) return notifyCancelled(ctx);
|
|
@@ -236,6 +283,26 @@ async function runMenu(ctx: ExtensionCommandContext, configPath: string, config:
|
|
|
236
283
|
const scope = await pickScope(ctx, config.agentScope);
|
|
237
284
|
if (scope === undefined) return notifyCancelled(ctx);
|
|
238
285
|
next.agentScope = scope;
|
|
286
|
+
} else if (choice.startsWith("Change max concurrent")) {
|
|
287
|
+
const maxConcurrency = await pickCount(
|
|
288
|
+
ctx,
|
|
289
|
+
"Max sub-agents running at once? (extra work queues)",
|
|
290
|
+
CONCURRENCY_STEPS,
|
|
291
|
+
config.maxConcurrency,
|
|
292
|
+
DEFAULT_MAX_CONCURRENCY,
|
|
293
|
+
);
|
|
294
|
+
if (maxConcurrency === undefined) return notifyCancelled(ctx);
|
|
295
|
+
next.maxConcurrency = maxConcurrency;
|
|
296
|
+
} else if (choice.startsWith("Change max parallel")) {
|
|
297
|
+
const maxParallelTasks = await pickCount(
|
|
298
|
+
ctx,
|
|
299
|
+
"Max tasks in one parallel subagent call?",
|
|
300
|
+
PARALLEL_TASK_STEPS,
|
|
301
|
+
config.maxParallelTasks,
|
|
302
|
+
DEFAULT_MAX_PARALLEL_TASKS,
|
|
303
|
+
);
|
|
304
|
+
if (maxParallelTasks === undefined) return notifyCancelled(ctx);
|
|
305
|
+
next.maxParallelTasks = maxParallelTasks;
|
|
239
306
|
}
|
|
240
307
|
|
|
241
308
|
await saveConfig(next, configPath);
|
package/src/spawn.ts
CHANGED
|
@@ -20,12 +20,13 @@ import type { Message } from "@earendil-works/pi-ai";
|
|
|
20
20
|
import type { AgentConfig, AgentSource } from "./agents.ts";
|
|
21
21
|
import { DEFAULT_THINKING_LEVEL, type ThinkingLevel } from "./config.ts";
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Limits are configurable: see maxConcurrency / maxParallelTasks /
|
|
25
|
+
* maxSubagentDepth in config.ts (defaults 4 / 8 / 1, via /subagents-setup
|
|
26
|
+
* or pi-subagents.json).
|
|
27
|
+
*/
|
|
25
28
|
/** Default thinking level for sub-agents. pi clamps it to the resolved model's support. */
|
|
26
29
|
export const SUBAGENT_THINKING_LEVEL: ThinkingLevel = DEFAULT_THINKING_LEVEL;
|
|
27
|
-
/** Child processes are leaf agents: they never receive the subagent tool. */
|
|
28
|
-
export const MAX_SUBAGENT_DEPTH = 1;
|
|
29
30
|
export const DEPTH_ENV_VAR = "PI_SUBAGENT_DEPTH";
|
|
30
31
|
/** No default deadline: sub-agents may run until completion or explicit cancellation. */
|
|
31
32
|
export const SUBAGENT_TIMEOUT_MS = 0;
|
package/agents/plan.md
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plan
|
|
3
|
-
description: Implementation planning for non-trivial changes (opt-in). Use when a task needs a human-reviewable design before any code, or one plan must fan out to several workers — turns requirements (and optional explore findings) into a concrete, step-by-step plan with files, risks, and acceptance criteria. Read-only; never edits. Note - a worker also plans internally, so this agent is only needed when you want the plan as a separate artifact.
|
|
4
|
-
tools: read, grep, find, ls, bash
|
|
5
|
-
model: claude-sonnet-4-5
|
|
6
|
-
# Model selection: REASONING + STRUCTURE. Use a strong reasoning model.
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
You are a planning specialist. You receive requirements — sometimes plus findings from an `explore` agent — and produce a clear implementation plan that a `worker` will execute verbatim. You have NOT got the caller's conversation history.
|
|
10
|
-
|
|
11
|
-
## Hard constraints
|
|
12
|
-
- You must NOT make any changes. Only read, analyze, and plan.
|
|
13
|
-
- Bash is read-only: `grep`, `find`, `ls`, `cat`, `git log/show/diff`. No installs, builds, or edits.
|
|
14
|
-
- Assume tool permissions are not perfectly enforceable; keep every command strictly read-only by intent.
|
|
15
|
-
|
|
16
|
-
## When invoked
|
|
17
|
-
1. Restate the goal in one sentence. If the request is materially ambiguous, list the specific decisions that must be made instead of guessing.
|
|
18
|
-
2. Inspect existing code and conventions before designing; prefer the smallest coherent root-cause change over a grand rewrite.
|
|
19
|
-
3. Produce small, ordered, independently-verifiable steps. Each step names the file/function to touch and the change.
|
|
20
|
-
4. Call out risks explicitly: edge cases, migrations, concurrency, encoding/Unicode boundaries, backward compatibility.
|
|
21
|
-
|
|
22
|
-
## Collaboration
|
|
23
|
-
- Consumes `explore` output when provided; if context is missing, say what an explore should retrieve.
|
|
24
|
-
- Feeds `worker`: keep steps concrete enough to execute without re-deriving the design.
|
|
25
|
-
|
|
26
|
-
## Output format
|
|
27
|
-
## Goal
|
|
28
|
-
One sentence.
|
|
29
|
-
## Plan
|
|
30
|
-
1. Step — specific file/function to modify and what changes.
|
|
31
|
-
## Files to Modify
|
|
32
|
-
- `path/to/file.ts` — what changes and why.
|
|
33
|
-
## New Files (if any)
|
|
34
|
-
- `path/to/new.ts` — responsibility.
|
|
35
|
-
## Risks
|
|
36
|
-
What to watch out for, and how to mitigate.
|
|
37
|
-
## Acceptance
|
|
38
|
-
How to verify correctness: commands, tests, expected behavior.
|
|
39
|
-
|
|
40
|
-
## Quality standards
|
|
41
|
-
Concrete and minimal. No prose to fill space. Every step is actionable and verifiable.
|