pi-subagents 0.9.0 → 0.9.2

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 CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.2] - 2026-02-19
6
+
7
+ ### Fixed
8
+ - TUI crash on async subagent completion: "Rendered line exceeds terminal width." `render.ts` never truncated output to fit the terminal — widget lines (`agents.join(" -> ")`), chain visualizations, skills lists, and task previews could all exceed the terminal width. Added `truncLine` helper using pi-tui's `truncateToWidth`/`visibleWidth` and applied it to every `Text` widget and widget string. Task preview lengths are now dynamic based on terminal width instead of hardcoded.
9
+ - Agent Manager scope badge showed `[built]` instead of `[builtin]` in list and detail views. Widened scope column to fit.
10
+
11
+ ## [0.9.1] - 2026-02-17
12
+
13
+ ### Fixed
14
+ - Builtin agents were silently excluded from management listings, chain validation, and agent resolution. Added `allAgents()` helper that includes all three tiers (builtin, user, project) and applied it to `handleList`, `findAgents`, `availableNames`, and `unknownChainAgents`.
15
+ - `resolveTarget` now blocks mutation of builtin agents with a clear error message suggesting the user create a same-named override, instead of allowing `fs.unlinkSync` or `fs.writeFileSync` on extension files.
16
+ - Agent Manager TUI guards: delete and edit actions on builtin agents are blocked with an error status. Detail screen hides `[e]dit` from the footer for builtins. Scope badge shows `[builtin]` instead of falling through to `[proj]`.
17
+ - Cloning a builtin agent set the scope to `"builtin"` at runtime (violating the `"user" | "project"` type), causing wrong badge display and the clone inheriting builtin protections until session reload. Now maps to `"user"`.
18
+ - Agent Manager `loadEntries` suppresses builtins overridden by user/project agents, preventing duplicate entries in the TUI list.
19
+ - `BUILTIN_AGENTS_DIR` resolved via `import.meta.url` instead of hardcoded `~/.pi/agent/extensions/subagent/agents` path. Works regardless of where the extension is installed.
20
+ - `handleCreate` now warns when creating an agent that shadows a builtin (informational, not an error).
21
+
22
+ ### Changed
23
+ - Simplified Agent Manager header from per-scope breakdown to total count (per-row badges already show scope).
24
+ - Reviewer builtin model changed from `openai/gpt-5.2` to `openai-codex/gpt-5.3-codex`.
25
+ - Removed `code-reviewer` builtin agent (redundant with `reviewer`).
26
+
5
27
  ## [0.9.0] - 2026-02-17
6
28
 
7
29
  ### Added
@@ -9,8 +31,7 @@
9
31
  - `scout` — fast codebase recon (claude-haiku-4-5)
10
32
  - `planner` — implementation plans from context (claude-opus-4-6, thinking: high)
11
33
  - `worker` — general-purpose execution (claude-sonnet-4-6)
12
- - `reviewer` — validates implementation against plans (gpt-5.2, thinking: high)
13
- - `code-reviewer` — bug hunting and code review (claude-opus-4-6, thinking: high)
34
+ - `reviewer` — validates implementation against plans (gpt-5.3-codex, thinking: high)
14
35
  - `context-builder` — analyzes requirements and codebase (claude-sonnet-4-6)
15
36
  - `researcher` — autonomous web research with search, evaluation, and synthesis (claude-sonnet-4-6)
16
37
  - **`"builtin"` agent source** — new third tier in agent discovery. Priority: builtin < user < project. Builtin agents appear in listings with a `[builtin]` badge and cannot be modified or deleted through management actions (create a same-named user agent to override instead).
package/README.md CHANGED
@@ -26,13 +26,18 @@ Agents are markdown files with YAML frontmatter that define specialized subagent
26
26
 
27
27
  **Agent file locations:**
28
28
 
29
- | Scope | Path |
30
- |-------|------|
31
- | User | `~/.pi/agent/agents/{name}.md` |
32
- | Project | `.pi/agents/{name}.md` (searches up directory tree) |
29
+ | Scope | Path | Priority |
30
+ |-------|------|----------|
31
+ | Builtin | `~/.pi/agent/extensions/subagent/agents/` | Lowest |
32
+ | User | `~/.pi/agent/agents/{name}.md` | Medium |
33
+ | Project | `.pi/agents/{name}.md` (searches up directory tree) | Highest |
33
34
 
34
35
  Use `agentScope` parameter to control discovery: `"user"`, `"project"`, or `"both"` (default; project takes priority).
35
36
 
