@maplezzk/pi-interactive-subagents 3.11.1 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,7 +92,7 @@ Subagent panes are created without stealing keyboard focus (cmux, tmux). Launch
92
92
 
93
93
  ### Extensions
94
94
 
95
- **Subagents** — 4 main-session tools + 3 commands, plus 1 subagent-only tool:
95
+ **Subagents** — 4 main-session tools + 2 commands, plus 1 subagent-only tool:
96
96
 
97
97
  | Tool | Description |
98
98
  | -------------------- | ------------------------------------------------------------------------------------------- |
@@ -104,7 +104,6 @@ Subagent panes are created without stealing keyboard focus (cmux, tmux). Launch
104
104
  | Command | Description |
105
105
  | -------------------------- | ------------------------------------ |
106
106
  | `/plan` | Start a full planning workflow |
107
- | `/iterate` | Fork into a subagent for quick fixes |
108
107
  | `/subagent <agent> <task>` | Spawn a named agent directly |
109
108
 
110
109
  ### Bundled Agents
@@ -155,26 +154,24 @@ The widget tracks each Pi-backed sub-agent from a child-written runtime snapshot
155
154
 
156
155
  These labels are no longer derived from session-file growth. Session JSONL is still used for transcript, resume, lineage, and result extraction, but Pi-backed liveness now comes from a small activity snapshot written by the child extension. A fixed internal watchdog marks a run as `stalled` when valid snapshots never appear, stop being readable, or stop matching the current child; valid long-running `active` or `waiting` states do not become `stalled` just because time passes. When a run enters `stalled` or recovers from it, the parent agent receives a steer message so it can react. All other status transitions stay in the widget only.
157
156
 
158
- **Interactive subagents stay silent.** Long-running user-driven subagents (e.g. `planner`, or any `/iterate` fork) do not wake the parent session on `stalled`/`recovered` transitions — the user is working directly in the subagent's pane, and a steer message there would just burn an orchestrator turn on a no-op "still waiting" ping. The widget still updates normally, and child snapshots are still recorded/classified regardless of the `interactive` setting. By default, agents with `auto-exit: true` are treated as autonomous and get stall pings; agents without it are treated as interactive and stay quiet. Override per-agent with `interactive: true|false` in frontmatter, or per-spawn with `interactive: true|false` on the tool call.
157
+ **Interactive subagents stay silent.** Long-running user-driven subagents (e.g. `planner`) do not wake the parent session on `stalled`/`recovered` transitions — the user is working directly in the subagent's pane, and a steer message there would just burn an orchestrator turn on a no-op "still waiting" ping. The widget still updates normally, and child snapshots are still recorded/classified regardless of the `interactive` setting. By default, agents with `auto-exit: true` are treated as autonomous and get stall pings; agents without it are treated as interactive and stay quiet. Override per-agent with `interactive: true|false` in frontmatter, or per-spawn with `interactive: true|false` on the tool call.
159
158
 
160
159
  #### Configuration
161
160
 
162
- Subagent settings, including status display and the persisted Herdr mode, are controlled by `config.json` in the extension directory. Copy `config.json.example` to get started:
163
-
164
- ```bash
165
- cp config.json.example config.json
166
- ```
161
+ The persisted Herdr mode, child-spawning policy, and explicit child-extension list are read from the user extension config at `~/.pi/agent/extensions/pi-interactive-subagents/config.json` (respecting `PI_CODING_AGENT_DIR`). The status panel's `status.enabled` is read from the installed package's `config.json`, falling back to `config.json.example`. The package's `config.json.example` documents the available fields; add `allowSubagentSpawning` and `subagentExtensions` to the user config without overwriting its existing mux settings:
167
162
 
168
163
  ```json
169
164
  {
170
165
  "herdrMode": "split",
166
+ "allowSubagentSpawning": false,
167
+ "subagentExtensions": [],
171
168
  "status": {
172
169
  "enabled": true
173
170
  }
174
171
  }
175
172
  ```
176
173
 
177
- `herdrMode` accepts `split` (default, backward-compatible pane layout) or `tab` (one background tab per subagent). The `/config:subagent herdr split|tab` command updates this field.
174
+ `herdrMode` accepts `split` (default, backward-compatible pane layout) or `tab` (one background tab per subagent). The `/config:subagent herdr split|tab` command updates this field. `allowSubagentSpawning` is a global switch for whether child subagents may create or manage other subagents; it defaults to `false`. Set it to `true` to enable the lifecycle tools in child sessions when the corresponding extension is selected. `subagentExtensions` is an optional list of extension paths to load explicitly in child sessions; automatic project/global extension discovery is disabled. Paths may be absolute, start with `~/`, or be relative to `PI_CODING_AGENT_DIR`. `subagent-done.ts` is always loaded. Explicit `deny-tools` restrictions still apply.
178
175
 
179
176
  `config.json` is gitignored so local overrides don't get committed.
180
177
 
@@ -186,10 +183,7 @@ cp config.json.example config.json
186
183
  // Named agent with defaults from agent definition
187
184
  subagent({ name: "Scout", agent: "scout", task: "Analyze the codebase..." });
188
185
 
189
- // Force a full-context fork for this spawn
190
- subagent({ name: "Iterate", fork: true, task: "Fix the bug where..." });
191
-
192
- // Agent defaults can choose a different session-mode via frontmatter
186
+ // Child sessions start fresh; use session-mode: lineage-only when lineage metadata is useful
193
187
  subagent({ name: "Planner", agent: "planner", task: "Work through the design with me" });
194
188
 
195
189
  // Custom working directory
