pi-subagents 0.16.1 → 0.17.1

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,30 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.17.1] - 2026-04-20
6
+
7
+ ### Added
8
+ - Foreground subagent runs now make deeper live detail easier to discover. Running cards show an explicit `Ctrl+O` hint, lightweight live-state signals like recent activity, current-tool durations, and artifact output paths when available. Common array-heavy tool previews such as `web_search.queries` and `fetch_content.urls` are now summarized more clearly instead of collapsing into opaque fallback text.
9
+
10
+ ### Changed
11
+ - Forked delegated runs now use stronger prompt-side guidance for `pi-intercom` coordination instead of runtime policing. The default fork preamble and intercom bridge instructions now explicitly treat inherited fork history as reference-only context, tell children not to continue the parent conversation in normal assistant text, and steer upstream questions or handoffs through `intercom` when needed.
12
+ - Documented an opt-in custom agent pattern for forked chat-back workflows so users can make that coordination contract explicit without changing builtin agents.
13
+ - Slash-run status text and `/subagents-status` summary output now use the same more explicit observability language, including clearer live-detail hints and surfaced output/session paths in the async status overlay.
14
+ - Builtin agent defaults now prefer `openai-codex` models for `planner`, `scout`, `researcher`, `context-builder`, and `worker`.
15
+
16
+ ### Fixed
17
+ - Removed the short-lived foreground intercom enforcement/retry layer from delegated fork runs. Coordination behavior is now shaped by prompt and agent design only, avoiding hidden retries, heuristic output inspection, and failure paths based on guessed intent.
18
+
19
+ ## [0.17.0] - 2026-04-16
20
+
21
+ ### Added
22
+ - 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.
23
+
24
+ ### Fixed
25
+ - 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.
26
+ - `/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.
27
+ - 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.
28
+
5
29
  ## [0.16.1] - 2026-04-16
6
30
 
7
31
  ### 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
 
@@ -923,12 +925,14 @@ Example `instructionFile`:
923
925
  ```md
924
926
  Intercom orchestration channel:
925
927
 
928
+ The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the orchestrator in normal assistant text.
929
+
926
930
  Use `intercom` only to coordinate with the orchestrator session `{orchestratorTarget}`.
927
931
 
928
932
  - Need a decision or you're blocked: `intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })`
929
- - Need to report progress or completion: `intercom({ action: "send", to: "{orchestratorTarget}", message: "DONE: <summary>" })`
933
+ - Need to report progress or a completion handoff: `intercom({ action: "send", to: "{orchestratorTarget}", message: "DONE: <summary>" })`
930
934
 