37
+ **Builtin agents:** The extension ships with ready-to-use agents — `scout`, `planner`, `worker`, `reviewer`, `context-builder`, and `researcher`. They load at lowest priority so any user or project agent with the same name overrides them. Builtin agents appear with a `[builtin]` badge in listings and cannot be modified through management actions (create a same-named user agent to override instead).
38
+
39
+ > **Note:** The `researcher` agent uses `web_search`, `fetch_content`, and `get_search_content` tools which require the [pi-web-access](https://github.com/nicobailon/pi-web-access) extension. Install it with `pi install npm:pi-web-access`.
40
+
36
41
  **Agent frontmatter:**
37
42
 
38
43
  ```yaml
@@ -5,6 +5,7 @@ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
5
5
  import {
6
6
  type AgentConfig,
7
7
  type AgentScope,
8
+ type AgentSource,
8
9
  type ChainConfig,
9
10
  type ChainStepConfig,
10
11
  discoverAgentsAll,
@@ -62,9 +63,13 @@ export function sanitizeName(name: string): string {
62
63
  return name.toLowerCase().trim().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
63
64
  }
64
65
 
66
+ function allAgents(d: { builtin: AgentConfig[]; user: AgentConfig[]; project: AgentConfig[] }): AgentConfig[] {
67
+ return [...d.builtin, ...d.user, ...d.project];
68
+ }
69
+
65
70
  function availableNames(cwd: string, kind: "agent" | "chain"): string[] {
66
71
  const d = discoverAgentsAll(cwd);
67
- const items = kind === "agent" ? [...d.user, ...d.project] : d.chains;
72
+ const items = kind === "agent" ? allAgents(d) : d.chains;
68
73
  return [...new Set(items.map((x) => x.name))].sort((a, b) => a.localeCompare(b));
69
74
  }
70
75
 
@@ -72,7 +77,7 @@ export function findAgents(name: string, cwd: string, scope: AgentScope = "both"
72
77
  const d = discoverAgentsAll(cwd);
73
78
  const raw = name.trim();
74
79
  const sanitized = sanitizeName(raw);
75
- return [...d.user, ...d.project]
80
+ return allAgents(d)
76
81
  .filter((a) => (scope === "both" || a.source === scope) && (a.name === raw || a.name === sanitized))
77
82
  .sort((a, b) => a.source.localeCompare(b.source));
78
83
  }
@@ -98,7 +103,7 @@ function nameExistsInScope(cwd: string, scope: ManagementScope, name: string, ex
98
103
 
99
104
  function unknownChainAgents(cwd: string, steps: ChainStepConfig[]): string[] {
100
105
  const d = discoverAgentsAll(cwd);
101
- const known = new Set([...d.user, ...d.project].map((a) => a.name));
106
+ const known = new Set(allAgents(d).map((a) => a.name));
102
107
  return [...new Set(steps.map((s) => s.agent).filter((a) => !known.has(a)))].sort((a, b) => a.localeCompare(b));
103
108
  }
104
109
 
@@ -233,24 +238,28 @@ function applyAgentConfig(target: AgentConfig, cfg: Record<string, unknown>): st
233
238
  return undefined;
234
239
  }
235
240
 
236
- function resolveTarget<T extends { source: "user" | "project"; filePath: string }>(
241
+ function resolveTarget<T extends { source: AgentSource; filePath: string }>(
237
242
  kind: "agent" | "chain",
238
243
  name: string,
239
244
  matches: T[],
240
245
  cwd: string,
241
246
  scopeHint?: string,
242
247
  ): T | AgentToolResult<Details> {
243
- if (matches.length === 0) {
248
+ const mutable = matches.filter((m) => m.source !== "builtin");
249
+ if (mutable.length === 0) {
250
+ if (matches.length > 0) {
251
+ return result(`${kind === "agent" ? "Agent" : "Chain"} '${name}' is builtin and cannot be modified. Create a same-named ${kind} in user or project scope to override it.`, true);
252
+ }
244
253
  const available = availableNames(cwd, kind);
245
254
  return result(`${kind === "agent" ? "Agent" : "Chain"} '${name}' not found. Available: ${available.join(", ") || "none"}.`, true);
246
255
  }
247
- if (matches.length === 1) return matches[0]!;
256
+ if (mutable.length === 1) return mutable[0]!;
248
257
  const scope = asDisambiguationScope(scopeHint);
249
258
  if (!scope) {
250
- const paths = matches.map((m) => `${m.source}: ${m.filePath}`).join("\n");
259
+ const paths = mutable.map((m) => `${m.source}: ${m.filePath}`).join("\n");
251
260
  return result(`${kind === "agent" ? "Agent" : "Chain"} '${name}' exists in both scopes. Specify agentScope: 'user' or 'project'.\n${paths}`, true);
252
261
  }
253
- const scoped = matches.filter((m) => m.source === scope);
262
+ const scoped = mutable.filter((m) => m.source === scope);
254
263
  if (scoped.length === 0) return result(`${kind === "agent" ? "Agent" : "Chain"} '${name}' not found in scope '${scope}'.`, true);
255
264
  if (scoped.length > 1) return result(`Multiple ${kind}s named '${name}' found in scope '${scope}': ${scoped.map((m) => m.filePath).join(", ")}`, true);
256
265
  return scoped[0]!;
@@ -309,7 +318,7 @@ export function formatChainDetail(chain: ChainConfig): string {
309
318
  export function handleList(params: ManagementParams, ctx: ManagementContext): AgentToolResult<Details> {
310
319
  const scope = normalizeListScope(params.agentScope) ?? "both";
311
320
  const d = discoverAgentsAll(ctx.cwd);
312
- const agents = [...d.user, ...d.project].filter((a) => scope === "both" || a.source === scope).sort((a, b) => a.name.localeCompare(b.name));
321
+ const agents = allAgents(d).filter((a) => scope === "both" || a.source === "builtin" || a.source === scope).sort((a, b) => a.name.localeCompare(b.name));
313
322
  const chains = d.chains.filter((c) => scope === "both" || c.source === scope).sort((a, b) => a.name.localeCompare(b.name));
314
323
  const lines = ["Agents:", ...(agents.length ? agents.map((a) => `- ${a.name} (${a.source}): ${a.description}`) : ["- (none)"]), "", "Chains:", ...(chains.length ? chains.map((c) => `- ${c.name} (${c.source}): ${c.description}`) : ["- (none)"])];
315
324
  return result(lines.join("\n"));
@@ -363,6 +372,7 @@ export function handleCreate(params: ManagementParams, ctx: ManagementContext):
363
372
  const targetPath = path.join(targetDir, isChain ? `${name}.chain.md` : `${name}.md`);
364
373
  if (fs.existsSync(targetPath)) return result(`File already exists at ${targetPath} but is not a valid ${isChain ? "chain" : "agent"} definition. Remove or rename it first.`, true);
365
374
  const warnings: string[] = [];
375
+ if (!isChain && d.builtin.some((a) => a.name === name)) warnings.push(`Note: this shadows the builtin agent '${name}'.`);
366
376
  if (isChain) {
367
377
  const parsed = parseStepList(cfg.steps);
368
378
  if (parsed.error) return result(parsed.error, true);
@@ -129,7 +129,7 @@ export function renderDetail(
129
129
  theme: Theme,
130
130
  ): string[] {
131
131
  const lines: string[] = [];
132
- const scopeBadge = agent.source === "user" ? "[user]" : "[proj]";
132
+ const scopeBadge = agent.source === "builtin" ? "[builtin]" : agent.source === "project" ? "[proj]" : "[user]";
133
133
  const headerText = ` ${agent.name} ${scopeBadge} ${formatPath(agent.filePath)} `;
134
134
  lines.push(renderHeader(headerText, width, theme));
135
135
  lines.push(row("", width, theme));
@@ -149,7 +149,10 @@ export function renderDetail(
149
149
  const scrollInfo = formatScrollInfo(state.scrollOffset, Math.max(0, contentLines.length - (state.scrollOffset + DETAIL_VIEWPORT_HEIGHT)));
150
150
  lines.push(row(scrollInfo ? ` ${theme.fg("dim", scrollInfo)}` : "", width, theme));
151
151
 
152
- lines.push(renderFooter(" [l]aunch [e]dit [v] raw/resolved [↑↓] scroll [esc] back ", width, theme));
152
+ const footer = agent.source === "builtin"
153
+ ? " [l]aunch [v] raw/resolved [↑↓] scroll [esc] back "
154
+ : " [l]aunch [e]dit [v] raw/resolved [↑↓] scroll [esc] back ";
155
+ lines.push(renderFooter(footer, width, theme));
153
156
  return lines;
154
157
  }
155
158
 
@@ -1,4 +1,5 @@
1
1
  import type { Theme } from "@mariozechner/pi-coding-agent";
2
+ import type { AgentSource } from "./agents.js";
2
3
  import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
3
4
  import { pad, row, renderHeader, renderFooter, fuzzyFilter, formatScrollInfo } from "./render-helpers.js";
4
5
 
@@ -7,7 +8,7 @@ export interface ListAgent {
7
8
  name: string;
8
9
  description: string;
9
10
  model?: string;
10
- source: "user" | "project";
11
+ source: AgentSource;
11
12
  kind: "agent" | "chain";
12
13
  stepCount?: number;
13
14
  }
@@ -28,7 +29,7 @@ export type ListAction =
28
29
  | { type: "run-parallel"; ids: string[] }
29
30
  | { type: "close" };
30
31
 
31
- const LIST_VIEWPORT_HEIGHT = 12;
32
+ const LIST_VIEWPORT_HEIGHT = 8;
32
33
 
33
34
  function selectionCount(selected: string[], id: string): number {
34
35
  let count = 0;
@@ -162,11 +163,11 @@ export function renderList(
162
163
  const filtered = fuzzyFilter(agents, state.filterQuery);
163
164
  clampCursor(state, filtered);
164
165
 
165
- const agentOnly = agents.filter((a) => a.kind === "agent");
166
- const userCount = agentOnly.filter((a) => a.source === "user").length;
167
- const projectCount = agentOnly.filter((a) => a.source === "project").length;
166
+ const agentCount = agents.filter((a) => a.kind === "agent").length;
168
167
  const chainCount = agents.filter((a) => a.kind === "chain").length;
169
- const headerText = ` Subagents [user: ${userCount} project: ${projectCount} chains: ${chainCount}] `;
168
+ const headerText = chainCount
169
+ ? ` Subagents [${agentCount} agents ${chainCount} chains] `
170
+ : ` Subagents [${agentCount}] `;
170
171
  lines.push(renderHeader(headerText, width, theme));
171
172
  lines.push(row("", width, theme));
172
173
 
@@ -189,7 +190,7 @@ export function renderList(
189
190
  const innerW = width - 2;
190
191
  const nameWidth = 16;
191
192
  const modelWidth = 12;
192
- const scopeWidth = 7;
193
+ const scopeWidth = 9;
193
194
 
194
195
  for (let i = 0; i < visible.length; i++) {
195
196
  const agent = visible[i]!;
@@ -207,7 +208,8 @@ export function renderList(
207
208
  const modelDisplay = modelRaw.includes("/") ? modelRaw.split("/").pop() ?? modelRaw : modelRaw;
208
209
  const nameText = isCursor ? theme.fg("accent", agent.name) : agent.name;
209
210
  const modelText = theme.fg("dim", modelDisplay);
210
- const scopeBadge = theme.fg("dim", agent.kind === "chain" ? "[chain]" : (agent.source === "user" ? "[user]" : "[proj]"));
211
+ const scopeLabel = agent.kind === "chain" ? "[chain]" : agent.source === "builtin" ? "[builtin]" : agent.source === "project" ? "[proj]" : "[user]";
212
+ const scopeBadge = theme.fg("dim", scopeLabel);
211
213
  const descText = theme.fg("dim", agent.description);
212
214
 
213
215
  const descWidth = Math.max(0, innerW - 1 - visibleWidth(prefix) - nameWidth - modelWidth - scopeWidth - 3);
package/agent-manager.ts CHANGED
@@ -24,7 +24,7 @@ export type ManagerResult =
24
24
  | { action: "launch-chain"; chain: ChainConfig; task: string; skipClarify?: boolean }
25
25
  | undefined;
26
26
 
27
- export interface AgentData { user: AgentConfig[]; project: AgentConfig[]; chains: ChainConfig[]; userDir: string; projectDir: string | null; cwd: string; }
27
+ export interface AgentData { builtin: AgentConfig[]; user: AgentConfig[]; project: AgentConfig[]; chains: ChainConfig[]; userDir: string; projectDir: string | null; cwd: string; }
28
28
  type ManagerScreen = "list" | "detail" | "chain-detail" | "edit" | "edit-field" | "edit-prompt" | "task-input" | "confirm-delete" | "name-input" | "chain-edit" | "template-select" | "parallel-builder";
29
29
  interface AgentEntry { id: string; kind: "agent"; config: AgentConfig; isNew: boolean; }
30
30
  interface ChainEntry { id: string; kind: "chain"; config: ChainConfig; }
@@ -65,7 +65,8 @@ export class AgentManagerComponent implements Component {
65
65
  constructor(private tui: TUI, private theme: Theme, private agentData: AgentData, private models: ModelInfo[], private skills: SkillInfo[], private done: (result: ManagerResult) => void) { this.loadEntries(); }
66
66
 
67
67
  private loadEntries(): void {
68
- const agents: AgentEntry[] = []; for (const config of this.agentData.user) agents.push({ id: `a${this.nextId++}`, kind: "agent", config: cloneConfig(config), isNew: false }); for (const config of this.agentData.project) agents.push({ id: `a${this.nextId++}`, kind: "agent", config: cloneConfig(config), isNew: false }); this.agents = agents;
68
+ const overridden = new Set([...this.agentData.user, ...this.agentData.project].map((c) => c.name));
69
+ const agents: AgentEntry[] = []; for (const config of this.agentData.builtin) { if (!overridden.has(config.name)) agents.push({ id: `a${this.nextId++}`, kind: "agent", config: cloneConfig(config), isNew: false }); } for (const config of this.agentData.user) agents.push({ id: `a${this.nextId++}`, kind: "agent", config: cloneConfig(config), isNew: false }); for (const config of this.agentData.project) agents.push({ id: `a${this.nextId++}`, kind: "agent", config: cloneConfig(config), isNew: false }); this.agents = agents;
69
70
  const chains: ChainEntry[] = []; for (const config of this.agentData.chains) chains.push({ id: `c${this.nextId++}`, kind: "chain", config: cloneChainConfig(config) }); this.chains = chains;
70
71
  }
71
72
 
@@ -104,8 +105,8 @@ export class AgentManagerComponent implements Component {
104
105
 
105
106
  private enterNameInput(mode: NameInputState["mode"], sourceId?: string, template?: AgentTemplate): void {
106
107
  const allowProject = Boolean(this.agentData.projectDir); let initial = ""; let scope: "user" | "project" = "user";
107
- if (mode === "clone-agent" && sourceId) { const entry = this.getAgentEntry(sourceId); if (entry) { initial = `${entry.config.name}-copy`; scope = entry.config.source; } }
108
- if (mode === "clone-chain" && sourceId) { const entry = this.getChainEntry(sourceId); if (entry) { initial = `${entry.config.name}-copy`; scope = entry.config.source; } }
108
+ if (mode === "clone-agent" && sourceId) { const entry = this.getAgentEntry(sourceId); if (entry) { initial = `${entry.config.name}-copy`; scope = entry.config.source === "project" ? "project" : "user"; } }
109
+ if (mode === "clone-chain" && sourceId) { const entry = this.getChainEntry(sourceId); if (entry) { initial = `${entry.config.name}-copy`; scope = entry.config.source === "project" ? "project" : "user"; } }
109
110
  if (mode === "new-agent" && template && template.name !== "Blank") initial = slugTemplateName(template.name);
110
111
  this.nameInputState = { mode, editor: createEditorState(initial), scope, allowProject, sourceId, template }; this.screen = "name-input";
111
112
  }
@@ -334,12 +335,14 @@ export class AgentManagerComponent implements Component {
334
335
  this.editState = null; this.enterDetail(entry); this.tui.requestRender();
335
336
  }
336
337
 
338
+ private isBuiltin(id: string): boolean { const a = this.getAgentEntry(id); return a?.config.source === "builtin"; }
339
+
337
340
  private handleListAction(action: ListAction): void {
338
341
  switch (action.type) {
339
342
  case "open-detail": { const agent = this.getAgentEntry(action.id); if (agent) { this.enterDetail(agent); return; } const chain = this.getChainEntry(action.id); if (chain) this.enterChainDetail(chain); return; }
340
343
  case "clone": if (this.getAgentEntry(action.id)) this.enterNameInput("clone-agent", action.id); else if (this.getChainEntry(action.id)) this.enterNameInput("clone-chain", action.id); return;
341
344
  case "new": this.enterTemplateSelect(); return;
342
- case "delete": this.confirmDeleteId = action.id; this.screen = "confirm-delete"; return;
345
+ case "delete": { if (this.isBuiltin(action.id)) { this.statusMessage = { text: "Builtin agents cannot be deleted. Clone to user scope to override.", type: "error" }; return; } this.confirmDeleteId = action.id; this.screen = "confirm-delete"; return; }
343
346
  case "run-chain": this.enterTaskInput(action.ids); return;
344
347
  case "run-parallel": this.enterParallelBuilder(action.ids); return;
345
348
  case "close": this.done(undefined); return;
@@ -348,7 +351,7 @@ export class AgentManagerComponent implements Component {
348
351
 
349
352
  private handleDetailAction(action: DetailAction, entry: AgentEntry): void {
350
353
  if (action.type === "back") { this.screen = "list"; return; }
351
- if (action.type === "edit") { this.enterEdit(entry); return; }
354
+ if (action.type === "edit") { if (entry.config.source === "builtin") { this.statusMessage = { text: "Builtin agents cannot be edited. Clone to user scope to override.", type: "error" }; this.screen = "list"; return; } this.enterEdit(entry); return; }
352
355
  if (action.type === "launch") { this.enterTaskInput([entry.id], "detail"); return; }
353
356
  }
354
357
 
@@ -4,9 +4,12 @@ export function mergeAgentsForScope(
4
4
  scope: AgentScope,
5
5
  userAgents: AgentConfig[],
6
6
  projectAgents: AgentConfig[],
7
+ builtinAgents: AgentConfig[] = [],
7
8
  ): AgentConfig[] {
8
9
  const agentMap = new Map<string, AgentConfig>();
9
10
 
11
+ for (const agent of builtinAgents) agentMap.set(agent.name, agent);
12
+
10
13
  if (scope === "both") {
11
14
  for (const agent of userAgents) agentMap.set(agent.name, agent);
12
15
  for (const agent of projectAgents) agentMap.set(agent.name, agent);
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: context-builder
3
+ description: Analyzes requirements and codebase, generates context and meta-prompt
4
+ tools: read, grep, find, ls, bash, web_search
5
+ model: claude-sonnet-4-6
6
+ output: context.md
7
+ ---
8
+
9
+ You analyze user requirements against a codebase to build comprehensive context.
10
+
11
+ Given a user request (prose, user stories, requirements), you will:
12
+
13
+ 1. **Analyze the request** - Understand what the user wants to build
14
+ 2. **Search the codebase** - Find all relevant files, patterns, dependencies
15
+ 3. **Research if needed** - Look up APIs, libraries, best practices online
16
+ 4. **Generate output files** - You'll receive instructions about where to write
17
+
18
+ When running in a chain, generate two files in the specified chain directory:
19
+
20
+ **context.md** - Code context:
21
+ # Code Context
22
+ ## Relevant Files
23
+ [files with line numbers and snippets]
24
+ ## Patterns Found
25
+ [existing patterns to follow]
26
+ ## Dependencies
27
+ [libraries, APIs involved]
28
+
29
+ **meta-prompt.md** - Optimized instructions for planner:
30
+ # Meta-Prompt for Planning
31
+ ## Requirements Summary
32
+ [distilled requirements]
33
+ ## Technical Constraints
34
+ [must-haves, limitations]
35
+ ## Suggested Approach
36
+ [recommended implementation strategy]
37
+ ## Questions Resolved
38
+ [decisions made during analysis]
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: planner
3
+ description: Creates implementation plans from context and requirements
4
+ tools: read, grep, find, ls, write
5
+ model: claude-opus-4-6
6
+ thinking: high
7
+ output: plan.md
8
+ defaultReads: context.md
9
+ ---
10
+
11
+ You are a planning specialist. You receive context and requirements, then produce a clear implementation plan.
12
+
13
+ You must NOT make any changes. Only read, analyze, and plan.
14
+
15
+ When running in a chain, you'll receive instructions about which files to read and where to write your output.
16
+
17
+ Output format (plan.md):
18
+
19
+ # Implementation Plan
20
+
21
+ ## Goal
22
+ One sentence summary of what needs to be done.
23
+
24
+ ## Tasks
25
+ Numbered steps, each small and actionable:
26
+ 1. **Task 1**: Description
27
+ - File: `path/to/file.ts`
28
+ - Changes: What to modify
29
+ - Acceptance: How to verify
30
+
31
+ 2. **Task 2**: Description
32
+ ...
33
+
34
+ ## Files to Modify
35
+ - `path/to/file.ts` - what changes
36
+
37
+ ## New Files (if any)
38
+ - `path/to/new.ts` - purpose
39
+
40
+ ## Dependencies
41
+ Which tasks depend on others.
42
+
43
+ ## Risks
44
+ Anything to watch out for.
45
+
46
+ Keep the plan concrete. The worker agent will execute it.
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: researcher
3
+ description: Autonomous web researcher — searches, evaluates, and synthesizes a focused research brief
4
+ tools: read, write, web_search, fetch_content, get_search_content
5
+ model: anthropic/claude-sonnet-4-6
6
+ output: research.md
7
+ defaultProgress: true
8
+ ---
9
+
10
+ You are a research specialist. Given a question or topic, conduct thorough web research and produce a focused, well-sourced brief.
11
+
12
+ Process:
13
+ 1. Break the question into 2-4 searchable facets
14
+ 2. Search with `web_search` using `queries` (parallel, varied angles) and `curate: false`
15
+ 3. Read the answers. Identify what's well-covered, what has gaps, what's noise.
16
+ 4. For the 2-3 most promising source URLs, use `fetch_content` to get full page content
17
+ 5. Synthesize everything into a brief that directly answers the question
18
+
19
+ Search strategy — always vary your angles:
20
+ - Direct answer query (the obvious one)
21
+ - Authoritative source query (official docs, specs, primary sources)
22
+ - Practical experience query (case studies, benchmarks, real-world usage)
23
+ - Recent developments query (only if the topic is time-sensitive)
24
+
25
+ Evaluation — what to keep vs drop:
26
+ - Official docs and primary sources outweigh blog posts and forum threads
27
+ - Recent sources outweigh stale ones (check URL path for dates like /2025/01/)
28
+ - Sources that directly address the question outweigh tangentially related ones
29
+ - Diverse perspectives outweigh redundant coverage of the same point
30
+ - Drop: SEO filler, outdated info, beginner tutorials (unless that's the audience)
31
+
32
+ If the first round of searches doesn't fully answer the question, search again with refined queries targeting the gaps. Don't settle for partial answers when a follow-up search could fill them.
33
+
34
+ Output format (research.md):
35
+
36
+ # Research: [topic]
37
+
38
+ ## Summary
39
+ 2-3 sentence direct answer.
40
+
41
+ ## Findings
42
+ Numbered findings with inline source citations:
43
+ 1. **Finding** — explanation. [Source](url)
44
+ 2. **Finding** — explanation. [Source](url)
45
+
46
+ ## Sources
47
+ - Kept: Source Title (url) — why relevant
48
+ - Dropped: Source Title — why excluded
49
+
50
+ ## Gaps
51
+ What couldn't be answered. Suggested next steps.
@@ -0,0 +1,30 @@
1
+ ---
2
+ name: reviewer
3
+ description: Code review specialist that validates implementation and fixes issues
4
+ tools: read, grep, find, ls, bash
5
+ model: openai-codex/gpt-5.3-codex
6
+ thinking: high
7
+ defaultReads: plan.md, progress.md
8
+ defaultProgress: true
9
+ ---
10
+
11
+ You are a senior code reviewer. Analyze implementation against the plan.
12
+
13
+ When running in a chain, you'll receive instructions about which files to read (plan and progress) and where to update progress.
14
+
15
+ Bash is for read-only commands only: `git diff`, `git log`, `git show`.
16
+
17
+ Review checklist:
18
+ 1. Implementation matches plan requirements
19
+ 2. Code quality and correctness
20
+ 3. Edge cases handled
21
+ 4. Security considerations
22
+
23
+ If issues found, fix them directly.
24
+
25
+ Update progress.md with:
26
+
27
+ ## Review
28
+ - What's correct
29
+ - Fixed: Issue and resolution
30
+ - Note: Observations
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: scout
3
+ description: Fast codebase recon that returns compressed context for handoff
4
+ tools: read, grep, find, ls, bash, write
5
+ model: anthropic/claude-haiku-4-5
6
+ output: context.md
7
+ defaultProgress: true
8
+ ---
9
+
10
+ You are a scout. Quickly investigate a codebase and return structured findings.
11
+
12
+ When running in a chain, you'll receive instructions about where to write your output.
13
+ When running solo, write to the provided output path and summarize what you found.
14
+
15
+ Thoroughness (infer from task, default medium):
16
+ - Quick: Targeted lookups, key files only
17
+ - Medium: Follow imports, read critical sections
18
+ - Thorough: Trace all dependencies, check tests/types
19
+
20
+ Strategy:
21
+ 1. grep/find to locate relevant code
22
+ 2. Read key sections (not entire files)
23
+ 3. Identify types, interfaces, key functions
24
+ 4. Note dependencies between files
25
+
26
+ Your output format (context.md):
27
+
28
+ # Code Context
29
+
30
+ ## Files Retrieved
31
+ List with exact line ranges:
32
+ 1. `path/to/file.ts` (lines 10-50) - Description
33
+ 2. `path/to/other.ts` (lines 100-150) - Description
34
+
35
+ ## Key Code
36
+ Critical types, interfaces, or functions with actual code snippets.
37
+
38
+ ## Architecture
39
+ Brief explanation of how the pieces connect.
40
+
41
+ ## Start Here
42
+ Which file to look at first and why.
@@ -0,0 +1,32 @@
1
+ ---
2
+ name: worker
3
+ description: General-purpose subagent with full capabilities, isolated context
4
+ model: claude-sonnet-4-6
5
+ defaultReads: context.md, plan.md
6
+ defaultProgress: true
7
+ ---
8
+
9
+ You are a worker agent with full capabilities. You operate in an isolated context window.
10
+
11
+ When running in a chain, you'll receive instructions about:
12
+ - Which files to read (context from previous steps)
13
+ - Where to maintain progress tracking
14
+
15
+ Work autonomously to complete the assigned task. Use all available tools as needed.
16
+
17
+ Progress.md format:
18
+
19
+ # Progress
20
+
21
+ ## Status
22
+ [In Progress | Completed | Blocked]
23
+
24
+ ## Tasks
25
+ - [x] Completed task
26
+ - [ ] Current task
27
+
28
+ ## Files Changed
29
+ - `path/to/file.ts` - what changed
30
+
31
+ ## Notes
32
+ Any blockers or decisions.
package/agents.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
+ import { fileURLToPath } from "node:url";
8
9
  import { KNOWN_FIELDS } from "./agent-serializer.js";
9
10
  import { parseChain } from "./chain-serializer.js";
10
11
  import { mergeAgentsForScope } from "./agent-selection.js";
@@ -242,7 +243,7 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
242
243
  }
243
244
  }
244
245
 
245
- const BUILTIN_AGENTS_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent", "agents");
246
+ const BUILTIN_AGENTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "agents");
246
247
 
247
248
  export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
248
249
  const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
@@ -257,6 +258,7 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
257
258
  }
258
259
 
259
260
  export function discoverAgentsAll(cwd: string): {
261
+ builtin: AgentConfig[];
260
262
  user: AgentConfig[];
261
263
  project: AgentConfig[];
262
264
  chains: ChainConfig[];
@@ -266,6 +268,7 @@ export function discoverAgentsAll(cwd: string): {
266
268
  const userDir = path.join(os.homedir(), ".pi", "agent", "agents");
267
269
  const projectDir = findNearestProjectAgentsDir(cwd);
268
270
 
271
+ const builtin = loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin");
269
272
  const user = loadAgentsFromDir(userDir, "user");
270
273
  const project = projectDir ? loadAgentsFromDir(projectDir, "project") : [];
271
274
  const chains = [
@@ -273,5 +276,5 @@ export function discoverAgentsAll(cwd: string): {
273
276
  ...(projectDir ? loadChainsFromDir(projectDir, "project") : []),
274
277
  ];
275
278
 
276
- return { user, project, chains, userDir, projectDir };
279
+ return { builtin, user, project, chains, userDir, projectDir };
277
280
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "Pi extension for delegating tasks to subagents with chains, parallel execution, and TUI clarification",
5
5
  "author": "Nico Bailon",
6
6
  "license": "MIT",
@@ -29,6 +29,7 @@
29
29
  "*.ts",
30
30
  "!*.test.ts",
31
31
  "*.mjs",
32
+ "agents/",
32
33
  "README.md",
33
34
  "CHANGELOG.md"
34
35
  ],
package/render.ts CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  import type { AgentToolResult } from "@mariozechner/pi-agent-core";
6
6
  import { getMarkdownTheme, type ExtensionContext } from "@mariozechner/pi-coding-agent";
7
- import { Container, Markdown, Spacer, Text, type Widget } from "@mariozechner/pi-tui";
7
+ import { Container, Markdown, Spacer, Text, truncateToWidth, visibleWidth, type Widget } from "@mariozechner/pi-tui";
8
8
  import {
9
9
  type AsyncJobState,
10
10
  type Details,
@@ -16,6 +16,15 @@ import { getFinalOutput, getDisplayItems, getOutputTail, getLastActivity } from
16
16
 
17
17
  type Theme = ExtensionContext["ui"]["theme"];
18
18
 
19
+ function getTermWidth(): number {
20
+ return process.stdout.columns || 120;
21
+ }
22
+
23
+ function truncLine(text: string, maxWidth: number): string {
24
+ if (visibleWidth(text) <= maxWidth) return text;
25
+ return truncateToWidth(text, maxWidth - 1) + "…";
26
+ }
27
+
19
28
  // Track last rendered widget state to avoid no-op re-renders
20
29
  let lastWidgetHash = "";
21
30
 
@@ -67,6 +76,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
67
76
  lastWidgetHash = newHash;
68
77
 
69
78
  const theme = ctx.ui.theme;
79
+ const w = getTermWidth();
70
80
  const lines: string[] = [];
71
81
  lines.push(theme.fg("accent", "Async subagents"));
72
82
 
@@ -90,12 +100,12 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
90
100
  const activityText = job.status === "running" ? getLastActivity(job.outputFile) : "";
91
101
  const activitySuffix = activityText ? ` | ${theme.fg("dim", activityText)}` : "";
92
102
 
93
- lines.push(`- ${id} ${status} | ${agentLabel} | ${stepText}${elapsed ? ` | ${elapsed}` : ""}${tokenText}${activitySuffix}`);
103
+ lines.push(truncLine(`- ${id} ${status} | ${agentLabel} | ${stepText}${elapsed ? ` | ${elapsed}` : ""}${tokenText}${activitySuffix}`, w));
94
104
 
95
105
  if (job.status === "running" && job.outputFile) {
96
106
  const tail = getOutputTail(job.outputFile, 3);
97
107
  for (const line of tail) {
98
- lines.push(theme.fg("dim", ` > ${line}`));
108
+ lines.push(truncLine(theme.fg("dim", ` > ${line}`), w));
99
109
  }
100
110
  }
101
111
  }
@@ -114,7 +124,8 @@ export function renderSubagentResult(
114
124
  const d = result.details;
115
125
  if (!d || !d.results.length) {
116
126
  const t = result.content[0];
117
- return new Text(t?.type === "text" ? t.text : "(no output)", 0, 0);
127
+ const text = t?.type === "text" ? t.text : "(no output)";
128
+ return new Text(truncLine(text, getTermWidth() - 4), 0, 0);
118
129
  }
119
130
 
120
131
  const mdTheme = getMarkdownTheme();
@@ -135,37 +146,42 @@ export function renderSubagentResult(
135
146
  ? ` | ${r.progressSummary.toolCount} tools, ${formatTokens(r.progressSummary.tokens)} tok, ${formatDuration(r.progressSummary.durationMs)}`
136
147
  : "";
137
148
 
149
+ const w = getTermWidth() - 4;
138
150
  const c = new Container();
139
- c.addChild(new Text(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${progressInfo}`, 0, 0));
151
+ c.addChild(new Text(truncLine(`${icon} ${theme.fg("toolTitle", theme.bold(r.agent))}${progressInfo}`, w), 0, 0));
140
152
  c.addChild(new Spacer(1));
