pi-subagents 0.16.1 → 0.17.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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.17.0] - 2026-04-16
6
+
7
+ ### Added
8
+ - Builtin agents can now be disabled through `subagents.agentOverrides.<name>.disabled` or the bulk `subagents.disableBuiltins` setting, with `/agents` keeping disabled builtins visible so they can be re-enabled from the manager. This builds on PR `#81`. Thanks @danielcherubini.
9
+
10
+ ### Fixed
11
+ - Builtin disable precedence is now coherent across user and project settings: project overrides beat user overrides, project bulk disable beats user re-enable attempts, and same-scope per-agent overrides can opt an agent out of bulk disable.
12
+ - `/agents` now blocks launching disabled builtins, shows their disabled state in list/detail views and management output, and avoids exposing the builtin-only `disabled` field when editing normal user/project agents.
13
+ - Multi-agent chain launches from `/agents` now collect a task before dispatching instead of emitting an empty task, and settings read failures now surface as read errors instead of being mislabeled as parse failures.
14
+
5
15
  ## [0.16.1] - 2026-04-16
6
16
 
7
17
  ### Changed
package/README.md CHANGED
@@ -53,7 +53,7 @@ You can also override selected builtin fields without copying the whole agent. B
53
53
  - User scope: `~/.pi/agent/settings.json`
54
54
  - Project scope: `.pi/settings.json`
55
55
 
56
- Supported builtin override fields are `model`, `fallbackModels`, `thinking`, `systemPromptMode`, `inheritProjectContext`, `inheritSkills`, `skills`, `tools`, and `systemPrompt`. Project overrides beat user overrides.
56
+ Supported builtin override fields are `model`, `fallbackModels`, `thinking`, `systemPromptMode`, `inheritProjectContext`, `inheritSkills`, `disabled`, `skills`, `tools`, and `systemPrompt`. Project overrides beat user overrides.
57
57
 
58
58
  **Overriding builtin defaults:**
59
59
 