931
- If intercom is unavailable in this run, continue the task normally.
935
+ If no upstream coordination is needed, continue the task normally and return a focused task result.
932
936
  ```
933
937
 
934
938
  Bridge activation also requires all of the following:
@@ -939,6 +943,33 @@ Bridge activation also requires all of the following:
939
943
 
940
944
  When an unnamed session falls back to `subagent-chat-<id>`, that alias is used only for the live intercom broker. It is not persisted as the Pi session title, so `pi --resume` can still show the transcript snippet.
941
945
 
946
+ If you want a stronger prompt contract for forked chat-back runs without changing builtins, define a custom agent for it. Keeping that as an opt-in agent works better than teaching every delegated run to behave this way.
947
+
948
+ Example agent:
949
+
950
+ ```md
951
+ ---
952
+ name: fork-chatback
953
+ description: Forked worker that asks the orchestrator questions through intercom when needed
954
+ tools: read, bash, edit, write, intercom
955
+ systemPromptMode: replace
956
+ inheritProjectContext: true
957
+ inheritSkills: false
958
+ ---
959
+
960
+ You are a delegated worker running from a fork of the orchestrator session.
961
+
962
+ Treat the inherited conversation as reference-only context. Do not continue that conversation in normal assistant text.
963
+
964
+ Your job is to do the task. If you need a decision, clarification, or unblock from the orchestrator, use `intercom` to ask the orchestrator session named in the runtime bridge instructions.
965
+
966
+ If you need to send a progress update or completion handoff upstream, use `intercom` to send it to that same orchestrator session.
967
+
968
+ If no upstream coordination is needed, just complete the work and return a focused task result.
969
+ ```
970
+
971
+ Pair that with task wording that makes the contract explicit, like "Work from the forked context below. If you need anything from me, ask through `intercom`. Otherwise complete the task and return the result."
972
+
942
973
  ### `worktreeSetupHook`
943
974
 
944
975
  `worktreeSetupHook` configures an optional setup hook for worktree-isolated parallel runs. The hook runs once per created worktree, after `git worktree add` succeeds and before the agent starts.
@@ -1003,7 +1034,7 @@ When fallback is used, metadata records both the ordered `attemptedModels` list
1003
1034
 
1004
1035
  Session files (JSONL) are stored under a per-run session directory. Directory selection follows the same precedence as session root resolution: explicit `sessionDir` > `config.defaultSessionDir` > parent-session-derived path. The session file path is shown in output.
1005
1036
 
1006
- When `context: "fork"` is used, each child run starts with `--session <branched-session-file>` produced from the parent's current leaf. This is a real session fork, not injected summary text.
1037
+ When `context: "fork"` is used, each child run starts with `--session <branched-session-file>` produced from the parent's current leaf. This is a real session fork, not injected summary text. The fork preamble explicitly tells the child to treat the inherited conversation as reference-only context rather than a live thread to continue.
1007
1038
 
1008
1039
  ## Session Sharing
1009
1040
 
@@ -1039,6 +1070,8 @@ During sync execution, the collapsed view shows real-time progress for single, c
1039
1070
  - Per-task step cards showing status icon, agent name, model, tool count, and duration
1040
1071
  - Current tool and recent output for each running task
1041
1072
 
1073
+ While a foreground run is active, the compact view also hints when richer detail is available and shows lightweight live-state signals like activity freshness and current-tool duration.
1074
+
1042
1075
  Press **Ctrl+O** to expand the full streaming view with complete output per step.
1043
1076
 
1044
1077
  > **Note:** Chain visualization (the `done scout → running planner` line) is only shown for sequential chains. Chains with parallel steps show per-step cards instead.
@@ -1093,7 +1126,7 @@ subagent_status({ id: "<id>" })
1093
1126
  subagent_status({ dir: "<tmpdir>/pi-subagents-<scope>/async-subagent-runs/<id>" })
1094
1127
  ```
1095
1128
 
1096
- For an interactive overview, run the `/subagents-status` slash command to open the overlay listing active runs and recent completed/failed runs. The overlay auto-refreshes every 2 seconds while it is open.
1129
+ For an interactive overview, run the `/subagents-status` slash command to open the overlay listing active runs and recent completed/failed runs. The overlay auto-refreshes every 2 seconds while it is open and focuses on summary/status information, including the current output/session paths when available.
1097
1130
 
1098
1131
  ## Events
1099
1132
 
@@ -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
  }
@@ -2,7 +2,7 @@
2
2
  name: context-builder
3
3
  description: Analyzes requirements and codebase, generates context and meta-prompt
4
4
  tools: read, grep, find, ls, bash, write, web_search
5
- model: claude-sonnet-4-6
5
+ model: openai-codex/gpt-5.4
6
6
  systemPromptMode: replace
7
7
  inheritProjectContext: true
8
8
  inheritSkills: false
package/agents/planner.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: planner
3
3
  description: Creates implementation plans from context and requirements
4
4
  tools: read, grep, find, ls, write
5
- model: claude-opus-4-6
5
+ model: openai-codex/gpt-5.4
6
6
  thinking: high
7
7
  systemPromptMode: replace
8
8
  inheritProjectContext: true
@@ -2,7 +2,7 @@
2
2
  name: researcher
3
3
  description: Autonomous web researcher — searches, evaluates, and synthesizes a focused research brief
4
4
  tools: read, write, web_search, fetch_content, get_search_content