153
+ const taskMaxLen = Math.max(20, w - 8);
154
+ const taskPreview = r.task.length > taskMaxLen
155
+ ? `${r.task.slice(0, taskMaxLen)}...`
156
+ : r.task;
141
157
  c.addChild(
142
- new Text(theme.fg("dim", `Task: ${r.task.slice(0, 150)}${r.task.length > 150 ? "..." : ""}`), 0, 0),
158
+ new Text(truncLine(theme.fg("dim", `Task: ${taskPreview}`), w), 0, 0),
143
159
  );
144
160
  c.addChild(new Spacer(1));
145
161
 
146
162
  const items = getDisplayItems(r.messages);
147
163
  for (const item of items) {
148
164
  if (item.type === "tool")
149
- c.addChild(new Text(theme.fg("muted", formatToolCall(item.name, item.args)), 0, 0));
165
+ c.addChild(new Text(truncLine(theme.fg("muted", formatToolCall(item.name, item.args)), w), 0, 0));
150
166
  }
151
167
  if (items.length) c.addChild(new Spacer(1));
152
168
 
153
169
  if (output) c.addChild(new Markdown(output, 0, 0, mdTheme));
154
170
  c.addChild(new Spacer(1));
155
171
  if (r.skills?.length) {
156
- c.addChild(new Text(theme.fg("dim", `Skills: ${r.skills.join(", ")}`), 0, 0));
172
+ c.addChild(new Text(truncLine(theme.fg("dim", `Skills: ${r.skills.join(", ")}`), w), 0, 0));
157
173
  }