@@ -203,7 +197,6 @@ subagent({ name: "Designer", agent: "game-designer", cwd: "agents/game-designer"
203
197
  | `name` | string | required | Display name (shown in widget and pane title) |
204
198
  | `task` | string | required | Task prompt for the sub-agent |
205
199
  | `agent` | string | — | Load defaults from agent definition |
206
- | `fork` | boolean | `false` | Force the full-context fork mode for this spawn, overriding any agent `session-mode` frontmatter |
207
200
  | `interactive` | boolean | derived | Mark this spawn as interactive (don't wake the parent on stall/recovery). Defaults to the agent's `interactive` frontmatter, otherwise the inverse of `auto-exit`. |
208
201
  | `model` | string | — | Override agent's default model |
209
202
  | `systemPrompt` | string | — | Append to system prompt |
@@ -290,18 +283,6 @@ Tab/window titles update to show current phase:
290
283
 
291
284
  ---
292
285
 
293
- ## The `/iterate` Workflow
294
-
295
- For quick, focused work without polluting the main session's context.
296
-
297
- ```
298
- /iterate Fix the off-by-one error in the pagination logic
299
- ```
300
-
301
- This always forks the current session into a subagent with full conversation context. It does not inherit an agent default `session-mode`. Make the fix, verify it, and exit to return. The main session gets a summary of what was done.
302
-
303
- ---
304
-
305
286
  ## Custom Agents
306
287
 
307
288
  Place a `.md` file in `.pi/agents/` (project) or `~/.pi/agent/agents/` (global):
@@ -314,7 +295,6 @@ model: anthropic/claude-sonnet-4-6
314
295
  thinking: minimal
315
296
  tools: read, bash, edit, write
316
297
  session-mode: lineage-only
317
- spawning: false
318
298
  ---
319
299
 
320
300
  # My Agent
@@ -332,8 +312,8 @@ You are a specialized agent that does X...
332
312
  | `thinking` | string | Thinking level: `minimal`, `medium`, `high` |
333
313
  | `tools` | string | Comma-separated **native pi tools only**: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls` |
334
314
  | `skills` | string | Comma-separated skill names to auto-load |
335
- | `session-mode` | string | Default child-session mode: `standalone`, `lineage-only`, or `fork` |
336
- | `spawning` | boolean | Set `false` to deny all subagent-spawning tools |
315
+ | `session-mode` | string | Default child-session mode: `standalone` or `lineage-only` |
316
+ | `spawning` | boolean | Legacy field retained for compatibility. Use the global `allowSubagentSpawning` setting to control child-session subagent lifecycle tools. |
337
317
  | `deny-tools` | string | Comma-separated extension tool names to deny |
338
318
  | `auto-exit` | boolean | Auto-shutdown when the agent finishes its turn — no `subagent_done` call needed. If the user sends any input, auto-exit is permanently disabled and the user takes over the session. Recommended for autonomous agents (scout, worker); not for interactive ones (planner). Also determines the default value of `interactive` (see below). |
339
319
  | `interactive` | boolean | derived | Override whether stall/recovery transitions wake the parent session. Defaults to the inverse of `auto-exit`: autonomous agents (`auto-exit: true`) are non-interactive and get stall pings; agents without `auto-exit` are interactive and stay quiet. Explicit values take precedence. |
@@ -346,15 +326,12 @@ Discovery still resolves precedence before visibility filtering. If a project-lo
346
326
 
347
327
  ### `session-mode`
348
328
 
349
- Choose how a subagent session starts:
329
+ Choose how a child session starts:
350
330
 
351
331
  - `standalone` — default fresh session with no lineage link to the caller
352
332
  - `lineage-only` — fresh blank child session with `parentSession` linkage, but no copied turns from the caller
353
- - `fork` — linked child session seeded with the caller's prior conversation context
354
333
 
355
- `lineage-only` is useful when you want session discovery and fork lineage UX to show the relationship later, but you do **not** want the child to inherit the parent's turns.
356
-
357
- `fork: true` on the tool call always forces the `fork` mode for that specific spawn. `/iterate` uses this explicit override on purpose.
334
+ All child tasks are delivered through an artifact-backed initial message. There is no full-context fork mode.
358
335
 
359
336
  ```yaml
360
337
  ---
@@ -376,7 +353,7 @@ When set to `true`, the agent session shuts down automatically as soon as the ag
376
353
  **When to use:**
377
354
 
378
355
  - ✅ Autonomous agents (scout, worker, reviewer) that run to completion
379
- - ❌ Interactive agents (planner, iterate) where the user drives the session
356
+ - ❌ Interactive agents (planner) where the user drives the session
380
357
 
381
358
  ```yaml
382
359
  ---
@@ -389,7 +366,7 @@ auto-exit: true
389
366
 
390
367
  Controls whether status transitions (`stalled`, `recovered`) wake the parent session with a steer message.
391
368
 
392
- **Default:** the inverse of `auto-exit`. Autonomous agents (`auto-exit: true`) are non-interactive and ping the parent on stall/recovery; agents without `auto-exit` are interactive and stay quiet. Bare spawns with no agent defs (e.g. `/iterate` with `fork: true`) are treated as interactive.
369
+ **Default:** the inverse of `auto-exit`. Autonomous agents (`auto-exit: true`) are non-interactive and ping the parent on stall/recovery; agents without `auto-exit` are interactive and stay quiet. Bare spawns with no agent defs are treated as interactive.
393
370
 
394
371
  **Why it exists:** Interactive agents can run for minutes or hours while the user thinks, types, and reads in the subagent's pane. Child snapshots still update the widget, but stalled/recovered supervision messages rarely need to wake the parent for user-driven sessions. Skipping the steer keeps the parent quiet until the child actually finishes.
395
372
 
@@ -415,18 +392,11 @@ subagent({ name: "Scout", agent: "scout", interactive: true, task: "..." });
415
392
 
416
393
  ## Tool Access Control
417
394
 
418
- By default, every sub-agent can spawn further sub-agents. Control this with frontmatter:
419
-
420
- ### `spawning: false`
395
+ Child subagent sessions cannot create or manage other subagents unless the global `allowSubagentSpawning` setting is `true`. When it is `false` (the default), the lifecycle tools `subagent`, `subagent_interrupt`, `subagents_list`, and `subagent_resume` are not registered in child sessions. The child-only `subagent_done` tool remains available; `caller_ping` remains available for requesting help from the parent.
421
396
 
422
- Denies all subagent lifecycle tools (`subagent`, `subagent_interrupt`, `subagents_list`, `subagent_resume`):
397
+ ### `spawning` (legacy)
423
398
 
424
- ```yaml
425
- ---
426
- name: worker
427
- spawning: false
428
- ---
429
- ```
399
+ The global `allowSubagentSpawning` setting is authoritative for child sessions. Existing `spawning: false` fields remain accepted for compatibility but do not override the global setting.
430
400
 
431
401
  ### `deny-tools`
432
402
 
@@ -439,15 +409,9 @@ deny-tools: subagent
439
409
  ---
440
410
  ```
441
411
 
442
- ### Recommended Configuration
412
+ ### Global setting
443
413
 
444
- | Agent | `spawning` | Rationale |
445
- | ---------- | ----------- | -------------------------------------------- |
446
- | planner | _(default)_ | Legitimately spawns scouts for investigation |
447
- | worker | `false` | Should implement tasks, not delegate |
448
- | researcher | `false` | Should research, not spawn |
449
- | reviewer | `false` | Should review, not spawn |
450
- | scout | `false` | Should gather context, not spawn |
414
+ The global switch applies uniformly to planner, worker, reviewer, scout, and custom child sessions. Leave `allowSubagentSpawning` as `false` unless a child agent is explicitly expected to create or manage another subagent.
451
415
 
452
416
  ---
453
417
 
@@ -478,7 +442,6 @@ Set a default `cwd` in agent frontmatter:
478
442
  ---
479
443
  name: game-designer
480
444
  cwd: ./agents/game-designer
481
- spawning: false
482
445
  ---
483
446
  ```
484
447
 
@@ -542,3 +505,14 @@ The sub-agent status supervision and turn-only interruption features were inspir
542
505
  ## License
543
506
 
544
507
  MIT
508
+
509
+
510
+ ## Naming composition
511
+
512
+ Subagents do not depend on `pi-naming`. Terminal creation supplies an initial surface label where supported; this extension does not schedule delayed title overwrites or rename the parent's terminal when `/plan` runs. Agent identity and activity remain visible in the subagent widget.
513
+
514
+ Each launch (Pi or Claude) and Pi resume writes fresh `PI_TERMINAL_RENAME_CONTEXT` ownership data through `pi-terminal-mux`, replacing inherited values. This allows a cooperating naming extension to update only the child's explicitly owned terminal target, never the shared workspace. Shared or unverified window/tab targets remain unchanged.
515
+
516
+ To use automatic titles or `/rename` in Pi children, explicitly add the installed `pi-naming` entrypoint to `subagentExtensions`. Merely installing both packages in the parent does not enable extensions in isolated child sessions. Naming uses the same terminal-mux protocol and does not import this package.
517
+
518
+ Publication requires a terminal-mux dependency version providing the ownership API.
package/README.zh-CN.md CHANGED
@@ -39,10 +39,9 @@ zellij --session pi # 然后运行 pi
39
39
 
40
40
  ## 主要能力
41
41
 
42
- - **4 个主会话工具 + 3 个命令**:`subagent`、`subagent_interrupt`、`subagents_list`、`subagent_resume`;命令 `/plan`、`/iterate`、`/subagent`
42
+ - **4 个主会话工具 + 2 个命令**:`subagent`、`subagent_interrupt`、`subagents_list`、`subagent_resume`;命令 `/plan`、`/subagent`
43
43
  - **内置 agent**:planner、scout、worker、reviewer、visual-tester
44
44
  - **`/plan` 工作流**:调研 → 规划 → 确认 → 执行 → 审查 的完整流水线
45
- - **`/iterate` 工作流**:fork 当前会话到子 agent 做快速修改,不污染主上下文
46
45
  - **caller_ping**:子 agent 向父 agent 求助的机制
47
46
  - **自定义 agent**:在 `.pi/agents/` 或 `~/.pi/agent/agents/` 放置 `.md` 定义文件
48
47
 
@@ -50,22 +49,20 @@ zellij --session pi # 然后运行 pi
50
49
 
51
50
  ## 配置
52
51
 
53
- 状态显示与持久化 Herdr 模式由扩展目录下的 `config.json` 控制。复制 `config.json.example` 开始:
54
-
55
- ```bash
56
- cp config.json.example config.json
57
- ```
52
+ 持久化的 Herdr 模式、子 agent 创建策略和子 agent 扩展列表读取自用户扩展配置 `~/.pi/agent/extensions/pi-interactive-subagents/config.json`(遵循 `PI_CODING_AGENT_DIR`)。状态面板的 `status.enabled` 读取已安装包目录的 `config.json`,不存在时回退到 `config.json.example`。`config.json.example` 仅用于说明字段;向用户配置添加 `allowSubagentSpawning` 和 `subagentExtensions` 时不要覆盖已有的 mux 设置:
58
53
 
59
54
  ```json
60
55
  {
61
56
  "herdrMode": "split",
57
+ "allowSubagentSpawning": false,
58
+ "subagentExtensions": [],
62
59
  "status": {
63
60
  "enabled": true
64
61
  }
65
62
  }
66
63
  ```
67
64
 
68
- `herdrMode` 支持 `split`(默认,兼容原有 pane 布局)和 `tab`(每个 subagent 独立后台 Tab)。`/config:subagent herdr split|tab` 会更新该字段。
65
+ `herdrMode` 支持 `split`(默认,兼容原有 pane 布局)和 `tab`(每个 subagent 独立后台 Tab)。`allowSubagentSpawning` 是全局开关,控制子 agent 是否可以创建或管理其他 subagent,默认值为 `false`;设置为 `true` 后,子 agent 才会获得这些生命周期工具。`subagentExtensions` 是可选扩展路径列表;子 agent 不再自动发现项目级和全局扩展,只加载列表中的扩展,`subagent-done.ts` 始终加载。路径可以是绝对路径、`~/` 路径,或相对于 `PI_CODING_AGENT_DIR` 的路径。`/config:subagent herdr split|tab` 会更新 Herdr 字段。
69
66
 
70
67
  ## 致谢
71
68
 
@@ -75,3 +72,14 @@ cp config.json.example config.json
75
72
  ## 许可证
76
73
 
77
74
  MIT
75
+
76
+
77
+ ## 与命名插件组合
78
+
79
+ 子代理插件不依赖 `pi-naming`。终端创建时在支持的范围内设置初始 surface 名称;不安排延迟覆盖标题,也不因 `/plan` 修改父终端名称。子代理身份与活动状态仍在 widget 中显示。
80
+
81
+ 每次启动(Pi 或 Claude)以及 Pi 恢复时,通过 `pi-terminal-mux` 写入新的 `PI_TERMINAL_RENAME_CONTEXT` 归属信息,覆盖继承值。配合命名插件时,只允许更新子代理明确拥有的终端目标,不修改共享 workspace。共享或无法确认独占的 window/tab 保持不变。
82
+
83
+ 如需在 Pi 子代理中使用自动标题或 `/rename`,将已安装的 `pi-naming` 入口显式加入 `subagentExtensions`。仅在父会话安装两个包不会开启隔离子会话的扩展。naming 通过相同的 mux 协议协作,不导入本包。
84
+
85
+ 发布时 terminal-mux 依赖版本必须包含归属 API。
package/SKILL.md CHANGED
@@ -7,7 +7,7 @@ description: "配置与排查 interactive subagents 的终端复用器、Herdr s
7
7
 
8
8
  ## 诊断
9
9
 
10
- 读取实际 Pi agent 目录下的 `extensions/pi-interactive-subagents/config.json`:`mux` 保存后端偏好,`herdrMode` 保存 `split|tab`。状态面板的 `status.enabled` 由安装包根目录的 `config.json` 读取;不存在时读取同目录 `config.json.example`。状态行数当前固定为 4,不是配置项。
10
+ 读取实际 Pi agent 目录下的 `extensions/pi-interactive-subagents/config.json`:`mux` 保存后端偏好,`herdrMode` 保存 `split|tab`,`allowSubagentSpawning` 控制子 agent 是否可以创建/管理其他 subagent,`subagentExtensions` 指定子 agent 显式加载的扩展路径列表,默认不自动发现其他扩展。默认 `allowSubagentSpawning` 为 `false`,`subagent-done.ts` 始终加载。状态面板的 `status.enabled` 由安装包根目录的 `config.json` 读取;不存在时读取同目录 `config.json.example`。状态行数当前固定为 4,不是配置项。
11
11
 
12
12
  环境变量优先级:`PI_TERMINAL_MUX` > `PI_SUBAGENT_MUX` > 持久化 `mux` > `auto`;`PI_SUBAGENT_HERDR_MODE` > 持久化 `herdrMode` > `split`。
13
13
 
@@ -1,5 +1,7 @@
1
1
  {
2
2
  "herdrMode": "split",
3
+ "allowSubagentSpawning": false,
4
+ "subagentExtensions": [],
3
5
  "status": {
4
6
  "enabled": true
5
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maplezzk/pi-interactive-subagents",
3
- "version": "3.11.1",
3
+ "version": "3.13.0",
4
4
  "description": "Interactive async subagents for pi — spawn, orchestrate, and manage sub-agent sessions in multiplexer panes. Fork of HazAT/pi-interactive-subagents.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -63,7 +63,7 @@
63
63
  "dependencies": {
64
64
  "ajv": "^8.20.0",
65
65
  "pi-extensions-i18n": "^0.4.0",
66
- "pi-terminal-mux": "^0.4.1"
66
+ "pi-terminal-mux": "^0.5.0"
67
67
  },
68
68
  "peerDependencies": {
69
69
  "@earendil-works/pi-coding-agent": ">=0.80.0 <0.81.0",
@@ -2,7 +2,7 @@ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-a
2
2
  import { keyHint } from "@earendil-works/pi-coding-agent";
3
3
  import { Type, type Static } from "@sinclair/typebox";
4
4
  import { Box, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
5
- import { dirname, join } from "node:path";
5
+ import { dirname, isAbsolute, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { createTranslator, loadCatalog } from "pi-extensions-i18n";
8
8
  import {
@@ -25,9 +25,8 @@ import {
25
25
  isMuxAvailable,
26
26
  sendEscape,
27
27
  shellEscape,
28
- renameCurrentTab,
29
- renameWorkspace,
30
- renameAgent,
28
+ createSurfaceRenameContext,
29
+ TERMINAL_RENAME_CONTEXT_ENV,
31
30
  readScreen,
32
31
  getLastSplitSource,
33
32
  clearLastSplitSource,
@@ -48,6 +47,8 @@ import {
48
47
  HERDR_SURFACE_MODES,
49
48
  loadHerdrModeConfig,
50
49
  loadMuxConfig,
50
+ loadSubagentExtensionsConfig,
51
+ loadSubagentSpawningConfig,
51
52
  saveHerdrMode,
52
53
  saveMuxPreference,
53
54
  SUBAGENT_MUX_BACKENDS,
@@ -56,6 +57,8 @@ import {
56
57
  } from "./mux-config.ts";
57
58
 
58
59
  import {
60
+ SUBAGENT_SESSION_MODE_LINEAGE_ONLY,
61
+ SUBAGENT_SESSION_MODE_STANDALONE,
59
62
  findLastAssistantMessage,
60
63
  getNewEntries,
61
64
  seedSubagentSessionFile,
@@ -168,12 +171,6 @@ const SubagentParams = Type.Object({
168
171
  "Working directory for the sub-agent. The agent starts in this folder and picks up its local .pi/ config, CLAUDE.md, skills, and extensions. Use for role-specific subfolders.",
169
172
  }),
170
173
  ),
171
- fork: Type.Optional(
172
- Type.Boolean({
173
- description:
174
- "Force the full-context fork mode for this spawn. The sub-agent inherits the current session conversation, overriding any agent frontmatter session-mode.",
175
- }),
176
- ),
177
174
  interactive: Type.Optional(
178
175
  Type.Boolean({
179
176
  description:
@@ -201,7 +198,10 @@ const SubagentParams = Type.Object({
201
198
  ),
202
199
  });
203
200
 
204
- type SubagentSessionMode = "standalone" | "lineage-only" | "fork";
201
+ type SubagentSessionMode =
202
+ | typeof SUBAGENT_SESSION_MODE_STANDALONE
203
+ | typeof SUBAGENT_SESSION_MODE_LINEAGE_ONLY;
204
+ const DEFAULT_SUBAGENT_SESSION_MODE: SubagentSessionMode = SUBAGENT_SESSION_MODE_STANDALONE;
205
205
 
206
206
  interface AgentDefaults {
207
207
  model?: string;
@@ -232,7 +232,7 @@ interface ListedAgentDefinition extends AgentDefinition {
232
232
  source: AgentSource;
233
233
  }
234
234
 
235
- /** Tools that are gated by `spawning: false` */
235
+ /** Tools that are unavailable inside child subagent sessions. */
236
236
  const SPAWNING_TOOLS = new Set([
237
237
  "subagent",
238
238
  "subagent_interrupt",
@@ -241,21 +241,19 @@ const SPAWNING_TOOLS = new Set([
241
241
  ]);
242
242
 
243
243
  /**
244
- * Resolve the effective set of denied tool names from agent defaults.
245
- * `spawning: false` expands to all SPAWNING_TOOLS.
246
- * `deny-tools` adds individual tool names on top.
244
+ * Resolve the effective set of denied tool names for a child subagent.
245
+ * The global switch controls whether every child session may create or manage
246
+ * other subagents; the legacy `spawning` field does not override it.
247
+ * `deny-tools` can add individual restrictions on top.
247
248
  */
248
- function resolveDenyTools(agentDefs: AgentDefaults | null): Set<string> {
249
- const denied = new Set<string>();
250
- if (!agentDefs) return denied;
251
-
252
- // spawning: false deny all spawning tools
253
- if (agentDefs.spawning === false) {
254
- for (const t of SPAWNING_TOOLS) denied.add(t);
255
- }
249
+ function resolveDenyTools(
250
+ agentDefs: AgentDefaults | null,
251
+ allowSubagentSpawning = false,
252
+ ): Set<string> {
253
+ const denied = allowSubagentSpawning ? new Set<string>() : new Set(SPAWNING_TOOLS);
256
254
 
257
255
  // deny-tools: explicit list
258
- if (agentDefs.denyTools) {
256
+ if (agentDefs?.denyTools) {
259
257
  for (const t of agentDefs.denyTools
260
258
  .split(",")
261
259
  .map((s) => s.trim())
@@ -276,6 +274,30 @@ function getBundledAgentsDir(): string {
276
274
  return join(SUBAGENTS_DIR, "../../agents");
277
275
  }
278
276
 
277
+ const HOME_PREFIX = "~/";
278
+
279
+ /** Resolve an extension path from the user config against the Pi agent directory. */
280
+ function resolveSubagentExtensionPath(extensionPath: string): string {
281
+ if (extensionPath === "~") return homedir();
282
+ if (extensionPath.startsWith(HOME_PREFIX)) return join(homedir(), extensionPath.slice(HOME_PREFIX.length));
283
+ return isAbsolute(extensionPath) ? extensionPath : join(getAgentConfigDir(), extensionPath);
284
+ }
285
+
286
+ /** Load and resolve explicitly configured child-session extension paths. */
287
+ function getConfiguredSubagentExtensions(): string[] {
288
+ return loadSubagentExtensionsConfig().extensions.map(resolveSubagentExtensionPath);
289
+ }
290
+
291
+ /** Build the no-discovery flag and explicit extensions for a child Pi command. */
292
+ function buildChildExtensionArgs(extensionPaths = getConfiguredSubagentExtensions()): string[] {
293
+ return [
294
+ NO_EXTENSIONS_FLAG,
295
+ EXTENSION_FLAG,
296
+ shellEscape(join(SUBAGENTS_DIR, SUBAGENT_DONE_EXTENSION_FILE)),
297
+ ...extensionPaths.flatMap((extensionPath) => [EXTENSION_FLAG, shellEscape(extensionPath)]),
298
+ ];
299
+ }
300
+
279
301
  function getFrontmatterValue(frontmatter: string, key: string): string | undefined {
280
302
  const match = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
281
303
  return match ? match[1].trim() : undefined;
@@ -285,8 +307,9 @@ function parseOptionalBoolean(value: string | undefined): boolean | undefined {
285
307
  return value != null ? value === "true" : undefined;
286
308
  }
287
309
 
310
+ /** Parse a supported session mode; invalid values return undefined. */
288
311
  function parseSessionMode(value: string | undefined): SubagentSessionMode | undefined {
289
- if (value === "standalone" || value === "lineage-only" || value === "fork") {
312
+ if (value === SUBAGENT_SESSION_MODE_STANDALONE || value === SUBAGENT_SESSION_MODE_LINEAGE_ONLY) {
290
313
  return value;
291
314
  }
292
315
  return undefined;
@@ -376,30 +399,20 @@ function getDefaultSessionDirFor(cwd: string, _agentDir: string): string {
376
399
  return sessionDir;
377
400
  }
378
401
 
379
- function resolveEffectiveSessionMode(
380
- params: Static<typeof SubagentParams>,
381
- agentDefs: AgentDefaults | null,
382
- ): SubagentSessionMode {
383
- if (params.fork) return "fork";
384
- return agentDefs?.sessionMode ?? "standalone";
402
+ /** Resolve the agent-configured session mode, defaulting to standalone. */
403
+ function resolveEffectiveSessionMode(agentDefs: AgentDefaults | null): SubagentSessionMode {
404
+ return agentDefs?.sessionMode ?? DEFAULT_SUBAGENT_SESSION_MODE;
385
405
  }
386
406
 
387
- function resolveLaunchBehavior(
388
- params: Static<typeof SubagentParams>,
389
- agentDefs: AgentDefaults | null,
390
- ): {
407
+ /** Resolve launch behavior for standalone and lineage-only child sessions. */
408
+ function resolveLaunchBehavior(agentDefs: AgentDefaults | null): {
391
409
  sessionMode: SubagentSessionMode;
392
- seededSessionMode: "lineage-only" | "fork" | null;
393
- inheritsConversationContext: boolean;
394
- taskDelivery: "direct" | "artifact";
410
+ seededSessionMode: typeof SUBAGENT_SESSION_MODE_LINEAGE_ONLY | null;
395
411
  } {
396
- const sessionMode = resolveEffectiveSessionMode(params, agentDefs);
397
- const inheritsConversationContext = sessionMode === "fork";
412
+ const sessionMode = resolveEffectiveSessionMode(agentDefs);
398
413
  return {
399
414
  sessionMode,
400
- seededSessionMode: sessionMode === "standalone" ? null : sessionMode,
401
- inheritsConversationContext,
402
- taskDelivery: inheritsConversationContext ? "direct" : "artifact",
415
+ seededSessionMode: sessionMode === DEFAULT_SUBAGENT_SESSION_MODE ? null : SUBAGENT_SESSION_MODE_LINEAGE_ONLY,
403
416
  };
404
417
  }
405
418
 
@@ -412,12 +425,11 @@ function resolveLaunchBehavior(
412
425
  * 3. Default: the inverse of `auto-exit`. Agents that auto-exit are
413
426
  * autonomous (scout, worker, reviewer) and the parent session should be
414
427
  * woken on stall/recovery transitions. Agents that don't auto-exit are
415
- * driven by the user in their own pane (planner, iterate/fork) and
416
- * stall pings are noise.
428
+ * driven by the user in their own pane (planner) and stall pings are
429
+ * noise.
417
430
  *
418
- * When no agent defs exist at all (bare `subagent({ name, task })` call,
419
- * typical for `/iterate` with `fork: true`), `autoExit` is undefined and the
420
- * subagent is treated as interactive — matching the intent of iterate.
431
+ * When no agent defs exist at all (bare `subagent({ name, task })` call),
432
+ * `autoExit` is undefined and the subagent is treated as interactive.
421
433
  */
422
434
  function resolveEffectiveInteractive(
423
435
  params: Static<typeof SubagentParams>,
@@ -706,6 +718,10 @@ function updateWidget() {
706
718
  * as standalone prompts in the child session.
707
719
  */
708
720
  const SUBAGENT_CONTROL_TOOLS = ["caller_ping", "subagent_done"] as const;
721
+ const FILE_TIMESTAMP_LENGTH = 19;
722
+ const NO_EXTENSIONS_FLAG = "--no-extensions";
723
+ const EXTENSION_FLAG = "--extension";
724
+ const SUBAGENT_DONE_EXTENSION_FILE = "subagent-done.ts";
709
725
 
710
726
  /**
711
727
  * Build the child --tools allowlist.
@@ -731,21 +747,19 @@ function buildSubagentToolAllowlist(effectiveTools?: string): string | null {
731
747
  return [...allow].join(",");
732
748
  }
733
749
 
734
- function buildPiPromptArgs(params: {
735
- effectiveSkills?: string;
736
- taskDelivery: "direct" | "artifact";
737
- taskArg: string;
738
- }): string[] {
750
+ /**
751
+ * Build positional prompt args for a child launch.
752
+ * Skills must be separate prompts so Pi expands each `/skill:` directive.
753
+ */
754
+ function buildPiPromptArgs(params: { effectiveSkills?: string; taskArg: string }): string[] {
739
755
  const skillPrompts = (params.effectiveSkills ?? "")
740
756
  .split(",")
741
757
  .map((s) => s.trim())
742
758
  .filter(Boolean)
743
759
  .map((skill) => `/skill:${skill}`);
744
760
 
745
- const needsSeparator = params.taskDelivery === "artifact" && skillPrompts.length > 0;
746
-
747
761
  return [
748
- ...(needsSeparator ? [""] : []),
762
+ ...(skillPrompts.length > 0 ? [""] : []),
749
763
  ...skillPrompts,
750
764
  params.taskArg,
751
765
  ];
@@ -882,10 +896,25 @@ function handleSubagentInterrupt(
882
896
  running.statusState = forceStatusAfterInterrupt(running.statusState, now);
883
897
  updateWidget();
884
898
 
885
- // Escape only cancels the child's current turn. Do not write the `.exit`
886
- // sidecar here: that file is the terminal completion signal consumed by
887
- // pollForExit, and writing it would close the pane and remove this running
888
- // entry instead of leaving the child alive for another turn.
899
+ // Interrupting from the parent is terminal: Escape stops the child's active
900
+ // turn, while the `.exit` sidecar tells the watcher to close the surface and
901
+ // remove the child from the running set. Without the sidecar, the child Pi
902
+ // returns to its prompt and the watcher waits forever.
903
+ if (running.sessionFile) {
904
+ const exitFile = `${running.sessionFile}.exit`;
905
+ try {
906
+ writeFileSync(exitFile, JSON.stringify({ type: "done" }));
907
+ } catch (writeErr: unknown) {
908
+ const errorMessage = writeErr instanceof Error ? writeErr.message : String(writeErr);
909
+ const error =
910
+ `Failed to signal subagent "${running.name}" termination via ${exitFile}: ` +
911
+ errorMessage;
912
+ return {
913
+ content: [{ type: "text" as const, text: error }],
914
+ details: { error, id: running.id, name: running.name },
915
+ };
916
+ }
917
+ }
889
918
 
890
919
  return {
891
920
  content: [{ type: "text" as const, text: `Interrupt requested for subagent "${running.name}".` }],
@@ -1104,7 +1133,14 @@ function registerMuxConfigCommand(pi: ExtensionAPI): void {
1104
1133
  }
1105
1134
  }
1106
1135
 
1136
+ /** 每次启动或恢复都用新 surface 重建归属,不能继承父进程的改名范围。 */
1137
+ function buildTerminalRenameEnvironment(surface: string, backend = getMuxBackend()): string {
1138
+ const context = createSurfaceRenameContext(surface, backend);
1139
+ return `${TERMINAL_RENAME_CONTEXT_ENV}=${shellEscape(JSON.stringify(context))}`;
1140
+ }
1141
+
1107
1142
  export const __test__ = {
1143
+ buildTerminalRenameEnvironment,
1108
1144
  borderLine,
1109
1145
  parseMuxConfigRequest,
1110
1146
  getShellReadyDelayMs,
@@ -1115,6 +1151,7 @@ export const __test__ = {
1115
1151
  resolveLaunchBehavior,
1116
1152
  resolveEffectiveInteractive,
1117
1153
  buildSubagentToolAllowlist,
1154
+ buildChildExtensionArgs,
1118
1155
  buildPiPromptArgs,
1119
1156
  formatWidgetRightLabel,
1120
1157
  observeRunningSubagent,
@@ -1182,17 +1219,17 @@ async function launchSubagent(
1182
1219
  // For new surfaces, pause briefly so the shell is ready before sending the command.
1183
1220
  const surfacePreCreated = !!options?.surface;
1184
1221
  const surface = options?.surface ?? createSurface(params.name);
1222
+ const renameEnvironment = buildTerminalRenameEnvironment(surface);
1185
1223
  const splitFrom = surfacePreCreated ? undefined : (getLastSplitSource() ?? undefined);
1186
1224
  if (!surfacePreCreated) clearLastSplitSource();
1187
1225
  if (!surfacePreCreated) {
1188
1226
  await new Promise<void>((resolve) => setTimeout(resolve, getShellReadyDelayMs()));
1189
1227
  }
1190
1228
 
1191
- const launchBehavior = resolveLaunchBehavior(params, agentDefs);
1229
+ const launchBehavior = resolveLaunchBehavior(agentDefs);
1192
1230
 
1193
1231
  if (launchBehavior.seededSessionMode) {
1194
1232
  seedSubagentSessionFile({
1195
- mode: launchBehavior.seededSessionMode,
1196
1233
  parentSessionFile: sessionFile,
1197
1234
  childSessionFile: subagentSessionFile,
1198
1235
  childCwd: targetCwdForSession,
@@ -1201,31 +1238,27 @@ async function launchSubagent(
1201
1238
 
1202
1239
  const activityFile = getSubagentActivityFile(artifactDir, id);
1203
1240
  mkdirSync(dirname(activityFile), { recursive: true });
1204
- const { inheritsConversationContext } = launchBehavior;
1205
1241
 
1206
- // Build the task message
1207
- // Only full-context fork mode inherits prior conversation state.
1208
- // Blank-session modes need the wrapper instructions and artifact-backed handoff.
1242
+ // Build the task message. All child sessions use artifact-backed delivery.
1209
1243
  const modeHint = agentDefs?.autoExit
1210
1244
  ? "Complete your task autonomously."
1211
1245
  : "Complete your task. When finished, call the subagent_done tool. The user can interact with you at any time.";
1212
1246
  const summaryInstruction = agentDefs?.autoExit
1213
1247
  ? "Your FINAL assistant message should summarize what you accomplished."
1214
1248
  : "Your FINAL assistant message (before calling subagent_done or before the user exits) should summarize what you accomplished.";
1215
- const denySet = resolveDenyTools(agentDefs);
1249
+ const { allowSubagentSpawning } = loadSubagentSpawningConfig();
1250
+ const denySet = resolveDenyTools(agentDefs, allowSubagentSpawning);
1216
1251
  const identity = agentDefs?.body ?? params.systemPrompt ?? null;
1217
1252
  const systemPromptMode = agentDefs?.systemPromptMode;
1218
1253
  const identityInSystemPrompt = systemPromptMode && identity;
1219
1254
  const roleBlock = identity && !identityInSystemPrompt ? `\n\n${identity}` : "";
1220
- const fullTask = inheritsConversationContext
1221
- ? params.task
1222
- : `${roleBlock}\n\n${modeHint}\n\n${params.task}\n\n${summaryInstruction}`;
1255
+ const fullTask = `${roleBlock}\n\n${modeHint}\n\n${params.task}\n\n${summaryInstruction}`;
1223
1256
  // ── Claude Code CLI path ──
1224
1257
  if (agentDefs?.cli === "claude") {
1225
1258
  const sentinelFile = `/tmp/pi-claude-${id}-done`;
1226
1259
  const pluginDir = join(SUBAGENTS_DIR, "plugin");
1227
1260
 
1228
- const cmdParts: string[] = [];
1261
+ const cmdParts: string[] = [renameEnvironment];
1229
1262
  cmdParts.push(`PI_CLAUDE_SENTINEL=${shellEscape(sentinelFile)}`);
1230
1263
  cmdParts.push("claude");
1231
1264
  cmdParts.push("--dangerously-skip-permissions");
@@ -1297,12 +1330,9 @@ async function launchSubagent(
1297
1330
  // ── Pi CLI path ──
1298
1331
 
1299
1332
  // Build pi command
1300
- const parts: string[] = ["pi"];
1333
+ const parts: string[] = ["pi", ...buildChildExtensionArgs()];
1301
1334
  parts.push("--session", shellEscape(subagentSessionFile));
1302
1335
 
1303
- const subagentDonePath = join(SUBAGENTS_DIR, "subagent-done.ts");
1304
- parts.push("-e", shellEscape(subagentDonePath));
1305
-
1306
1336
  if (effectiveModel) {
1307
1337
  const model = effectiveThinking ? `${effectiveModel}:${effectiveThinking}` : effectiveModel;
1308
1338
  parts.push("--model", shellEscape(model));
@@ -1313,7 +1343,7 @@ async function launchSubagent(
1313
1343
  // auto-detect file paths and read their contents.
1314
1344
  if (identityInSystemPrompt && identity) {
1315
1345
  const flag = systemPromptMode === "replace" ? "--system-prompt" : "--append-system-prompt";
1316
- const spTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
1346
+ const spTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, FILE_TIMESTAMP_LENGTH);
1317
1347
  const spSafeName = params.name
1318
1348
  .toLowerCase()
1319
1349
  .replace(/[^a-z0-9\s-]/g, "")
@@ -1332,7 +1362,7 @@ async function launchSubagent(
1332
1362
  }
1333
1363
 
1334
1364
  // Build env prefix: denied tools + subagent identity + config dir propagation
1335
- const envParts: string[] = [];
1365
+ const envParts: string[] = [renameEnvironment];
1336
1366
 
1337
1367
  // If the target cwd has its own .pi/agent/, use that as the config root.
1338
1368
  // Otherwise propagate the current/global agent dir.
@@ -1363,33 +1393,22 @@ async function launchSubagent(
1363
1393
  }
1364
1394
  const envPrefix = envParts.join(" ") + " ";
1365
1395
 
1366
- // Pass task and skill prompts to the sub-agent.
1367
- // Only full-context fork mode gets a direct task argument because it already
1368
- // inherits the parent conversation. Blank-session modes use artifact-backed
1369
- // handoff so the wrapper instructions arrive as the initial user message.
1370
- let taskArg: string;
1371
- if (launchBehavior.taskDelivery === "direct") {
1372
- taskArg = fullTask;
1373
- } else {
1374
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
1375
- const safeName = params.name
1376
- .toLowerCase()
1377
- .replace(/[^a-z0-9\s-]/g, "") // strip everything except alphanumeric, spaces, hyphens
1378
- .replace(/\s+/g, "-") // spaces to hyphens
1379
- .replace(/-+/g, "-") // collapse multiple hyphens
1380
- .replace(/^-|-$/g, ""); // trim leading/trailing hyphens
1381
- const artifactName = `context/${safeName || "subagent"}-${timestamp}.md`;
1382
- const artifactPath = join(artifactDir, artifactName);
1383
- mkdirSync(dirname(artifactPath), { recursive: true });
1384
- writeFileSync(artifactPath, fullTask, "utf8");
1385
- taskArg = `@${artifactPath}`;
1386
- }
1387
-
1388
- for (const promptArg of buildPiPromptArgs({
1389
- effectiveSkills,
1390
- taskDelivery: launchBehavior.taskDelivery,
1391
- taskArg,
1392
- })) {
1396
+ // Pass task and skill prompts to the child. The task is written to an
1397
+ // artifact so wrapper instructions arrive as the initial user message.
1398
+ const artifactTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, FILE_TIMESTAMP_LENGTH);
1399
+ const safeName = params.name
1400
+ .toLowerCase()
1401
+ .replace(/[^a-z0-9\s-]/g, "") // strip everything except alphanumeric, spaces, hyphens
1402
+ .replace(/\s+/g, "-") // spaces to hyphens
1403
+ .replace(/-+/g, "-") // collapse multiple hyphens
1404
+ .replace(/^-|-$/g, ""); // trim leading/trailing hyphens
1405
+ const artifactName = `context/${safeName || "subagent"}-${artifactTimestamp}.md`;
1406
+ const artifactPath = join(artifactDir, artifactName);
1407
+ mkdirSync(dirname(artifactPath), { recursive: true });
1408
+ writeFileSync(artifactPath, fullTask, "utf8");
1409
+ const taskArg = `@${artifactPath}`;
1410
+
1411
+ for (const promptArg of buildPiPromptArgs({ effectiveSkills, taskArg })) {
1393
1412
  parts.push(shellEscape(promptArg));
1394
1413
  }
1395
1414
 
@@ -1416,12 +1435,6 @@ async function launchSubagent(
1416
1435
  ].join("\n"),
1417
1436
  });
1418
1437
 
1419
- // 延迟重命名 agent 标题(左侧侧栏),需要等 pi 启动被 herdr 检测到
1420
- const agentName = params.name;
1421
- const agentSurface = surface;
1422
- setTimeout(() => renameAgent(agentSurface, agentName), 3000);
1423
- setTimeout(() => renameAgent(agentSurface, agentName), 5000);
1424
-
1425
1438
  const running: RunningSubagent = {
1426
1439
  id,
1427
1440
  name: params.name,
@@ -1852,13 +1865,11 @@ export default function subagentsExtension(pi: ExtensionAPI) {
1852
1865
  name: "subagent_interrupt",
1853
1866
  label: "Interrupt Subagent",
1854
1867
  description:
1855
- "Send Escape to the active turn of a currently running Pi-backed subagent. " +
1856
- "The child pane, session, watcher, and running entry remain alive; this returns only a local acknowledgement " +
1857
- "and does not emit a subagent_result solely because of this request.",
1868
+ "Send Escape to stop the active turn of a currently running Pi-backed subagent, then terminate the child Pi process. " +
1869
+ "The parent watcher consumes the completion signal, closes the child pane, and removes the subagent from the running set.",
1858
1870
  promptSnippet:
1859
- "Send Escape to the active turn of a currently running Pi-backed subagent. " +
1860
- "The child pane, session, watcher, and running entry remain alive; this returns only a local acknowledgement " +
1861
- "and does not emit a subagent_result solely because of this request.",
1871
+ "Send Escape to stop the active turn of a currently running Pi-backed subagent, then terminate the child Pi process. " +
1872
+ "The parent watcher consumes the completion signal, closes the child pane, and removes the subagent from the running set.",
1862
1873
  parameters: Type.Object({
1863
1874
  id: Type.Optional(Type.String({ description: "Exact running subagent id" })),
1864
1875
  name: Type.Optional(Type.String({ description: "Exact running subagent display name" })),
@@ -2039,14 +2050,11 @@ export default function subagentsExtension(pi: ExtensionAPI) {
2039
2050
  const entryCountBefore = getNewEntries(params.sessionPath, 0).length;
2040
2051
 
2041
2052
  const surface = createSurface(name);
2053
+ const renameEnvironment = buildTerminalRenameEnvironment(surface);
2042
2054
  await new Promise<void>((resolve) => setTimeout(resolve, getShellReadyDelayMs()));
2043
2055
 
2044
2056
  // Build pi resume command
2045
- const parts = ["pi", "--session", shellEscape(params.sessionPath)];
2046
-
2047
- // Load subagent-done extension so the agent can self-terminate if needed
2048
- const subagentDonePath = join(SUBAGENTS_DIR, "subagent-done.ts");
2049
- parts.push("-e", shellEscape(subagentDonePath));
2057
+ const parts = ["pi", ...buildChildExtensionArgs(), "--session", shellEscape(params.sessionPath)];
2050
2058
 
2051
2059
  const sessionId = ctx.sessionManager.getSessionId();
2052
2060
  const artifactDir = getArtifactDir(ctx.sessionManager.getSessionDir(), sessionId);
@@ -2055,7 +2063,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
2055
2063
 
2056
2064
  let resumeMsgFile: string | undefined;
2057
2065
  if (params.message) {
2058
- const msgTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
2066
+ const msgTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, FILE_TIMESTAMP_LENGTH);
2059
2067
  resumeMsgFile = join(
2060
2068
  artifactDir,
2061
2069
  "subagent-resume",
@@ -2072,14 +2080,19 @@ export default function subagentsExtension(pi: ExtensionAPI) {
2072
2080
  }
2073
2081
 
2074
2082
  // Build env prefix — propagate PI_CODING_AGENT_DIR for config isolation
2075
- const resumeEnvParts: string[] = [];
2083
+ const resumeEnvParts: string[] = [renameEnvironment];
2084
+ const { allowSubagentSpawning } = loadSubagentSpawningConfig();
2076
2085
  if (process.env.PI_CODING_AGENT_DIR) {
2077
2086
  resumeEnvParts.push(`PI_CODING_AGENT_DIR=${shellEscape(process.env.PI_CODING_AGENT_DIR)}`);
2078
2087
  }
2079
2088
  resumeEnvParts.push(`PI_SUBAGENT_NAME=${shellEscape(name)}`);
2080
2089
  resumeEnvParts.push(`PI_SUBAGENT_SESSION=${shellEscape(params.sessionPath)}`);
2081
2090
  resumeEnvParts.push(`PI_SUBAGENT_ID=${shellEscape(id)}`);
2091
+ resumeEnvParts.push(`PI_SUBAGENT_SURFACE=${shellEscape(surface)}`);
2082
2092
  resumeEnvParts.push(`PI_SUBAGENT_ACTIVITY_FILE=${shellEscape(activityFile)}`);
2093
+ resumeEnvParts.push(
2094
+ `PI_DENY_TOOLS=${shellEscape([...resolveDenyTools(null, allowSubagentSpawning)].join(","))}`,
2095
+ );
2083
2096
  if (autoExit) {
2084
2097
  resumeEnvParts.push(`PI_SUBAGENT_AUTO_EXIT=1`);
2085
2098
  }
@@ -2205,18 +2218,6 @@ export default function subagentsExtension(pi: ExtensionAPI) {
2205
2218
  },
2206
2219
  });
2207
2220
 
2208
- // /iterate command — fork the session into a subagent
2209
- pi.registerCommand("iterate", {
2210
- description: "Fork session into a subagent for focused work (bugfixes, iteration)",
2211
- handler: async (args, _ctx) => {
2212
- const task = args.trim() || "";
2213
- const toolCall = task
2214
- ? `Use subagent to fork a session. fork: true, name: "Iterate", task: ${JSON.stringify(task)}`
2215
- : `Use subagent to fork a session. fork: true, name: "Iterate", task: "The user wants to do some hands-on work. Help them with whatever they need."`;
2216
- pi.sendUserMessage(toolCall);
2217
- },
2218
- });
2219
-
2220
2221
  // /subagent command — spawn a subagent by name
2221
2222
  pi.registerCommand("subagent", {
2222
2223
  description: "Spawn a subagent: /subagent <agent> <task>",
@@ -2395,17 +2396,6 @@ export default function subagentsExtension(pi: ExtensionAPI) {
2395
2396
  return;
2396
2397
  }
2397
2398
 
2398
- // Rename workspace and tab to show this is a planning session
2399
- if (isMuxAvailable()) {
2400
- try {
2401
- const label = task.length > 40 ? task.slice(0, 40) + "..." : task;
2402
- renameWorkspace(`🎯 ${label}`);
2403
- renameCurrentTab(`🎯 Plan: ${label}`);
2404
- } catch {
2405
- // non-critical -- do not block the plan
2406
- }
2407
- }
2408
-
2409
2399
  // Load the plan skill from the subagents extension directory
2410
2400
  const planSkillPath = join(SUBAGENTS_DIR, "plan-skill.md");
2411
2401
  let content = readFileSync(planSkillPath, "utf8");
@@ -20,10 +20,22 @@ export interface SubagentHerdrModeConfig {
20
20
  source: SubagentMuxConfigSource;
21
21
  }
22
22
 
23
+ export interface SubagentSpawningConfig {
24
+ allowSubagentSpawning: boolean;
25
+ source: SubagentMuxConfigSource;
26
+ }
27
+
28
+ export interface SubagentExtensionsConfig {
29
+ extensions: string[];
30
+ source: SubagentMuxConfigSource;
31
+ }
32
+
23
33
  const BACKENDS: readonly MuxBackend[] = ["muxy", "cmux", "tmux", "zellij", "wezterm", "herdr", "otty", "orca"];
24
34
  const CONFIG_FILE = "config.json";
25
35
  const HERDR_MODE_ENV = "PI_SUBAGENT_HERDR_MODE";
26
36
  const DEFAULT_HERDR_MODE: HerdrSurfaceMode = HERDR_SURFACE_MODE_SPLIT;
37
+ const DEFAULT_ALLOW_SUBAGENT_SPAWNING = false;
38
+ const DEFAULT_SUBAGENT_EXTENSIONS: readonly string[] = [];
27
39
 
28
40
  function isMuxBackend(value: unknown): value is MuxBackend {
29
41
  return typeof value === "string" && (BACKENDS as readonly string[]).includes(value);
@@ -111,6 +123,29 @@ export function loadHerdrModeConfig(path = muxConfigPath()): SubagentHerdrModeCo
111
123
  return { herdrMode: DEFAULT_HERDR_MODE, source: "default" };
112
124
  }
113
125
 
126
+ /** Read the global child-subagent spawning switch; disabled by default. */
127
+ export function loadSubagentSpawningConfig(path = muxConfigPath()): SubagentSpawningConfig {
128
+ const stored = readConfigObject(path).allowSubagentSpawning;
129
+ if (typeof stored === "boolean") {
130
+ return { allowSubagentSpawning: stored, source: "file" };
131
+ }
132
+
133
+ return { allowSubagentSpawning: DEFAULT_ALLOW_SUBAGENT_SPAWNING, source: "default" };
134
+ }
135
+
136
+ /** Read the explicit extension list for child sessions; empty by default. */
137
+ export function loadSubagentExtensionsConfig(path = muxConfigPath()): SubagentExtensionsConfig {
138
+ const stored = readConfigObject(path).subagentExtensions;
139
+ if (Array.isArray(stored) && stored.every((value) => typeof value === "string" && value.trim())) {
140
+ return {
141
+ extensions: stored.map((value) => value.trim()),
142
+ source: "file",
143
+ };
144
+ }
145
+
146
+ return { extensions: [...DEFAULT_SUBAGENT_EXTENSIONS], source: "default" };
147
+ }
148
+
114
149
  /** Apply persisted mux and Herdr mode settings when no explicit environment override exists. */
115
150
  export function applyPersistedMuxPreference(path = muxConfigPath()): void {
116
151
  if (!environmentPreference()) {
@@ -17,36 +17,10 @@ export interface MessageEntry extends SessionEntry {
17
17
  };
18
18
  }
19
19
 
20
- export type SeededSubagentSessionMode = "lineage-only" | "fork";
21
-
22
- function getForkContentLines(parentSessionFile: string): string[] {
23
- const raw = readFileSync(parentSessionFile, "utf8");
24
- const lines = raw.split("\n").filter((line) => line.trim());
25
-
26
- let truncateAt = lines.length;
27
- for (let i = lines.length - 1; i >= 0; i--) {
28
- try {
29
- const entry = JSON.parse(lines[i]);
30
- if (entry.type === "message" && entry.message?.role === "user") {
31
- truncateAt = i;
32
- break;
33
- }
34
- } catch {
35
- // ignore malformed lines
36
- }
37
- }
38
-
39
- return lines.slice(0, truncateAt).filter((line) => {
40
- try {
41
- return JSON.parse(line).type !== "session";
42
- } catch {
43
- return true;
44
- }
45
- });
46
- }
20
+ export const SUBAGENT_SESSION_MODE_STANDALONE = "standalone" as const;
21
+ export const SUBAGENT_SESSION_MODE_LINEAGE_ONLY = "lineage-only" as const;
47
22
 
48
23
  export function seedSubagentSessionFile(params: {
49
- mode: SeededSubagentSessionMode;
50
24
  parentSessionFile: string;
51
25
  childSessionFile: string;
52
26
  childCwd: string;
@@ -59,9 +33,7 @@ export function seedSubagentSessionFile(params: {
59
33
  cwd: params.childCwd,
60
34
  parentSession: params.parentSessionFile,
61
35
  };
62
- const contentLines =
63
- params.mode === "fork" ? getForkContentLines(params.parentSessionFile) : [];
64
- const lines = [JSON.stringify(header), ...contentLines];
36
+ const lines = [JSON.stringify(header)];
65
37
 
66
38
  mkdirSync(dirname(params.childSessionFile), { recursive: true });
67
39
  writeFileSync(params.childSessionFile, lines.join("\n") + "\n", "utf8");