5
- model: anthropic/claude-sonnet-4-6
5
+ model: openai-codex/gpt-5.4
6
6
  systemPromptMode: replace
7
7
  inheritProjectContext: true
8
8
  inheritSkills: false
package/agents/scout.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: scout
3
3
  description: Fast codebase recon that returns compressed context for handoff
4
4
  tools: read, grep, find, ls, bash, write
5
- model: anthropic/claude-haiku-4-5
5
+ model: openai-codex/gpt-5.4-mini
6
6
  systemPromptMode: replace
7
7
  inheritProjectContext: true
8
8
  inheritSkills: false
package/agents/worker.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: worker
3
3
  description: General-purpose subagent with full capabilities
4
- model: claude-sonnet-4-6
4
+ model: openai-codex/gpt-5.4
5
5
  systemPromptMode: replace
6
6
  inheritProjectContext: true
7
7
  inheritSkills: false
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/execution.ts CHANGED
@@ -63,6 +63,35 @@ function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
63
63
  }
64
64
  }
65
65
 
66
+ function snapshotProgress(progress: AgentProgress): AgentProgress {
67
+ return {
68
+ ...progress,
69
+ skills: progress.skills ? [...progress.skills] : undefined,
70
+ recentTools: progress.recentTools.map((tool) => ({ ...tool })),
71
+ recentOutput: [...progress.recentOutput],
72
+ };
73
+ }
74
+
75
+ function snapshotResult(result: SingleResult, progress: AgentProgress): SingleResult {
76
+ return {
77
+ ...result,
78
+ messages: result.messages ? [...result.messages] : undefined,
79
+ usage: { ...result.usage },
80
+ skills: result.skills ? [...result.skills] : undefined,
81
+ attemptedModels: result.attemptedModels ? [...result.attemptedModels] : undefined,
82
+ modelAttempts: result.modelAttempts
83
+ ? result.modelAttempts.map((attempt) => ({
84
+ ...attempt,
85
+ usage: attempt.usage ? { ...attempt.usage } : undefined,
86
+ }))
87
+ : undefined,
88
+ progress,
89
+ progressSummary: result.progressSummary ? { ...result.progressSummary } : undefined,
90
+ artifactPaths: result.artifactPaths ? { ...result.artifactPaths } : undefined,
91
+ truncation: result.truncation ? { ...result.truncation } : undefined,
92
+ };
93
+ }
94
+
66
95
  async function runSingleAttempt(
67
96
  runtimeCwd: string,
68
97
  agent: AgentConfig,
@@ -75,6 +104,7 @@ async function runSingleAttempt(
75
104
  resolvedSkillNames?: string[];
76
105
  skillsWarning?: string;
77
106
  jsonlPath?: string;
107
+ artifactPaths?: ArtifactPaths;
78
108
  attemptNotes: string[];
79
109
  outputSnapshot?: SingleOutputSnapshot;
80
110
  },
@@ -105,6 +135,7 @@ async function runSingleAttempt(
105
135
  messages: [],
106
136
  usage: emptyUsage(),
107
137
  model: modelArg,
138
+ artifactPaths: shared.artifactPaths,
108
139
  skills: shared.resolvedSkillNames,
109
140
  skillsWarning: shared.skillsWarning,
110
141
  };
@@ -120,6 +151,7 @@ async function runSingleAttempt(
120
151
  toolCount: 0,
121
152
  tokens: 0,
122
153
  durationMs: 0,
154
+ lastActivityAt: Date.now(),
123
155
  };
124
156
  result.progress = progress;
125
157
 
@@ -175,15 +207,22 @@ async function runSingleAttempt(
175
207
  resolve(code);
176
208
  };
177
209
 
178
- const fireUpdate = () => {
210
+ const emitUpdateSnapshot = (text: string) => {
179
211
  if (!options.onUpdate || processClosed) return;
180
- progress.durationMs = Date.now() - startTime;
212
+ const progressSnapshot = snapshotProgress(progress);
213
+ const resultSnapshot = snapshotResult(result, progressSnapshot);
181
214
  options.onUpdate({
182
- content: [{ type: "text", text: getFinalOutput(result.messages) || "(running...)" }],
183
- details: { mode: "single", results: [result], progress: [progress] },
215
+ content: [{ type: "text", text }],
216
+ details: { mode: "single", results: [resultSnapshot], progress: [progressSnapshot] },
184
217
  });
185
218
  };
186
219
 
220
+ const fireUpdate = () => {
221
+ if (!options.onUpdate || processClosed) return;
222
+ progress.durationMs = Date.now() - startTime;
223
+ emitUpdateSnapshot(getFinalOutput(result.messages) || "(running...)");
224
+ };
225
+
187
226
  const processLine = (line: string) => {
188
227
  if (!line.trim()) return;
189
228
  jsonlWriter.writeLine(line);
@@ -197,6 +236,7 @@ async function runSingleAttempt(
197
236
 
198
237
  const now = Date.now();
199
238
  progress.durationMs = now - startTime;
239
+ progress.lastActivityAt = now;
200
240
 
201
241
  if (evt.type === "tool_execution_start") {
202
242
  if (options.allowIntercomDetach && evt.toolName === "intercom") {
@@ -205,6 +245,7 @@ async function runSingleAttempt(
205
245
  progress.toolCount++;
206
246
  progress.currentTool = evt.toolName;
207
247
  progress.currentToolArgs = extractToolArgsPreview((evt.args || {}) as Record<string, unknown>);
248
+ progress.currentToolStartedAt = now;
208
249
  fireUpdate();
209
250
  }
210
251
 
@@ -218,6 +259,7 @@ async function runSingleAttempt(
218
259
  }
219
260
  progress.currentTool = undefined;
220
261
  progress.currentToolArgs = undefined;
262
+ progress.currentToolStartedAt = undefined;
221
263
  fireUpdate();
222
264
  }
223
265
 
@@ -343,6 +385,15 @@ async function runSingleAttempt(
343
385
  result.outputSaveError = resolvedOutput.saveError;
344
386
  }
345
387
  result.finalOutput = fullOutput;
388
+ if (options.onUpdate) {
389
+ const finalText = result.finalOutput || result.error || "(no output)";
390
+ const progressSnapshot = snapshotProgress(progress);
391
+ const resultSnapshot = snapshotResult(result, progressSnapshot);
392
+ options.onUpdate({
393
+ content: [{ type: "text", text: finalText }],
394
+ details: { mode: "single", results: [resultSnapshot], progress: [progressSnapshot] },
395
+ });
396
+ }
346
397
  return result;
347
398
  }
348
399
 
@@ -417,6 +468,7 @@ export async function runSync(
417
468
  resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
418
469
  skillsWarning: missingSkills.length > 0 ? `Skills not found: ${missingSkills.join(", ")}` : undefined,
419
470
  jsonlPath,
471
+ artifactPaths: artifactPathsResult,
420
472
  attemptNotes,
421
473
  outputSnapshot,
422
474
  });
@@ -9,10 +9,13 @@ const DEFAULT_INTERCOM_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "in
9
9
  const DEFAULT_SUBAGENT_CONFIG_DIR = path.join(os.homedir(), ".pi", "agent", "extensions", "subagent");
10
10
  const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
11
11
  const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
12
- const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `Use intercom only for coordination with the orchestrator session:
12
+ const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the orchestrator in normal assistant text.
13
+
14
+ Use intercom only for coordination with the orchestrator session "{orchestratorTarget}".
13
15
  - Need a decision or blocked: intercom({ action: "ask", to: "{orchestratorTarget}", message: "<question>" })
14
- - Completion/update: intercom({ action: "send", to: "{orchestratorTarget}", message: "DONE: <summary>" })
15
- If intercom is unavailable in this run, continue the task normally.`;
16
+ - Need to report progress or a completion handoff: intercom({ action: "send", to: "{orchestratorTarget}", message: "DONE: <summary>" })
17
+
18
+ If no upstream coordination is needed, continue the task normally and return a focused task result.`;
16
19
 
17
20
  export interface IntercomBridgeState {
18
21
  active: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-subagents",
3
- "version": "0.16.1",
3
+ "version": "0.17.1",
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",
package/render.ts CHANGED
@@ -6,6 +6,7 @@ import type { AgentToolResult } from "@mariozechner/pi-agent-core";
6
6
  import { getMarkdownTheme, type ExtensionContext } from "@mariozechner/pi-coding-agent";
7
7
  import { Container, Markdown, Spacer, Text, visibleWidth, type Component } from "@mariozechner/pi-tui";
8
8
  import {
9
+ type AgentProgress,
9
10
  type AsyncJobState,
10
11
  type Details,
11
12
  MAX_WIDGET_JOBS,
@@ -113,6 +114,34 @@ function getToolCallLines(
113
114
  return result.toolCalls?.map((toolCall) => expanded ? toolCall.expandedText : toolCall.text) ?? [];
114
115
  }
115
116
 
117
+ function formatActivityLabel(lastActivityAt: number | undefined, now = Date.now()): string | undefined {
118
+ if (lastActivityAt === undefined) return undefined;
119
+ const ago = Math.max(0, now - lastActivityAt);
120
+ if (ago < 1000) return "active now";
121
+ if (ago < 60000) return `active ${Math.floor(ago / 1000)}s ago`;
122
+ return `active ${Math.floor(ago / 60000)}m ago`;
123
+ }
124
+
125
+ function formatCurrentToolLine(progress: Pick<AgentProgress, "currentTool" | "currentToolArgs" | "currentToolStartedAt">, availableWidth: number, expanded: boolean): string | undefined {
126
+ if (!progress.currentTool) return undefined;
127
+ const maxToolArgsLen = Math.max(50, availableWidth - 20);
128
+ const toolArgsPreview = progress.currentToolArgs
129
+ ? (expanded || progress.currentToolArgs.length <= maxToolArgsLen
130
+ ? progress.currentToolArgs
131
+ : `${progress.currentToolArgs.slice(0, maxToolArgsLen)}...`)
132
+ : "";
133
+ const durationSuffix = progress.currentToolStartedAt !== undefined
134
+ ? ` | ${formatDuration(Math.max(0, Date.now() - progress.currentToolStartedAt))}`
135
+ : "";
136
+ return toolArgsPreview
137
+ ? `${progress.currentTool}: ${toolArgsPreview}${durationSuffix}`
138
+ : `${progress.currentTool}${durationSuffix}`;
139
+ }
140
+
141
+ function buildLiveStatusLine(progress: Pick<AgentProgress, "lastActivityAt">): string | undefined {
142
+ return formatActivityLabel(progress.lastActivityAt);
143
+ }
144
+
116
145
  /**
117
146
  * Render the async jobs widget
118
147
  */
@@ -226,18 +255,18 @@ export function renderSubagentResult(
226
255
  c.addChild(new Spacer(1));
227
256
 
228
257
  if (isRunning && r.progress) {
229
- if (r.progress.currentTool) {
230
- const maxToolArgsLen = Math.max(50, w - 20);
231
- const toolArgsPreview = r.progress.currentToolArgs
232
- ? (expanded || r.progress.currentToolArgs.length <= maxToolArgsLen
233
- ? r.progress.currentToolArgs
234
- : `${r.progress.currentToolArgs.slice(0, maxToolArgsLen)}...`)
235
- : "";
236
- const toolLine = toolArgsPreview
237
- ? `${r.progress.currentTool}: ${toolArgsPreview}`
238
- : r.progress.currentTool;
258
+ const toolLine = formatCurrentToolLine(r.progress, w, expanded);
259
+ if (toolLine) {
239
260
  c.addChild(new Text(fit(theme.fg("warning", `> ${toolLine}`)), 0, 0));
240
261
  }
262
+ const liveStatusLine = buildLiveStatusLine(r.progress);
263
+ if (liveStatusLine) {
264
+ c.addChild(new Text(fit(theme.fg("accent", liveStatusLine)), 0, 0));
265
+ }
266
+ c.addChild(new Text(fit(theme.fg("accent", "Press Ctrl+O for live detail")), 0, 0));
267
+ if (r.artifactPaths) {
268
+ c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
269
+ }
241
270
  if (r.progress.recentTools?.length) {
242
271
  for (const t of r.progress.recentTools.slice(-3)) {
243
272
  const maxArgsLen = Math.max(40, w - 24);
@@ -250,7 +279,7 @@ export function renderSubagentResult(
250
279
  for (const line of (r.progress.recentOutput ?? []).slice(-5)) {
251
280
  c.addChild(new Text(fit(theme.fg("dim", ` ${line}`)), 0, 0));
252
281
  }
253
- if (r.progress.currentTool || r.progress.recentTools?.length || r.progress.recentOutput?.length) {
282
+ if (toolLine || liveStatusLine || r.progress.recentTools?.length || r.progress.recentOutput?.length || r.artifactPaths) {
254
283
  c.addChild(new Spacer(1));
255
284
  }
256
285
  }
@@ -278,7 +307,7 @@ export function renderSubagentResult(
278
307
  c.addChild(new Text(fit(theme.fg("dim", `Session: ${shortenPath(r.sessionFile)}`)), 0, 0));
279
308
  }
280
309
 
281
- if (r.artifactPaths) {
310
+ if (!isRunning && r.artifactPaths) {
282
311
  c.addChild(new Spacer(1));
283
312
  c.addChild(new Text(fit(theme.fg("dim", `Artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
284
313
  }
@@ -391,6 +420,7 @@ export function renderSubagentResult(
391
420
  || d.progress?.find((p) => p.agent === r.agent && p.status === "running");
392
421
  const rProg = r.progress || progressFromArray || r.progressSummary;
393
422
  const rRunning = rProg?.status === "running";
423
+ const stepNumber = typeof rProg?.index === "number" ? rProg.index + 1 : i + 1;
394
424
 
395
425
  const resultOutput = getSingleResultOutput(r);
396
426
  const statusIcon = rRunning
@@ -403,8 +433,8 @@ export function renderSubagentResult(
403
433
  const stats = rProg ? ` | ${rProg.toolCount} tools, ${formatDuration(rProg.durationMs)}` : "";
404
434
  const modelDisplay = r.model ? theme.fg("dim", ` (${r.model})`) : "";
405
435
  const stepHeader = rRunning
406
- ? `${statusIcon} Step ${i + 1}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
407
- : `${statusIcon} Step ${i + 1}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
436
+ ? `${statusIcon} Step ${stepNumber}: ${theme.bold(theme.fg("warning", r.agent))}${modelDisplay}${stats}`
437
+ : `${statusIcon} Step ${stepNumber}: ${theme.bold(r.agent)}${modelDisplay}${stats}`;
408
438
  const toolCallLines = getToolCallLines(r, expanded);
409
439
  c.addChild(new Text(fit(stepHeader), 0, 0));
410
440
 
@@ -433,18 +463,18 @@ export function renderSubagentResult(
433
463
  if (rProg.skills?.length) {
434
464
  c.addChild(new Text(fit(theme.fg("accent", ` skills: ${rProg.skills.join(", ")}`)), 0, 0));
435
465
  }
436
- if (rProg.currentTool) {
437
- const maxToolArgsLen = Math.max(50, w - 20);
438
- const toolArgsPreview = rProg.currentToolArgs
439
- ? (expanded || rProg.currentToolArgs.length <= maxToolArgsLen
440
- ? rProg.currentToolArgs
441
- : `${rProg.currentToolArgs.slice(0, maxToolArgsLen)}...`)
442
- : "";
443
- const toolLine = toolArgsPreview
444
- ? `${rProg.currentTool}: ${toolArgsPreview}`
445
- : rProg.currentTool;
466
+ const toolLine = formatCurrentToolLine(rProg, w, expanded);
467
+ if (toolLine) {
446
468
  c.addChild(new Text(fit(theme.fg("warning", ` > ${toolLine}`)), 0, 0));
447
469
  }
470
+ const liveStatusLine = buildLiveStatusLine(rProg);
471
+ if (liveStatusLine) {
472
+ c.addChild(new Text(fit(theme.fg("accent", ` ${liveStatusLine}`)), 0, 0));
473
+ }
474
+ c.addChild(new Text(fit(theme.fg("accent", " Press Ctrl+O for live detail")), 0, 0));
475
+ if (r.artifactPaths) {
476
+ c.addChild(new Text(fit(theme.fg("dim", ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
477
+ }
448
478
  if (rProg.recentTools?.length) {
449
479
  for (const t of rProg.recentTools.slice(-3)) {
450
480
  const maxArgsLen = Math.max(40, w - 30);
@@ -460,6 +490,10 @@ export function renderSubagentResult(
460
490
  }
461
491
  }
462
492
 
493
+ if (!rRunning && r.artifactPaths) {
494
+ c.addChild(new Text(fit(theme.fg("dim", ` artifacts: ${shortenPath(r.artifactPaths.outputPath)}`)), 0, 0));
495
+ }
496
+
463
497
  if (expanded && !rRunning) {
464
498
  for (const line of toolCallLines) {
465
499
  c.addChild(new Text(fit(theme.fg("muted", ` ${line}`)), 0, 0));
package/slash-commands.ts CHANGED
@@ -145,7 +145,7 @@ async function requestSlashRun(
145
145
  if (!ctx.hasUI) return;
146
146
  const tool = update.currentTool ? ` ${update.currentTool}` : "";
147
147
  const count = update.toolCount ?? 0;
148
- ctx.ui.setStatus("subagent-slash", `${count} tools${tool}`);
148
+ ctx.ui.setStatus("subagent-slash", `${count} tools${tool} | Ctrl+O live detail`);
149
149
  };
150
150
 
151
151
  const onTerminalInput = ctx.hasUI
@@ -157,6 +157,12 @@ export class SubagentsStatusComponent implements Component {
157
157
  const lines = [
158
158
  row(`cwd: ${truncateToWidth(shortenPath(run.cwd ?? run.asyncDir), innerW - 5)}`, width, this.theme),
159
159
  ];
160
+ if (run.outputFile) {
161
+ lines.push(row(`output: ${truncateToWidth(shortenPath(run.outputFile), innerW - 8)}`, width, this.theme));
162
+ }
163
+ if (run.sessionFile) {
164
+ lines.push(row(`session: ${truncateToWidth(shortenPath(run.sessionFile), innerW - 9)}`, width, this.theme));
165
+ }
160
166
  for (const step of run.steps) {
161
167
  const model = step.model ? ` | ${step.model}` : "";
162
168
  const attempts = step.attemptedModels && step.attemptedModels.length > 1
@@ -226,7 +232,7 @@ export class SubagentsStatusComponent implements Component {
226
232
  lines.push(row(this.theme.fg("dim", "No runs selected."), w, this.theme));
227
233
  }
228
234
 
229
- const footer = `↑↓ select esc close ${this.active.length} active / ${this.recent.length} recent`;
235
+ const footer = `↑↓ select esc close summary view ${this.active.length} active / ${this.recent.length} recent`;
230
236
  lines.push(renderFooter(truncateToWidth(footer, innerW), w, this.theme));
231
237
  return lines;
232
238
  }
package/types.ts CHANGED
@@ -50,8 +50,10 @@ export interface AgentProgress {
50
50
  status: "pending" | "running" | "completed" | "failed" | "detached";
51
51
  task: string;
52
52
  skills?: string[];
53
+ lastActivityAt?: number;
53
54
  currentTool?: string;
54
55
  currentToolArgs?: string;
56
+ currentToolStartedAt?: number;
55
57
  recentTools: Array<{ tool: string; args: string; endMs: number }>;
56
58
  recentOutput: string[];
57
59
  toolCount: number;
@@ -384,9 +386,10 @@ export const MAX_WIDGET_JOBS = 4;
384
386
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 2;
385
387
 
386
388
  export const DEFAULT_FORK_PREAMBLE =
387
- "You are a delegated subagent with access to the parent session's context for reference. " +
388
- "Your sole job is to execute the task below. Do not continue or respond to the prior conversation " +
389
- " focus exclusively on completing this task using your tools.";
389
+ "You are a delegated subagent running from a fork of the parent session. " +
390
+ "Treat the inherited conversation as reference-only context, not a live thread to continue. " +
391
+ "Do not continue or answer prior messages as if they are waiting for a reply. " +
392
+ "Your sole job is to execute the task below and return a focused result for that task using your tools.";
390
393
 
391
394
  function normalizeTopLevelParallelValue(value: unknown): number | undefined {
392
395
  const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
package/utils.ts CHANGED
@@ -362,25 +362,54 @@ export function detectSubagentError(messages: Message[]): ErrorInfo {
362
362
  * Extract a preview of tool arguments for display
363
363
  */
364
364
  export function extractToolArgsPreview(args: Record<string, unknown>): string {
365
+ const truncatePreview = (value: string, maxLength: number): string =>
366
+ value.length > maxLength ? `${value.slice(0, maxLength - 3)}...` : value;
367
+
368
+ const stringifyPreviewValue = (value: unknown): string | undefined => {
369
+ if (typeof value === "string" && value.trim().length > 0) return value;
370
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
371
+ return undefined;
372
+ };
373
+
374
+ const previewArray = (value: unknown): string | undefined => {
375
+ if (!Array.isArray(value) || value.length === 0) return undefined;
376
+ const first = stringifyPreviewValue(value[0]);
377
+ if (!first) return undefined;
378
+ const suffix = value.length > 1 ? ` (+${value.length - 1} more)` : "";
379
+ return `${first}${suffix}`;
380
+ };
381
+
365
382
  // Handle MCP tool calls - show server/tool info
366
383
  if (args.tool && typeof args.tool === "string") {
367
384
  const server = args.server && typeof args.server === "string" ? `${args.server}/` : "";
368
385
  const toolArgs = args.args && typeof args.args === "string" ? ` ${args.args.slice(0, 40)}` : "";
369
386
  return `${server}${args.tool}${toolArgs}`;
370
387
  }
388
+
389
+ const queriesPreview = previewArray(args.queries);
390
+ if (queriesPreview) return truncatePreview(queriesPreview, 60);
391
+ if (typeof args.query === "string" && args.query.trim().length > 0) return truncatePreview(args.query, 60);
392
+ if (typeof args.workflow === "string" && args.workflow.trim().length > 0) return `workflow=${truncatePreview(args.workflow, 48)}`;
393
+
394
+ if (typeof args.url === "string" && args.url.trim().length > 0) return truncatePreview(args.url, 60);
395
+ const urlsPreview = previewArray(args.urls);
396
+ if (urlsPreview) return truncatePreview(urlsPreview, 60);
397
+ if (typeof args.prompt === "string" && args.prompt.trim().length > 0) return truncatePreview(args.prompt, 60);
371
398
 
372
399
  const previewKeys = ["command", "path", "file_path", "pattern", "query", "url", "task", "describe", "search"];
373
400
  for (const key of previewKeys) {
374
401
  if (args[key] && typeof args[key] === "string") {
375
402
  const value = args[key] as string;
376
- return value.length > 60 ? `${value.slice(0, 57)}...` : value;
403
+ return truncatePreview(value, 60);
377
404
  }
378
405
  }
379
406
 
380
407
  // Fallback: show first string value found
381
408
  for (const [key, value] of Object.entries(args)) {
409
+ const arrayPreview = previewArray(value);
410
+ if (arrayPreview) return `${key}=${truncatePreview(arrayPreview, 50)}`;
382
411
  if (typeof value === "string" && value.length > 0) {
383
- const preview = value.length > 50 ? `${value.slice(0, 47)}...` : value;
412
+ const preview = truncatePreview(value, 50);
384
413
  return `${key}=${preview}`;
385
414
  }
386
415
  }