158
174
  if (r.skillsWarning) {
159
- c.addChild(new Text(theme.fg("warning", `⚠️ ${r.skillsWarning}`), 0, 0));
175
+ c.addChild(new Text(truncLine(theme.fg("warning", `⚠️ ${r.skillsWarning}`), w), 0, 0));
160
176
  }
161
- c.addChild(new Text(theme.fg("dim", formatUsage(r.usage, r.model)), 0, 0));
177
+ c.addChild(new Text(truncLine(theme.fg("dim", formatUsage(r.usage, r.model)), w), 0, 0));
162
178
  if (r.sessionFile) {
163
- c.addChild(new Text(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`), 0, 0));
179
+ c.addChild(new Text(truncLine(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`), w), 0, 0));
164
180
  }
165
181
 
166
182
  if (r.artifactPaths) {
167
183
  c.addChild(new Spacer(1));
168
- c.addChild(new Text(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`), 0, 0));
184
+ c.addChild(new Text(truncLine(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`), w), 0, 0));
169
185
  }
170
186
  return c;
171
187
  }
@@ -231,7 +247,7 @@ export function renderSubagentResult(
231
247
  && Boolean(isComplete)
232
248
  && hasEmptyTextOutputWithoutOutputTarget(result.task, getFinalOutput(result.messages));
233
249
  const isCurrent = i === (d.currentStepIndex ?? d.results.length);
234
- const icon = isFailed
250
+ const stepIcon = isFailed
235
251
  ? theme.fg("error", "✗")
236
252
  : isEmptyWithoutTarget
237
253
  ? theme.fg("warning", "⚠")
@@ -240,22 +256,23 @@ export function renderSubagentResult(
240
256
  : isCurrent && hasRunning
241
257
  ? theme.fg("warning", "●")
242
258
  : theme.fg("dim", "○");
243
- return `${icon} ${agent}`;
259
+ return `${stepIcon} ${agent}`;
244
260
  })
