@maplezzk/pi-interactive-subagents 3.11.1 → 3.12.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 +18 -55
- package/README.zh-CN.md +5 -8
- package/SKILL.md +1 -1
- package/config.json.example +2 -0
- package/package.json +1 -1
- package/pi-extension/subagents/index.ts +98 -113
- package/pi-extension/subagents/mux-config.ts +35 -0
- package/pi-extension/subagents/session.ts +3 -31
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 +
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
336
|
-
| `spawning` | boolean |
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
397
|
+
### `spawning` (legacy)
|
|
423
398
|
|
|
424
|
-
|
|
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
|
-
###
|
|
412
|
+
### Global setting
|
|
443
413
|
|
|
444
|
-
|
|
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
|
|
package/README.zh-CN.md
CHANGED
|
@@ -39,10 +39,9 @@ zellij --session pi # 然后运行 pi
|
|
|
39
39
|
|
|
40
40
|
## 主要能力
|
|
41
41
|
|
|
42
|
-
- **4 个主会话工具 +
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
package/config.json.example
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@maplezzk/pi-interactive-subagents",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.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",
|
|
@@ -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 {
|
|
@@ -48,6 +48,8 @@ import {
|
|
|
48
48
|
HERDR_SURFACE_MODES,
|
|
49
49
|
loadHerdrModeConfig,
|
|
50
50
|
loadMuxConfig,
|
|
51
|
+
loadSubagentExtensionsConfig,
|
|
52
|
+
loadSubagentSpawningConfig,
|
|
51
53
|
saveHerdrMode,
|
|
52
54
|
saveMuxPreference,
|
|
53
55
|
SUBAGENT_MUX_BACKENDS,
|
|
@@ -56,6 +58,8 @@ import {
|
|
|
56
58
|
} from "./mux-config.ts";
|
|
57
59
|
|
|
58
60
|
import {
|
|
61
|
+
SUBAGENT_SESSION_MODE_LINEAGE_ONLY,
|
|
62
|
+
SUBAGENT_SESSION_MODE_STANDALONE,
|
|
59
63
|
findLastAssistantMessage,
|
|
60
64
|
getNewEntries,
|
|
61
65
|
seedSubagentSessionFile,
|
|
@@ -168,12 +172,6 @@ const SubagentParams = Type.Object({
|
|
|
168
172
|
"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
173
|
}),
|
|
170
174
|
),
|
|
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
175
|
interactive: Type.Optional(
|
|
178
176
|
Type.Boolean({
|
|
179
177
|
description:
|
|
@@ -201,7 +199,10 @@ const SubagentParams = Type.Object({
|
|
|
201
199
|
),
|
|
202
200
|
});
|
|
203
201
|
|
|
204
|
-
type SubagentSessionMode =
|
|
202
|
+
type SubagentSessionMode =
|
|
203
|
+
| typeof SUBAGENT_SESSION_MODE_STANDALONE
|
|
204
|
+
| typeof SUBAGENT_SESSION_MODE_LINEAGE_ONLY;
|
|
205
|
+
const DEFAULT_SUBAGENT_SESSION_MODE: SubagentSessionMode = SUBAGENT_SESSION_MODE_STANDALONE;
|
|
205
206
|
|
|
206
207
|
interface AgentDefaults {
|
|
207
208
|
model?: string;
|
|
@@ -232,7 +233,7 @@ interface ListedAgentDefinition extends AgentDefinition {
|
|
|
232
233
|
source: AgentSource;
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
/** Tools that are
|
|
236
|
+
/** Tools that are unavailable inside child subagent sessions. */
|
|
236
237
|
const SPAWNING_TOOLS = new Set([
|
|
237
238
|
"subagent",
|
|
238
239
|
"subagent_interrupt",
|
|
@@ -241,21 +242,19 @@ const SPAWNING_TOOLS = new Set([
|
|
|
241
242
|
]);
|
|
242
243
|
|
|
243
244
|
/**
|
|
244
|
-
* Resolve the effective set of denied tool names
|
|
245
|
-
*
|
|
246
|
-
* `
|
|
245
|
+
* Resolve the effective set of denied tool names for a child subagent.
|
|
246
|
+
* The global switch controls whether every child session may create or manage
|
|
247
|
+
* other subagents; the legacy `spawning` field does not override it.
|
|
248
|
+
* `deny-tools` can add individual restrictions on top.
|
|
247
249
|
*/
|
|
248
|
-
function resolveDenyTools(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (agentDefs.spawning === false) {
|
|
254
|
-
for (const t of SPAWNING_TOOLS) denied.add(t);
|
|
255
|
-
}
|
|
250
|
+
function resolveDenyTools(
|
|
251
|
+
agentDefs: AgentDefaults | null,
|
|
252
|
+
allowSubagentSpawning = false,
|
|
253
|
+
): Set<string> {
|
|
254
|
+
const denied = allowSubagentSpawning ? new Set<string>() : new Set(SPAWNING_TOOLS);
|
|
256
255
|
|
|
257
256
|
// deny-tools: explicit list
|
|
258
|
-
if (agentDefs
|
|
257
|
+
if (agentDefs?.denyTools) {
|
|
259
258
|
for (const t of agentDefs.denyTools
|
|
260
259
|
.split(",")
|
|
261
260
|
.map((s) => s.trim())
|
|
@@ -276,6 +275,30 @@ function getBundledAgentsDir(): string {
|
|
|
276
275
|
return join(SUBAGENTS_DIR, "../../agents");
|
|
277
276
|
}
|
|
278
277
|
|
|
278
|
+
const HOME_PREFIX = "~/";
|
|
279
|
+
|
|
280
|
+
/** Resolve an extension path from the user config against the Pi agent directory. */
|
|
281
|
+
function resolveSubagentExtensionPath(extensionPath: string): string {
|
|
282
|
+
if (extensionPath === "~") return homedir();
|
|
283
|
+
if (extensionPath.startsWith(HOME_PREFIX)) return join(homedir(), extensionPath.slice(HOME_PREFIX.length));
|
|
284
|
+
return isAbsolute(extensionPath) ? extensionPath : join(getAgentConfigDir(), extensionPath);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** Load and resolve explicitly configured child-session extension paths. */
|
|
288
|
+
function getConfiguredSubagentExtensions(): string[] {
|
|
289
|
+
return loadSubagentExtensionsConfig().extensions.map(resolveSubagentExtensionPath);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Build the no-discovery flag and explicit extensions for a child Pi command. */
|
|
293
|
+
function buildChildExtensionArgs(extensionPaths = getConfiguredSubagentExtensions()): string[] {
|
|
294
|
+
return [
|
|
295
|
+
NO_EXTENSIONS_FLAG,
|
|
296
|
+
EXTENSION_FLAG,
|
|
297
|
+
shellEscape(join(SUBAGENTS_DIR, SUBAGENT_DONE_EXTENSION_FILE)),
|
|
298
|
+
...extensionPaths.flatMap((extensionPath) => [EXTENSION_FLAG, shellEscape(extensionPath)]),
|
|
299
|
+
];
|
|
300
|
+
}
|
|
301
|
+
|
|
279
302
|
function getFrontmatterValue(frontmatter: string, key: string): string | undefined {
|
|
280
303
|
const match = frontmatter.match(new RegExp(`^${key}:\\s*(.+)$`, "m"));
|
|
281
304
|
return match ? match[1].trim() : undefined;
|
|
@@ -285,8 +308,9 @@ function parseOptionalBoolean(value: string | undefined): boolean | undefined {
|
|
|
285
308
|
return value != null ? value === "true" : undefined;
|
|
286
309
|
}
|
|
287
310
|
|
|
311
|
+
/** Parse a supported session mode; invalid values return undefined. */
|
|
288
312
|
function parseSessionMode(value: string | undefined): SubagentSessionMode | undefined {
|
|
289
|
-
if (value ===
|
|
313
|
+
if (value === SUBAGENT_SESSION_MODE_STANDALONE || value === SUBAGENT_SESSION_MODE_LINEAGE_ONLY) {
|
|
290
314
|
return value;
|
|
291
315
|
}
|
|
292
316
|
return undefined;
|
|
@@ -376,30 +400,20 @@ function getDefaultSessionDirFor(cwd: string, _agentDir: string): string {
|
|
|
376
400
|
return sessionDir;
|
|
377
401
|
}
|
|
378
402
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
agentDefs
|
|
382
|
-
): SubagentSessionMode {
|
|
383
|
-
if (params.fork) return "fork";
|
|
384
|
-
return agentDefs?.sessionMode ?? "standalone";
|
|
403
|
+
/** Resolve the agent-configured session mode, defaulting to standalone. */
|
|
404
|
+
function resolveEffectiveSessionMode(agentDefs: AgentDefaults | null): SubagentSessionMode {
|
|
405
|
+
return agentDefs?.sessionMode ?? DEFAULT_SUBAGENT_SESSION_MODE;
|
|
385
406
|
}
|
|
386
407
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
agentDefs: AgentDefaults | null,
|
|
390
|
-
): {
|
|
408
|
+
/** Resolve launch behavior for standalone and lineage-only child sessions. */
|
|
409
|
+
function resolveLaunchBehavior(agentDefs: AgentDefaults | null): {
|
|
391
410
|
sessionMode: SubagentSessionMode;
|
|
392
|
-
seededSessionMode:
|
|
393
|
-
inheritsConversationContext: boolean;
|
|
394
|
-
taskDelivery: "direct" | "artifact";
|
|
411
|
+
seededSessionMode: typeof SUBAGENT_SESSION_MODE_LINEAGE_ONLY | null;
|
|
395
412
|
} {
|
|
396
|
-
const sessionMode = resolveEffectiveSessionMode(
|
|
397
|
-
const inheritsConversationContext = sessionMode === "fork";
|
|
413
|
+
const sessionMode = resolveEffectiveSessionMode(agentDefs);
|
|
398
414
|
return {
|
|
399
415
|
sessionMode,
|
|
400
|
-
seededSessionMode: sessionMode ===
|
|
401
|
-
inheritsConversationContext,
|
|
402
|
-
taskDelivery: inheritsConversationContext ? "direct" : "artifact",
|
|
416
|
+
seededSessionMode: sessionMode === DEFAULT_SUBAGENT_SESSION_MODE ? null : SUBAGENT_SESSION_MODE_LINEAGE_ONLY,
|
|
403
417
|
};
|
|
404
418
|
}
|
|
405
419
|
|
|
@@ -412,12 +426,11 @@ function resolveLaunchBehavior(
|
|
|
412
426
|
* 3. Default: the inverse of `auto-exit`. Agents that auto-exit are
|
|
413
427
|
* autonomous (scout, worker, reviewer) and the parent session should be
|
|
414
428
|
* woken on stall/recovery transitions. Agents that don't auto-exit are
|
|
415
|
-
* driven by the user in their own pane (planner
|
|
416
|
-
*
|
|
429
|
+
* driven by the user in their own pane (planner) and stall pings are
|
|
430
|
+
* noise.
|
|
417
431
|
*
|
|
418
|
-
* When no agent defs exist at all (bare `subagent({ name, task })` call,
|
|
419
|
-
*
|
|
420
|
-
* subagent is treated as interactive — matching the intent of iterate.
|
|
432
|
+
* When no agent defs exist at all (bare `subagent({ name, task })` call),
|
|
433
|
+
* `autoExit` is undefined and the subagent is treated as interactive.
|
|
421
434
|
*/
|
|
422
435
|
function resolveEffectiveInteractive(
|
|
423
436
|
params: Static<typeof SubagentParams>,
|
|
@@ -706,6 +719,10 @@ function updateWidget() {
|
|
|
706
719
|
* as standalone prompts in the child session.
|
|
707
720
|
*/
|
|
708
721
|
const SUBAGENT_CONTROL_TOOLS = ["caller_ping", "subagent_done"] as const;
|
|
722
|
+
const FILE_TIMESTAMP_LENGTH = 19;
|
|
723
|
+
const NO_EXTENSIONS_FLAG = "--no-extensions";
|
|
724
|
+
const EXTENSION_FLAG = "--extension";
|
|
725
|
+
const SUBAGENT_DONE_EXTENSION_FILE = "subagent-done.ts";
|
|
709
726
|
|
|
710
727
|
/**
|
|
711
728
|
* Build the child --tools allowlist.
|
|
@@ -731,21 +748,19 @@ function buildSubagentToolAllowlist(effectiveTools?: string): string | null {
|
|
|
731
748
|
return [...allow].join(",");
|
|
732
749
|
}
|
|
733
750
|
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
}): string[] {
|
|
751
|
+
/**
|
|
752
|
+
* Build positional prompt args for a child launch.
|
|
753
|
+
* Skills must be separate prompts so Pi expands each `/skill:` directive.
|
|
754
|
+
*/
|
|
755
|
+
function buildPiPromptArgs(params: { effectiveSkills?: string; taskArg: string }): string[] {
|
|
739
756
|
const skillPrompts = (params.effectiveSkills ?? "")
|
|
740
757
|
.split(",")
|
|
741
758
|
.map((s) => s.trim())
|
|
742
759
|
.filter(Boolean)
|
|
743
760
|
.map((skill) => `/skill:${skill}`);
|
|
744
761
|
|
|
745
|
-
const needsSeparator = params.taskDelivery === "artifact" && skillPrompts.length > 0;
|
|
746
|
-
|
|
747
762
|
return [
|
|
748
|
-
...(
|
|
763
|
+
...(skillPrompts.length > 0 ? [""] : []),
|
|
749
764
|
...skillPrompts,
|
|
750
765
|
params.taskArg,
|
|
751
766
|
];
|
|
@@ -1115,6 +1130,7 @@ export const __test__ = {
|
|
|
1115
1130
|
resolveLaunchBehavior,
|
|
1116
1131
|
resolveEffectiveInteractive,
|
|
1117
1132
|
buildSubagentToolAllowlist,
|
|
1133
|
+
buildChildExtensionArgs,
|
|
1118
1134
|
buildPiPromptArgs,
|
|
1119
1135
|
formatWidgetRightLabel,
|
|
1120
1136
|
observeRunningSubagent,
|
|
@@ -1188,11 +1204,10 @@ async function launchSubagent(
|
|
|
1188
1204
|
await new Promise<void>((resolve) => setTimeout(resolve, getShellReadyDelayMs()));
|
|
1189
1205
|
}
|
|
1190
1206
|
|
|
1191
|
-
const launchBehavior = resolveLaunchBehavior(
|
|
1207
|
+
const launchBehavior = resolveLaunchBehavior(agentDefs);
|
|
1192
1208
|
|
|
1193
1209
|
if (launchBehavior.seededSessionMode) {
|
|
1194
1210
|
seedSubagentSessionFile({
|
|
1195
|
-
mode: launchBehavior.seededSessionMode,
|
|
1196
1211
|
parentSessionFile: sessionFile,
|
|
1197
1212
|
childSessionFile: subagentSessionFile,
|
|
1198
1213
|
childCwd: targetCwdForSession,
|
|
@@ -1201,25 +1216,21 @@ async function launchSubagent(
|
|
|
1201
1216
|
|
|
1202
1217
|
const activityFile = getSubagentActivityFile(artifactDir, id);
|
|
1203
1218
|
mkdirSync(dirname(activityFile), { recursive: true });
|
|
1204
|
-
const { inheritsConversationContext } = launchBehavior;
|
|
1205
1219
|
|
|
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.
|
|
1220
|
+
// Build the task message. All child sessions use artifact-backed delivery.
|
|
1209
1221
|
const modeHint = agentDefs?.autoExit
|
|
1210
1222
|
? "Complete your task autonomously."
|
|
1211
1223
|
: "Complete your task. When finished, call the subagent_done tool. The user can interact with you at any time.";
|
|
1212
1224
|
const summaryInstruction = agentDefs?.autoExit
|
|
1213
1225
|
? "Your FINAL assistant message should summarize what you accomplished."
|
|
1214
1226
|
: "Your FINAL assistant message (before calling subagent_done or before the user exits) should summarize what you accomplished.";
|
|
1215
|
-
const
|
|
1227
|
+
const { allowSubagentSpawning } = loadSubagentSpawningConfig();
|
|
1228
|
+
const denySet = resolveDenyTools(agentDefs, allowSubagentSpawning);
|
|
1216
1229
|
const identity = agentDefs?.body ?? params.systemPrompt ?? null;
|
|
1217
1230
|
const systemPromptMode = agentDefs?.systemPromptMode;
|
|
1218
1231
|
const identityInSystemPrompt = systemPromptMode && identity;
|
|
1219
1232
|
const roleBlock = identity && !identityInSystemPrompt ? `\n\n${identity}` : "";
|
|
1220
|
-
const fullTask =
|
|
1221
|
-
? params.task
|
|
1222
|
-
: `${roleBlock}\n\n${modeHint}\n\n${params.task}\n\n${summaryInstruction}`;
|
|
1233
|
+
const fullTask = `${roleBlock}\n\n${modeHint}\n\n${params.task}\n\n${summaryInstruction}`;
|
|
1223
1234
|
// ── Claude Code CLI path ──
|
|
1224
1235
|
if (agentDefs?.cli === "claude") {
|
|
1225
1236
|
const sentinelFile = `/tmp/pi-claude-${id}-done`;
|
|
@@ -1297,12 +1308,9 @@ async function launchSubagent(
|
|
|
1297
1308
|
// ── Pi CLI path ──
|
|
1298
1309
|
|
|
1299
1310
|
// Build pi command
|
|
1300
|
-
const parts: string[] = ["pi"];
|
|
1311
|
+
const parts: string[] = ["pi", ...buildChildExtensionArgs()];
|
|
1301
1312
|
parts.push("--session", shellEscape(subagentSessionFile));
|
|
1302
1313
|
|
|
1303
|
-
const subagentDonePath = join(SUBAGENTS_DIR, "subagent-done.ts");
|
|
1304
|
-
parts.push("-e", shellEscape(subagentDonePath));
|
|
1305
|
-
|
|
1306
1314
|
if (effectiveModel) {
|
|
1307
1315
|
const model = effectiveThinking ? `${effectiveModel}:${effectiveThinking}` : effectiveModel;
|
|
1308
1316
|
parts.push("--model", shellEscape(model));
|
|
@@ -1313,7 +1321,7 @@ async function launchSubagent(
|
|
|
1313
1321
|
// auto-detect file paths and read their contents.
|
|
1314
1322
|
if (identityInSystemPrompt && identity) {
|
|
1315
1323
|
const flag = systemPromptMode === "replace" ? "--system-prompt" : "--append-system-prompt";
|
|
1316
|
-
const spTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0,
|
|
1324
|
+
const spTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, FILE_TIMESTAMP_LENGTH);
|
|
1317
1325
|
const spSafeName = params.name
|
|
1318
1326
|
.toLowerCase()
|
|
1319
1327
|
.replace(/[^a-z0-9\s-]/g, "")
|
|
@@ -1363,33 +1371,22 @@ async function launchSubagent(
|
|
|
1363
1371
|
}
|
|
1364
1372
|
const envPrefix = envParts.join(" ") + " ";
|
|
1365
1373
|
|
|
1366
|
-
// Pass task and skill prompts to the
|
|
1367
|
-
//
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
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
|
-
})) {
|
|
1374
|
+
// Pass task and skill prompts to the child. The task is written to an
|
|
1375
|
+
// artifact so wrapper instructions arrive as the initial user message.
|
|
1376
|
+
const artifactTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, FILE_TIMESTAMP_LENGTH);
|
|
1377
|
+
const safeName = params.name
|
|
1378
|
+
.toLowerCase()
|
|
1379
|
+
.replace(/[^a-z0-9\s-]/g, "") // strip everything except alphanumeric, spaces, hyphens
|
|
1380
|
+
.replace(/\s+/g, "-") // spaces to hyphens
|
|
1381
|
+
.replace(/-+/g, "-") // collapse multiple hyphens
|
|
1382
|
+
.replace(/^-|-$/g, ""); // trim leading/trailing hyphens
|
|
1383
|
+
const artifactName = `context/${safeName || "subagent"}-${artifactTimestamp}.md`;
|
|
1384
|
+
const artifactPath = join(artifactDir, artifactName);
|
|
1385
|
+
mkdirSync(dirname(artifactPath), { recursive: true });
|
|
1386
|
+
writeFileSync(artifactPath, fullTask, "utf8");
|
|
1387
|
+
const taskArg = `@${artifactPath}`;
|
|
1388
|
+
|
|
1389
|
+
for (const promptArg of buildPiPromptArgs({ effectiveSkills, taskArg })) {
|
|
1393
1390
|
parts.push(shellEscape(promptArg));
|
|
1394
1391
|
}
|
|
1395
1392
|
|
|
@@ -2042,11 +2039,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2042
2039
|
await new Promise<void>((resolve) => setTimeout(resolve, getShellReadyDelayMs()));
|
|
2043
2040
|
|
|
2044
2041
|
// 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));
|
|
2042
|
+
const parts = ["pi", ...buildChildExtensionArgs(), "--session", shellEscape(params.sessionPath)];
|
|
2050
2043
|
|
|
2051
2044
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
2052
2045
|
const artifactDir = getArtifactDir(ctx.sessionManager.getSessionDir(), sessionId);
|
|
@@ -2055,7 +2048,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2055
2048
|
|
|
2056
2049
|
let resumeMsgFile: string | undefined;
|
|
2057
2050
|
if (params.message) {
|
|
2058
|
-
const msgTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0,
|
|
2051
|
+
const msgTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, FILE_TIMESTAMP_LENGTH);
|
|
2059
2052
|
resumeMsgFile = join(
|
|
2060
2053
|
artifactDir,
|
|
2061
2054
|
"subagent-resume",
|
|
@@ -2073,6 +2066,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2073
2066
|
|
|
2074
2067
|
// Build env prefix — propagate PI_CODING_AGENT_DIR for config isolation
|
|
2075
2068
|
const resumeEnvParts: string[] = [];
|
|
2069
|
+
const { allowSubagentSpawning } = loadSubagentSpawningConfig();
|
|
2076
2070
|
if (process.env.PI_CODING_AGENT_DIR) {
|
|
2077
2071
|
resumeEnvParts.push(`PI_CODING_AGENT_DIR=${shellEscape(process.env.PI_CODING_AGENT_DIR)}`);
|
|
2078
2072
|
}
|
|
@@ -2080,6 +2074,9 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2080
2074
|
resumeEnvParts.push(`PI_SUBAGENT_SESSION=${shellEscape(params.sessionPath)}`);
|
|
2081
2075
|
resumeEnvParts.push(`PI_SUBAGENT_ID=${shellEscape(id)}`);
|
|
2082
2076
|
resumeEnvParts.push(`PI_SUBAGENT_ACTIVITY_FILE=${shellEscape(activityFile)}`);
|
|
2077
|
+
resumeEnvParts.push(
|
|
2078
|
+
`PI_DENY_TOOLS=${shellEscape([...resolveDenyTools(null, allowSubagentSpawning)].join(","))}`,
|
|
2079
|
+
);
|
|
2083
2080
|
if (autoExit) {
|
|
2084
2081
|
resumeEnvParts.push(`PI_SUBAGENT_AUTO_EXIT=1`);
|
|
2085
2082
|
}
|
|
@@ -2205,18 +2202,6 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2205
2202
|
},
|
|
2206
2203
|
});
|
|
2207
2204
|
|
|
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
2205
|
// /subagent command — spawn a subagent by name
|
|
2221
2206
|
pi.registerCommand("subagent", {
|
|
2222
2207
|
description: "Spawn a subagent: /subagent <agent> <task>",
|
|
@@ -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
|
|
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
|
|
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");
|