@@ -74,7 +74,9 @@ All builtin agents inherit project instruction files (`AGENTS.md`, `CLAUDE.md`,
74
74
 
75
75
  **Via `/agents` UI** — press `e` on any builtin agent (like `reviewer`), then toggle `inheritProjectContext` to `false` and save. This creates an override that you can edit or remove later without modifying the builtin definition itself.
76
76
 
77
- Overridden builtins show badges like `[builtin+user]` or `[builtin+project]` to indicate they have customizations applied.
77
+ Set `disabled: true` inside an agent override to hide one builtin from runtime discovery while still keeping it visible in `/agents` so you can re-enable it later. For bulk control, set `subagents.disableBuiltins: true` in `settings.json`; project scope beats user scope, and an explicit same-scope override can opt one builtin back in with `disabled: false` or any other override fields.
78
+
79
+ Overridden builtins show badges like `[builtin+user]` or `[builtin+project]`. Disabled builtins show `off` badges in the manager so they are easy to spot and re-enable.
78
80
 
79
81
  > **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`.
80
82
 
@@ -338,6 +338,7 @@ export function formatAgentDetail(agent: AgentConfig): string {
338
338
  lines.push(`System prompt mode: ${agent.systemPromptMode}`);
339
339
  lines.push(`Inherit project context: ${agent.inheritProjectContext ? "true" : "false"}`);
340
340
  lines.push(`Inherit skills: ${agent.inheritSkills ? "true" : "false"}`);
341
+ if (agent.source === "builtin") lines.push(`Disabled: ${agent.disabled ? "true" : "false"}`);
341
342
  if (agent.extensions !== undefined) lines.push(`Extensions: ${agent.extensions.length ? agent.extensions.join(", ") : "(none)"}`);
342
343
  if (agent.thinking) lines.push(`Thinking: ${agent.thinking}`);
343
344
  if (agent.output) lines.push(`Output: ${agent.output}`);
@@ -371,7 +372,7 @@ export function handleList(params: ManagementParams, ctx: ManagementContext): Ag
371
372
  const d = discoverAgentsAll(ctx.cwd);
372
373
  const agents = allAgents(d).filter((a) => scope === "both" || a.source === "builtin" || a.source === scope).sort((a, b) => a.name.localeCompare(b.name));
373
374
  const chains = d.chains.filter((c) => scope === "both" || c.source === scope).sort((a, b) => a.name.localeCompare(b.name));
374
- 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)"])];
375
+ const lines = ["Agents:", ...(agents.length ? agents.map((a) => `- ${a.name} (${a.source}${a.disabled ? ", disabled" : ""}): ${a.description}`) : ["- (none)"]), "", "Chains:", ...(chains.length ? chains.map((c) => `- ${c.name} (${c.source}): ${c.description}`) : ["- (none)"])];
375
376
  return result(lines.join("\n"));
376
377
  }
377
378
 
@@ -64,6 +64,9 @@ function buildDetailLines(
64
64
  lines.push(renderFieldLine("Prompt mode:", agent.systemPromptMode, contentWidth, theme));
65
65
  lines.push(renderFieldLine("Project ctx:", agent.inheritProjectContext ? "on" : "off", contentWidth, theme));
66
66
  lines.push(renderFieldLine("Skills ctx:", agent.inheritSkills ? "on" : "off", contentWidth, theme));
67
+ if (agent.source === "builtin") {
68
+ lines.push(renderFieldLine("Disabled:", agent.disabled ? "on" : "off", contentWidth, theme));
69
+ }
67
70
  if (agent.override) {
68
71
  const overrideLabel = `${agent.override.scope} · ${formatPath(agent.override.path)}`;
69
72
  lines.push(renderFieldLine("Override:", overrideLabel, contentWidth, theme));
@@ -139,7 +142,9 @@ export function renderDetail(
139
142
  ): string[] {
140
143
  const lines: string[] = [];
141
144
  const scopeBadge = agent.source === "builtin"
142
- ? (agent.override ? `[builtin+${agent.override.scope}]` : "[builtin]")
145
+ ? (agent.disabled
146
+ ? (agent.override ? `[builtin off+${agent.override.scope}]` : "[builtin off]")
147
+ : (agent.override ? `[builtin+${agent.override.scope}]` : "[builtin]"))
143
148
  : agent.source === "project"
144
149
  ? "[proj]"
145
150
  : "[user]";
@@ -164,8 +169,12 @@ export function renderDetail(
164
169
 
165
170
  const footer = agent.source === "builtin"
166
171
  ? agent.override
167
- ? " [l]aunch [e]dit override [v] raw/resolved [↑↓] scroll [esc] back "
168
- : " [l]aunch [e]create override [v] raw/resolved [↑↓] scroll [esc] back "
172
+ ? (agent.disabled
173
+ ? " [e]dit override [v] raw/resolved [↑↓] scroll [esc] back "
174
+ : " [l]aunch [e]dit override [v] raw/resolved [↑↓] scroll [esc] back ")
175
+ : (agent.disabled
176
+ ? " [e]create override [v] raw/resolved [↑↓] scroll [esc] back "
177
+ : " [l]aunch [e]create override [v] raw/resolved [↑↓] scroll [esc] back ")
169
178
  : " [l]aunch [e]dit [v] raw/resolved [↑↓] scroll [esc] back ";
170
179
  lines.push(renderFooter(footer, width, theme));
171
180
  return lines;
@@ -48,6 +48,7 @@ function fieldValueMatchesBase(field: EditField, state: EditState): boolean {
48
48
  case "systemPromptMode": return state.draft.systemPromptMode === base.systemPromptMode;
49
49
  case "inheritProjectContext": return state.draft.inheritProjectContext === base.inheritProjectContext;
50
50
  case "inheritSkills": return state.draft.inheritSkills === base.inheritSkills;
51
+ case "disabled": return state.draft.disabled === base.disabled;
51
52
  case "tools": return arraysEqual(toolList(state.draft), toolList(base));
52
53
  case "skills": return arraysEqual(state.draft.skills, base.skills);
53
54
  case "prompt": return state.draft.systemPrompt === base.systemPrompt;
@@ -65,6 +66,7 @@ function resetFieldToBase(field: EditField, state: EditState): void {
65
66
  case "systemPromptMode": state.draft.systemPromptMode = base.systemPromptMode; break;
66
67
  case "inheritProjectContext": state.draft.inheritProjectContext = base.inheritProjectContext; break;
67
68
  case "inheritSkills": state.draft.inheritSkills = base.inheritSkills; break;
69
+ case "disabled": state.draft.disabled = base.disabled; break;
68
70
  case "tools": state.draft.tools = base.tools ? [...base.tools] : undefined; state.draft.mcpDirectTools = base.mcpDirectTools ? [...base.mcpDirectTools] : undefined; break;
69
71
  case "skills": state.draft.skills = base.skills ? [...base.skills] : undefined; break;
70
72
  case "prompt": state.draft.systemPrompt = base.systemPrompt; state.promptEditor = createEditorState(base.systemPrompt); break;
@@ -92,6 +94,7 @@ function renderFieldValue(field: EditField, state: EditState): string {
92
94
  case "systemPromptMode": return draft.systemPromptMode ?? defaultSystemPromptMode(draft.name);
93
95
  case "inheritProjectContext": return draft.inheritProjectContext ? "on" : "off";
94
96
  case "inheritSkills": return draft.inheritSkills ? "on" : "off";
97
+ case "disabled": return draft.disabled ? "on" : "off";
95
98
  case "tools": return formatTools(draft);
96
99
  case "extensions": return draft.extensions !== undefined ? (draft.extensions.length > 0 ? draft.extensions.join(", ") : "") : "(all)";
97
100
  case "skills": return draft.skills && draft.skills.length > 0 ? draft.skills.join(", ") : "";
@@ -128,6 +131,7 @@ function applyFieldValue(field: EditField, state: EditState, value: string): voi
128
131
  case "reads": draft.defaultReads = parseCommaList(value); break;
129
132
  case "inheritProjectContext":
130
133
  case "inheritSkills":
134
+ case "disabled":
131
135
  case "progress":
132
136
  case "interactive":
133
137
  case "prompt":
@@ -256,9 +260,10 @@ export function handleEditInput(screen: EditScreen, state: EditState, data: stri
256
260
  if (data === "m") { openModelPicker(state, models); return { nextScreen: "edit-field" }; }
257
261
  if (data === "t") { openThinkingPicker(state); return { nextScreen: "edit-field" }; }
258
262
  if (data === "s") { openSkillPicker(state, skills); return { nextScreen: "edit-field" }; }
259
- if (data === " " && (field === "inheritProjectContext" || field === "inheritSkills" || field === "progress" || field === "interactive")) {
263
+ if (data === " " && (field === "inheritProjectContext" || field === "inheritSkills" || field === "disabled" || field === "progress" || field === "interactive")) {
260
264
  if (field === "inheritProjectContext") state.draft.inheritProjectContext = !state.draft.inheritProjectContext;
261
265
  if (field === "inheritSkills") state.draft.inheritSkills = !state.draft.inheritSkills;
266
+ if (field === "disabled") state.draft.disabled = !state.draft.disabled;
262
267
  if (field === "progress") state.draft.defaultProgress = !state.draft.defaultProgress;
263
268
  if (field === "interactive") state.draft.interactive = !state.draft.interactive;
264
269
  return;
@@ -268,7 +273,7 @@ export function handleEditInput(screen: EditScreen, state: EditState, data: stri
268
273
  if (field === "thinking") { openThinkingPicker(state); return { nextScreen: "edit-field" }; }
269
274
  if (field === "skills") { openSkillPicker(state, skills); return { nextScreen: "edit-field" }; }
270
275
  if (field === "prompt") { state.promptEditor = createEditorState(state.draft.systemPrompt ?? ""); return { nextScreen: "edit-prompt" }; }
271
- if (field === "inheritProjectContext" || field === "inheritSkills" || field === "progress" || field === "interactive") return;
276
+ if (field === "inheritProjectContext" || field === "inheritSkills" || field === "disabled" || field === "progress" || field === "interactive") return;
272
277
  state.fieldMode = "text"; state.fieldEditor = createEditorState(renderFieldValue(field, state)); return { nextScreen: "edit-field" };
273
278
  }
274
279
  return;
@@ -341,11 +346,14 @@ export function renderEdit(screen: EditScreen, state: EditState, width: number,
341
346
  ? "Project Ctx"
342
347
  : field === "inheritSkills"
343
348
  ? "Skills Ctx"
349
+ : field === "disabled"
350
+ ? "Disabled"
344
351
  : `${field[0]!.toUpperCase()}${field.slice(1)}`;
345
352
  const rawLabel = pad(`${fieldLabel}:`, labelWidth);
346
353
  const labelText = state.overrideBase && !fieldValueMatchesBase(field, state) ? theme.fg("accent", rawLabel) : rawLabel; let valueText = renderFieldValue(field, state);
347
354
  if (field === "inheritProjectContext") { const toggle = state.draft.inheritProjectContext ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.inheritProjectContext ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
348
355
  if (field === "inheritSkills") { const toggle = state.draft.inheritSkills ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.inheritSkills ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
356
+ if (field === "disabled") { const toggle = state.draft.disabled ? theme.fg("warning", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.disabled ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
349
357
  if (field === "progress") { const toggle = state.draft.defaultProgress ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.defaultProgress ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
350
358
  if (field === "interactive") { const toggle = state.draft.interactive ? theme.fg("success", "[x]") : "[ ]"; valueText = `${toggle} ${state.draft.interactive ? "on" : "off"}`; lines.push(row(` ${prefix}${labelText} ${pad(truncateToWidth(valueText, valueWidth), valueWidth)}`, width, theme)); continue; }
351
359
  let displayValue = truncateToWidth(valueText, valueWidth);
@@ -10,6 +10,7 @@ export interface ListAgent {
10
10
  model?: string;
11
11
  source: AgentSource;
12
12
  overrideScope?: "user" | "project";
13
+ disabled?: boolean;
13
14
  kind: "agent" | "chain";
14
15
  stepCount?: number;
15
16
  }
@@ -191,7 +192,7 @@ export function renderList(
191
192
  const innerW = width - 2;
192
193
  const nameWidth = 16;
193
194
  const modelWidth = 12;
194
- const scopeWidth = 17;
195
+ const scopeWidth = 21;
195
196
 
196
197
  for (let i = 0; i < visible.length; i++) {
197
198
  const agent = visible[i]!;
@@ -212,7 +213,9 @@ export function renderList(
212
213
  const scopeLabel = agent.kind === "chain"
213
214
  ? "[chain]"
214
215
  : agent.source === "builtin"
215
- ? (agent.overrideScope ? `[builtin+${agent.overrideScope}]` : "[builtin]")
216
+ ? (agent.disabled
217
+ ? (agent.overrideScope ? `[builtin off+${agent.overrideScope}]` : "[builtin off]")
218
+ : (agent.overrideScope ? `[builtin+${agent.overrideScope}]` : "[builtin]"))
216
219
  : agent.source === "project"
217
220
  ? "[proj]"
218
221
  : "[user]";
package/agent-manager.ts CHANGED
@@ -43,7 +43,7 @@ interface NameInputState { mode: "new-agent" | "clone-agent" | "clone-chain" | "
43
43
  interface StatusMessage { text: string; type: "error" | "info"; }
44
44
  interface OverrideScopeState { selectedScope: "user" | "project"; allowProject: boolean; }
45
45
 
46
- const BUILTIN_OVERRIDE_FIELDS: EditField[] = ["model", "fallbackModels", "thinking", "systemPromptMode", "inheritProjectContext", "inheritSkills", "tools", "skills", "prompt"];
46
+ const BUILTIN_OVERRIDE_FIELDS: EditField[] = ["model", "fallbackModels", "thinking", "systemPromptMode", "inheritProjectContext", "inheritSkills", "disabled", "tools", "skills", "prompt"];
47
47
 
48
48
  function cloneConfig(config: AgentConfig): AgentConfig {
49
49
  return {
@@ -59,6 +59,7 @@ function cloneConfig(config: AgentConfig): AgentConfig {
59
59
  ...config.override,
60
60
  base: {
61
61
  ...config.override.base,
62
+ disabled: config.override.base.disabled,
62
63
  fallbackModels: config.override.base.fallbackModels ? [...config.override.base.fallbackModels] : undefined,
63
64
  skills: config.override.base.skills ? [...config.override.base.skills] : undefined,
64
65
  tools: config.override.base.tools ? [...config.override.base.tools] : undefined,
@@ -124,8 +125,9 @@ export class AgentManagerComponent implements Component {
124
125
 
125
126
  private getAgentEntry(id: string | null): AgentEntry | undefined { if (!id) return undefined; return this.agents.find((entry) => entry.id === id); }
126
127
  private getChainEntry(id: string | null): ChainEntry | undefined { if (!id) return undefined; return this.chains.find((entry) => entry.id === id); }
127
- private listAgents(): ListAgent[] { const a = this.agents.map((entry) => ({ id: entry.id, name: entry.config.name, description: entry.config.description, model: entry.config.model, source: entry.config.source, overrideScope: entry.config.override?.scope, kind: "agent" as const })); const c = this.chains.map((entry) => ({ id: entry.id, name: entry.config.name, description: entry.config.description, source: entry.config.source, kind: "chain" as const, stepCount: entry.config.steps.length })); return [...a, ...c]; }
128
+ private listAgents(): ListAgent[] { const a = this.agents.map((entry) => ({ id: entry.id, name: entry.config.name, description: entry.config.description, model: entry.config.model, source: entry.config.source, overrideScope: entry.config.override?.scope, disabled: entry.config.disabled, kind: "agent" as const })); const c = this.chains.map((entry) => ({ id: entry.id, name: entry.config.name, description: entry.config.description, source: entry.config.source, kind: "chain" as const, stepCount: entry.config.steps.length })); return [...a, ...c]; }
128
129
  private clearStatus(): void { this.statusMessage = undefined; }
130
+ private disabledAgentEntries(ids: string[]): AgentEntry[] { return ids.map((id) => this.getAgentEntry(id)).filter((entry): entry is AgentEntry => Boolean(entry?.config.disabled)); }
129
131
 
130
132
  private resolveBuiltinOverrideBase(entry: AgentEntry): BuiltinAgentOverrideBase {
131
133
  if (entry.config.override) return entry.config.override.base;
@@ -136,6 +138,7 @@ export class AgentManagerComponent implements Component {
136
138
  systemPromptMode: entry.config.systemPromptMode,
137
139
  inheritProjectContext: entry.config.inheritProjectContext,
138
140
  inheritSkills: entry.config.inheritSkills,
141
+ disabled: entry.config.disabled,
139
142
  systemPrompt: entry.config.systemPrompt,
140
143
  skills: entry.config.skills ? [...entry.config.skills] : undefined,
141
144
  tools: entry.config.tools ? [...entry.config.tools] : undefined,
@@ -185,11 +188,6 @@ export class AgentManagerComponent implements Component {
185
188
  this.screen = "parallel-builder";
186
189
  }
187
190
  private enterTaskInput(ids: string[], backScreen: ManagerScreen = "list"): void {
188
- if (ids.length > 1) {
189
- const names = ids.map((id) => { const e = this.getAgentEntry(id); return e ? e.config.name : id; });
190
- this.done({ action: "chain", agents: names, task: "", skipClarify: false });
191
- return;
192
- }
193
191
  this.chainAgentIds = ids; this.chainLaunchId = null; this.parallelMode = false; this.taskBackScreen = backScreen; this.taskEditor = createEditorState(); this.skipClarify = true; this.screen = "task-input";
194
192
  }
195
193
  private enterSavedChainLaunch(entry: ChainEntry): void { this.chainLaunchId = entry.id; this.chainAgentIds = []; this.parallelMode = false; this.taskBackScreen = "chain-detail"; this.taskEditor = createEditorState(); this.skipClarify = true; this.screen = "task-input"; }
@@ -507,6 +505,13 @@ export class AgentManagerComponent implements Component {
507
505
  const tasks = this.parallelState.slots.map((slot) => ({ agent: slot.agentName, task: slot.customTask || sharedTask }));
508
506
  this.done({ action: "parallel", tasks, skipClarify: this.skipClarify }); return;
509
507
  }
508
+ if (this.chainAgentIds.length > 1) {
509
+ const agents = this.chainAgentIds
510
+ .map((id) => this.getAgentEntry(id)?.config.name)
511
+ .filter((name): name is string => Boolean(name));
512
+ if (agents.length !== this.chainAgentIds.length) { this.screen = "list"; this.tui.requestRender(); return; }
513
+ this.done({ action: "chain", agents, task: this.taskEditor.buffer, skipClarify: this.skipClarify }); return;
514
+ }
510
515
  const name = this.getAgentEntry(this.chainAgentIds[0] ?? null)?.config.name;
511
516
  if (!name) { this.screen = "list"; this.tui.requestRender(); return; }
512
517
  this.done({ action: "launch", agent: name, task: this.taskEditor.buffer, skipClarify: this.skipClarify }); return;
@@ -562,8 +567,24 @@ export class AgentManagerComponent implements Component {
562
567
  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;
563
568
  case "new": this.enterTemplateSelect(); return;
564
569
  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; }
565
- case "run-chain": this.enterTaskInput(action.ids); return;
566
- case "run-parallel": this.enterParallelBuilder(action.ids); return;
570
+ case "run-chain": {
571
+ const disabled = this.disabledAgentEntries(action.ids);
572
+ if (disabled.length > 0) {
573
+ this.statusMessage = { text: `Disabled builtin agents cannot run: ${disabled.map((entry) => entry.config.name).join(", ")}. Edit the override to re-enable them.`, type: "error" };
574
+ return;
575
+ }
576
+ this.enterTaskInput(action.ids);
577
+ return;
578
+ }
579
+ case "run-parallel": {
580
+ const disabled = this.disabledAgentEntries(action.ids);
581
+ if (disabled.length > 0) {
582
+ this.statusMessage = { text: `Disabled builtin agents cannot run: ${disabled.map((entry) => entry.config.name).join(", ")}. Edit the override to re-enable them.`, type: "error" };
583
+ return;
584
+ }
585
+ this.enterParallelBuilder(action.ids);
586
+ return;
587
+ }
567
588
  case "close": this.done(undefined); return;
568
589
  }
569
590
  }
@@ -579,7 +600,11 @@ export class AgentManagerComponent implements Component {
579
600
  this.enterEdit(entry);
580
601
  return;
581
602
  }
582
- if (action.type === "launch") { this.enterTaskInput([entry.id], "detail"); return; }
603
+ if (action.type === "launch") {
604
+ if (entry.config.disabled) return;
605
+ this.enterTaskInput([entry.id], "detail");
606
+ return;
607
+ }
583
608
  }
584
609
 
585
610
  private handleChainDetailAction(action: ChainDetailAction, entry: ChainEntry): void {
@@ -605,6 +630,12 @@ export class AgentManagerComponent implements Component {
605
630
  case "task-input": {
606
631
  if (this.chainLaunchId) { const entry = this.getChainEntry(this.chainLaunchId); const title = entry ? `Chain: ${entry.config.name}` : "Chain"; return renderTaskInput(title, this.taskEditor, this.skipClarify, w, this.theme); }
607
632
  if (this.parallelMode && this.parallelState) return renderTaskInput(formatParallelTitle(this.parallelState.slots), this.taskEditor, this.skipClarify, w, this.theme);
633
+ if (this.chainAgentIds.length > 1) {
634
+ const names = this.chainAgentIds
635
+ .map((id) => this.getAgentEntry(id)?.config.name)
636
+ .filter((name): name is string => Boolean(name));
637
+ return renderTaskInput(`Chain: ${names.join(" → ")}`, this.taskEditor, this.skipClarify, w, this.theme);
638
+ }
608
639
  const name = this.getAgentEntry(this.chainAgentIds[0] ?? null)?.config.name ?? "Agent";
609
640
  return renderTaskInput(`Run: ${name}`, this.taskEditor, this.skipClarify, w, this.theme);
610
641
  }
package/agents.ts CHANGED
@@ -35,6 +35,7 @@ export interface BuiltinAgentOverrideBase {
35
35
  systemPromptMode: SystemPromptMode;
36
36
  inheritProjectContext: boolean;
37
37
  inheritSkills: boolean;
38
+ disabled?: boolean;
38
39
  systemPrompt: string;
39
40
  skills?: string[];
40
41
  tools?: string[];
@@ -48,6 +49,7 @@ export interface BuiltinAgentOverrideConfig {
48
49
  systemPromptMode?: SystemPromptMode;
49
50
  inheritProjectContext?: boolean;
50
51
  inheritSkills?: boolean;
52
+ disabled?: boolean;
51
53
  systemPrompt?: string;
52
54
  skills?: string[] | false;
53
55
  tools?: string[] | false;
@@ -80,10 +82,18 @@ export interface AgentConfig {
80
82
  defaultProgress?: boolean;
81
83
  interactive?: boolean;
82
84
  maxSubagentDepth?: number;
85
+ disabled?: boolean;
83
86
  extraFields?: Record<string, string>;
84
87
  override?: BuiltinAgentOverrideInfo;
85
88
  }
86
89
 
90
+ interface SubagentSettings {
91
+ overrides: Record<string, BuiltinAgentOverrideConfig>;
92
+ disableBuiltins?: boolean;
93
+ }
94
+
95
+ const EMPTY_SUBAGENT_SETTINGS: SubagentSettings = { overrides: {} };
96
+
87
97
  export interface ChainStepConfig {
88
98
  agent: string;
89
99
  task: string;
@@ -150,6 +160,7 @@ function cloneOverrideBase(agent: AgentConfig): BuiltinAgentOverrideBase {
150
160
  systemPromptMode: agent.systemPromptMode,
151
161
  inheritProjectContext: agent.inheritProjectContext,
152
162
  inheritSkills: agent.inheritSkills,
163
+ disabled: agent.disabled,
153
164
  systemPrompt: agent.systemPrompt,
154
165
  skills: agent.skills ? [...agent.skills] : undefined,
155
166
  tools: agent.tools ? [...agent.tools] : undefined,
@@ -167,6 +178,7 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
167
178
  ...(override.systemPromptMode !== undefined ? { systemPromptMode: override.systemPromptMode } : {}),
168
179
  ...(override.inheritProjectContext !== undefined ? { inheritProjectContext: override.inheritProjectContext } : {}),
169
180
  ...(override.inheritSkills !== undefined ? { inheritSkills: override.inheritSkills } : {}),
181
+ ...(override.disabled !== undefined ? { disabled: override.disabled } : {}),
170
182
  ...(override.systemPrompt !== undefined ? { systemPrompt: override.systemPrompt } : {}),
171
183
  ...(override.skills !== undefined ? { skills: override.skills === false ? false : [...override.skills] } : {}),
172
184
  ...(override.tools !== undefined ? { tools: override.tools === false ? false : [...override.tools] } : {}),
@@ -197,9 +209,17 @@ export function getProjectAgentSettingsPath(cwd: string): string | null {
197
209
 
198
210
  function readSettingsFileStrict(filePath: string): Record<string, unknown> {
199
211
  if (!fs.existsSync(filePath)) return {};
212
+ let raw: string;
213
+ try {
214
+ raw = fs.readFileSync(filePath, "utf-8");
215
+ } catch (error) {
216
+ const message = error instanceof Error ? error.message : String(error);
217
+ throw new Error(`Failed to read settings file '${filePath}': ${message}`, { cause: error });
218
+ }
219
+
200
220
  let parsed: unknown;
201
221
  try {
202
- parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
222
+ parsed = JSON.parse(raw);
203
223
  } catch (error) {
204
224
  const message = error instanceof Error ? error.message : String(error);
205
225
  throw new Error(`Failed to parse settings file '${filePath}': ${message}`, { cause: error });
@@ -282,6 +302,14 @@ function parseBuiltinOverrideEntry(
282
302
  }
283
303
  }
284
304
 
305
+ if ("disabled" in input) {
306
+ if (typeof input.disabled === "boolean") {
307
+ override.disabled = input.disabled;
308
+ } else {
309
+ throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'disabled'; expected a boolean.`);
310
+ }
311
+ }
312
+
285
313
  if ("systemPrompt" in input) {
286
314
  if (typeof input.systemPrompt === "string") override.systemPrompt = input.systemPrompt;
287
315
  else throw new Error(`Builtin override '${name}' in '${filePath}' has invalid 'systemPrompt'; expected a string.`);
@@ -299,20 +327,32 @@ function parseBuiltinOverrideEntry(
299
327
  return Object.keys(override).length > 0 ? override : undefined;
300
328
  }
301
329
 
302
- function readBuiltinOverrides(filePath: string | null): Record<string, BuiltinAgentOverrideConfig> {
303
- if (!filePath || !fs.existsSync(filePath)) return {};
330
+ function readSubagentSettings(filePath: string | null): SubagentSettings {
331
+ if (!filePath) return EMPTY_SUBAGENT_SETTINGS;
304
332
  const settings = readSettingsFileStrict(filePath);
305
333
  const subagents = settings.subagents;
306
- if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return {};
307
- const agentOverrides = (subagents as Record<string, unknown>).agentOverrides;
308
- if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) return {};
334
+ if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return EMPTY_SUBAGENT_SETTINGS;
335
+
336
+ const subagentsObject = subagents as Record<string, unknown>;
337
+ let disableBuiltins: boolean | undefined;
338
+ if ("disableBuiltins" in subagentsObject) {
339
+ if (typeof subagentsObject.disableBuiltins === "boolean") {
340
+ disableBuiltins = subagentsObject.disableBuiltins;
341
+ } else {
342
+ throw new Error(`Subagent settings in '${filePath}' have invalid 'disableBuiltins'; expected a boolean.`);
343
+ }
344
+ }
309
345
 
310
346
  const parsed: Record<string, BuiltinAgentOverrideConfig> = {};
347
+ const agentOverrides = subagentsObject.agentOverrides;
348
+ if (!agentOverrides || typeof agentOverrides !== "object" || Array.isArray(agentOverrides)) {
349
+ return { overrides: parsed, disableBuiltins };
350
+ }
311
351
  for (const [name, value] of Object.entries(agentOverrides)) {
312
352
  const override = parseBuiltinOverrideEntry(name, value, filePath);
313
353
  if (override) parsed[name] = override;
314
354
  }
315
- return parsed;
355
+ return { overrides: parsed, disableBuiltins };
316
356
  }
317
357
 
318
358
  function applyBuiltinOverride(
@@ -333,6 +373,7 @@ function applyBuiltinOverride(
333
373
  if (override.systemPromptMode !== undefined) next.systemPromptMode = override.systemPromptMode;
334
374
  if (override.inheritProjectContext !== undefined) next.inheritProjectContext = override.inheritProjectContext;
335
375
  if (override.inheritSkills !== undefined) next.inheritSkills = override.inheritSkills;
376
+ if (override.disabled !== undefined) next.disabled = override.disabled;
336
377
  if (override.systemPrompt !== undefined) next.systemPrompt = override.systemPrompt;
337
378
  if (override.skills !== undefined) next.skills = override.skills === false ? undefined : [...override.skills];
338
379
  if (override.tools !== undefined) {
@@ -346,29 +387,40 @@ function applyBuiltinOverride(
346
387
 
347
388
  function applyBuiltinOverrides(
348
389
  builtinAgents: AgentConfig[],
349
- userOverrides: Record<string, BuiltinAgentOverrideConfig>,
350
- projectOverrides: Record<string, BuiltinAgentOverrideConfig>,
390
+ userSettings: SubagentSettings,
391
+ projectSettings: SubagentSettings,
351
392
  userSettingsPath: string,
352
393
  projectSettingsPath: string | null,
353
394
  ): AgentConfig[] {
395
+ const projectBulkDisabled = projectSettings.disableBuiltins === true && projectSettingsPath !== null;
396
+ const userBulkDisabled = projectSettings.disableBuiltins === undefined && userSettings.disableBuiltins === true;
397
+
354
398
  return builtinAgents.map((agent) => {
355
- const projectOverride = projectOverrides[agent.name];
399
+ const projectOverride = projectSettings.overrides[agent.name];
356
400
  if (projectOverride && projectSettingsPath) {
357
401
  return applyBuiltinOverride(agent, projectOverride, { scope: "project", path: projectSettingsPath });
358
402
  }
359
403
 
360
- const userOverride = userOverrides[agent.name];
404
+ if (projectBulkDisabled && projectSettingsPath) {
405
+ return applyBuiltinOverride(agent, { disabled: true }, { scope: "project", path: projectSettingsPath });
406
+ }
407
+
408
+ const userOverride = userSettings.overrides[agent.name];
361
409
  if (userOverride) {
362
410
  return applyBuiltinOverride(agent, userOverride, { scope: "user", path: userSettingsPath });
363
411
  }
364
412
 
413
+ if (userBulkDisabled) {
414
+ return applyBuiltinOverride(agent, { disabled: true }, { scope: "user", path: userSettingsPath });
415
+ }
416
+
365
417
  return agent;
366
418
  });
367
419
  }
368
420
 
369
421
  export function buildBuiltinOverrideConfig(
370
422
  base: BuiltinAgentOverrideBase,
371
- draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools">,
423
+ draft: Pick<AgentConfig, "model" | "fallbackModels" | "thinking" | "systemPromptMode" | "inheritProjectContext" | "inheritSkills" | "disabled" | "systemPrompt" | "skills" | "tools" | "mcpDirectTools">,
372
424
  ): BuiltinAgentOverrideConfig | undefined {
373
425
  const override: BuiltinAgentOverrideConfig = {};
374
426
 
@@ -378,6 +430,7 @@ export function buildBuiltinOverrideConfig(
378
430
  if (draft.systemPromptMode !== base.systemPromptMode) override.systemPromptMode = draft.systemPromptMode;
379
431
  if (draft.inheritProjectContext !== base.inheritProjectContext) override.inheritProjectContext = draft.inheritProjectContext;
380
432
  if (draft.inheritSkills !== base.inheritSkills) override.inheritSkills = draft.inheritSkills;
433
+ if (draft.disabled !== base.disabled) override.disabled = draft.disabled ?? false;
381
434
  if (draft.systemPrompt !== base.systemPrompt) override.systemPrompt = draft.systemPrompt;
382
435
  if (!arraysEqual(draft.skills, base.skills)) override.skills = draft.skills ? [...draft.skills] : false;
383
436
 
@@ -621,7 +674,6 @@ function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; pref
621
674
  preferredDir,
622
675
  };
623
676
  }
624
-
625
677
  const BUILTIN_AGENTS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "agents");
626
678
 
627
679
  export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
@@ -630,11 +682,13 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
630
682
  const { readDirs: projectAgentDirs, preferredDir: projectAgentsDir } = resolveNearestProjectAgentDirs(cwd);
631
683
  const userSettingsPath = getUserAgentSettingsPath();
632
684
  const projectSettingsPath = getProjectAgentSettingsPath(cwd);
685
+ const userSettings = scope === "project" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(userSettingsPath);
686
+ const projectSettings = scope === "user" ? EMPTY_SUBAGENT_SETTINGS : readSubagentSettings(projectSettingsPath);
633
687
 
634
688
  const builtinAgents = applyBuiltinOverrides(
635
689
  loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
636
- scope === "project" ? {} : readBuiltinOverrides(userSettingsPath),
637
- scope === "user" ? {} : readBuiltinOverrides(projectSettingsPath),
690
+ userSettings,
691
+ projectSettings,
638
692
  userSettingsPath,
639
693
  projectSettingsPath,
640
694
  );
@@ -644,7 +698,8 @@ export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryRe
644
698
  const userAgents = [...userAgentsOld, ...userAgentsNew];
645
699
 
646
700
  const projectAgents = scope === "user" ? [] : projectAgentDirs.flatMap((dir) => loadAgentsFromDir(dir, "project"));
647
- const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents);
701
+ const agents = mergeAgentsForScope(scope, userAgents, projectAgents, builtinAgents)
702
+ .filter((agent) => agent.disabled !== true);
648
703
 
649
704
  return { agents, projectAgentsDir };
650
705
  }
@@ -664,11 +719,13 @@ export function discoverAgentsAll(cwd: string): {
664
719
  const { readDirs: projectDirs, preferredDir: projectDir } = resolveNearestProjectAgentDirs(cwd);
665
720
  const userSettingsPath = getUserAgentSettingsPath();
666
721
  const projectSettingsPath = getProjectAgentSettingsPath(cwd);
722
+ const userSettings = readSubagentSettings(userSettingsPath);
723
+ const projectSettings = readSubagentSettings(projectSettingsPath);
667
724
 
668
725
  const builtin = applyBuiltinOverrides(
669
726
  loadAgentsFromDir(BUILTIN_AGENTS_DIR, "builtin"),
670
- readBuiltinOverrides(userSettingsPath),
671
- readBuiltinOverrides(projectSettingsPath),
727
+ userSettings,
728
+ projectSettings,
672
729
  userSettingsPath,
673
730
  projectSettingsPath,
674
731
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
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",