245
261
  .join(theme.fg("dim", " → "))
246
262
  : null;
247
263
 
264
+ const w = getTermWidth() - 4;
248
265
  const c = new Container();
249
266
  c.addChild(
250
267
  new Text(
251
- `${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${stepInfo}${summaryStr}`,
268
+ truncLine(`${icon} ${theme.fg("toolTitle", theme.bold(modeLabel))}${stepInfo}${summaryStr}`, w),
252
269
  0,
253
270
  0,
254
271
  ),
255
272
  );
256
273
  // Show chain visualization
257
274
  if (chainVis) {
258
- c.addChild(new Text(` ${chainVis}`, 0, 0));
275
+ c.addChild(new Text(truncLine(` ${chainVis}`, w), 0, 0));
259
276
  }
260
277
 
261
278
  // === STATIC STEP LAYOUT (like clarification UI) ===
@@ -275,7 +292,7 @@ export function renderSubagentResult(
275
292
 
276
293
  if (!r) {
277
294
  // Pending step
278
- c.addChild(new Text(theme.fg("dim", ` Step ${i + 1}: ${agentName}`), 0, 0));
295
+ c.addChild(new Text(truncLine(theme.fg("dim", ` Step ${i + 1}: ${agentName}`), w), 0, 0));
279
296
  c.addChild(new Text(theme.fg("dim", ` status: ○ pending`), 0, 0));
280
297
  c.addChild(new Spacer(1));
281
298
  continue;
@@ -299,45 +316,46 @@ export function renderSubagentResult(
299
316
  const stepHeader = rRunning
300
317
  ? `${statusIcon} Step ${i + 1}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
301
318
  : `${statusIcon} Step ${i + 1}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
302
- c.addChild(new Text(stepHeader, 0, 0));
319
+ c.addChild(new Text(truncLine(stepHeader, w), 0, 0));
303
320
 
304
- const taskPreview = r.task.slice(0, 120) + (r.task.length > 120 ? "..." : "");
305
- c.addChild(new Text(theme.fg("dim", ` task: ${taskPreview}`), 0, 0));
321
+ const taskMaxLen = Math.max(20, w - 12);
322
+ const taskPreview = r.task.slice(0, taskMaxLen) + (r.task.length > taskMaxLen ? "..." : "");
323
+ c.addChild(new Text(truncLine(theme.fg("dim", ` task: ${taskPreview}`), w), 0, 0));
306
324
 
307
325
  const outputTarget = extractOutputTarget(r.task);
308
326
  if (outputTarget) {
309
- c.addChild(new Text(theme.fg("dim", ` output: ${outputTarget}`), 0, 0));
327
+ c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${outputTarget}`), w), 0, 0));
310
328
  }
311
329
 
312
330
  if (r.skills?.length) {
313
- c.addChild(new Text(theme.fg("dim", ` skills: ${r.skills.join(", ")}`), 0, 0));
331
+ c.addChild(new Text(truncLine(theme.fg("dim", ` skills: ${r.skills.join(", ")}`), w), 0, 0));
314
332
  }
315
333
  if (r.skillsWarning) {
316
- c.addChild(new Text(theme.fg("warning", ` ⚠️ ${r.skillsWarning}`), 0, 0));
334
+ c.addChild(new Text(truncLine(theme.fg("warning", ` ⚠️ ${r.skillsWarning}`), w), 0, 0));
317
335
  }
318
336
 
319
337
  if (rRunning && rProg) {
320
338
  if (rProg.skills?.length) {
321
- c.addChild(new Text(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`), 0, 0));
339
+ c.addChild(new Text(truncLine(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`), w), 0, 0));
322
340
  }
323
341
  // Current tool for running step
324
342
  if (rProg.currentTool) {
325
343
  const toolLine = rProg.currentToolArgs
326
344
  ? `${rProg.currentTool}: ${rProg.currentToolArgs.slice(0, 100)}${rProg.currentToolArgs.length > 100 ? "..." : ""}`
327
345
  : rProg.currentTool;
328
- c.addChild(new Text(theme.fg("warning", ` > ${toolLine}`), 0, 0));
346
+ c.addChild(new Text(truncLine(theme.fg("warning", ` > ${toolLine}`), w), 0, 0));
329
347
  }
330
348
  // Recent tools
331
349
  if (rProg.recentTools?.length) {
332
350
  for (const t of rProg.recentTools.slice(0, 3)) {
333
351
  const args = t.args.slice(0, 90) + (t.args.length > 90 ? "..." : "");
334
- c.addChild(new Text(theme.fg("dim", ` ${t.tool}: ${args}`), 0, 0));
352
+ c.addChild(new Text(truncLine(theme.fg("dim", ` ${t.tool}: ${args}`), w), 0, 0));
335
353
  }
336
354
  }
337
355
  // Recent output (limited)
338
356
  const recentLines = (rProg.recentOutput ?? []).slice(-5);
339
357
  for (const line of recentLines) {
340
- c.addChild(new Text(theme.fg("dim", ` ${line.slice(0, 100)}${line.length > 100 ? "..." : ""}`), 0, 0));
358
+ c.addChild(new Text(truncLine(theme.fg("dim", ` ${line.slice(0, 100)}${line.length > 100 ? "..." : ""}`), w), 0, 0));
341
359
  }
342
360
  }
343
361
 
@@ -346,7 +364,7 @@ export function renderSubagentResult(
346
364
 
347
365
  if (d.artifacts) {
348
366
  c.addChild(new Spacer(1));
349
- c.addChild(new Text(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`), 0, 0));
367
+ c.addChild(new Text(truncLine(theme.fg("dim", `Artifacts dir: ${shortenPath(d.artifacts.dir)}`), w), 0, 0));
350
368
  }
351
369
  return c;
352
370
  }