@maplezzk/pi-interactive-subagents 3.11.0 → 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 +102 -126
- package/pi-extension/subagents/mux-config.ts +35 -0
- package/pi-extension/subagents/session.ts +3 -31
- package/agents/claude-code.md +0 -22
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
|
];
|
|
@@ -882,19 +897,10 @@ function handleSubagentInterrupt(
|
|
|
882
897
|
running.statusState = forceStatusAfterInterrupt(running.statusState, now);
|
|
883
898
|
updateWidget();
|
|
884
899
|
|
|
885
|
-
//
|
|
886
|
-
//
|
|
887
|
-
//
|
|
888
|
-
|
|
889
|
-
const exitFile = `${running.sessionFile}.exit`;
|
|
890
|
-
try {
|
|
891
|
-
writeFileSync(exitFile, JSON.stringify({ type: "done" }));
|
|
892
|
-
} catch (writeErr: any) {
|
|
893
|
-
process.stderr.write(
|
|
894
|
-
`[interrupt] .exit 写入失败 file=${exitFile} err=${writeErr?.message ?? String(writeErr)}\n`,
|
|
895
|
-
);
|
|
896
|
-
}
|
|
897
|
-
}
|
|
900
|
+
// Escape only cancels the child's current turn. Do not write the `.exit`
|
|
901
|
+
// sidecar here: that file is the terminal completion signal consumed by
|
|
902
|
+
// pollForExit, and writing it would close the pane and remove this running
|
|
903
|
+
// entry instead of leaving the child alive for another turn.
|
|
898
904
|
|
|
899
905
|
return {
|
|
900
906
|
content: [{ type: "text" as const, text: `Interrupt requested for subagent "${running.name}".` }],
|
|
@@ -1124,6 +1130,7 @@ export const __test__ = {
|
|
|
1124
1130
|
resolveLaunchBehavior,
|
|
1125
1131
|
resolveEffectiveInteractive,
|
|
1126
1132
|
buildSubagentToolAllowlist,
|
|
1133
|
+
buildChildExtensionArgs,
|
|
1127
1134
|
buildPiPromptArgs,
|
|
1128
1135
|
formatWidgetRightLabel,
|
|
1129
1136
|
observeRunningSubagent,
|
|
@@ -1197,11 +1204,10 @@ async function launchSubagent(
|
|
|
1197
1204
|
await new Promise<void>((resolve) => setTimeout(resolve, getShellReadyDelayMs()));
|
|
1198
1205
|
}
|
|
1199
1206
|
|
|
1200
|
-
const launchBehavior = resolveLaunchBehavior(
|
|
1207
|
+
const launchBehavior = resolveLaunchBehavior(agentDefs);
|
|
1201
1208
|
|
|
1202
1209
|
if (launchBehavior.seededSessionMode) {
|
|
1203
1210
|
seedSubagentSessionFile({
|
|
1204
|
-
mode: launchBehavior.seededSessionMode,
|
|
1205
1211
|
parentSessionFile: sessionFile,
|
|
1206
1212
|
childSessionFile: subagentSessionFile,
|
|
1207
1213
|
childCwd: targetCwdForSession,
|
|
@@ -1210,25 +1216,21 @@ async function launchSubagent(
|
|
|
1210
1216
|
|
|
1211
1217
|
const activityFile = getSubagentActivityFile(artifactDir, id);
|
|
1212
1218
|
mkdirSync(dirname(activityFile), { recursive: true });
|
|
1213
|
-
const { inheritsConversationContext } = launchBehavior;
|
|
1214
1219
|
|
|
1215
|
-
// Build the task message
|
|
1216
|
-
// Only full-context fork mode inherits prior conversation state.
|
|
1217
|
-
// Blank-session modes need the wrapper instructions and artifact-backed handoff.
|
|
1220
|
+
// Build the task message. All child sessions use artifact-backed delivery.
|
|
1218
1221
|
const modeHint = agentDefs?.autoExit
|
|
1219
1222
|
? "Complete your task autonomously."
|
|
1220
1223
|
: "Complete your task. When finished, call the subagent_done tool. The user can interact with you at any time.";
|
|
1221
1224
|
const summaryInstruction = agentDefs?.autoExit
|
|
1222
1225
|
? "Your FINAL assistant message should summarize what you accomplished."
|
|
1223
1226
|
: "Your FINAL assistant message (before calling subagent_done or before the user exits) should summarize what you accomplished.";
|
|
1224
|
-
const
|
|
1227
|
+
const { allowSubagentSpawning } = loadSubagentSpawningConfig();
|
|
1228
|
+
const denySet = resolveDenyTools(agentDefs, allowSubagentSpawning);
|
|
1225
1229
|
const identity = agentDefs?.body ?? params.systemPrompt ?? null;
|
|
1226
1230
|
const systemPromptMode = agentDefs?.systemPromptMode;
|
|
1227
1231
|
const identityInSystemPrompt = systemPromptMode && identity;
|
|
1228
1232
|
const roleBlock = identity && !identityInSystemPrompt ? `\n\n${identity}` : "";
|
|
1229
|
-
const fullTask =
|
|
1230
|
-
? params.task
|
|
1231
|
-
: `${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}`;
|
|
1232
1234
|
// ── Claude Code CLI path ──
|
|
1233
1235
|
if (agentDefs?.cli === "claude") {
|
|
1234
1236
|
const sentinelFile = `/tmp/pi-claude-${id}-done`;
|
|
@@ -1306,12 +1308,9 @@ async function launchSubagent(
|
|
|
1306
1308
|
// ── Pi CLI path ──
|
|
1307
1309
|
|
|
1308
1310
|
// Build pi command
|
|
1309
|
-
const parts: string[] = ["pi"];
|
|
1311
|
+
const parts: string[] = ["pi", ...buildChildExtensionArgs()];
|
|
1310
1312
|
parts.push("--session", shellEscape(subagentSessionFile));
|
|
1311
1313
|
|
|
1312
|
-
const subagentDonePath = join(SUBAGENTS_DIR, "subagent-done.ts");
|
|
1313
|
-
parts.push("-e", shellEscape(subagentDonePath));
|
|
1314
|
-
|
|
1315
1314
|
if (effectiveModel) {
|
|
1316
1315
|
const model = effectiveThinking ? `${effectiveModel}:${effectiveThinking}` : effectiveModel;
|
|
1317
1316
|
parts.push("--model", shellEscape(model));
|
|
@@ -1322,7 +1321,7 @@ async function launchSubagent(
|
|
|
1322
1321
|
// auto-detect file paths and read their contents.
|
|
1323
1322
|
if (identityInSystemPrompt && identity) {
|
|
1324
1323
|
const flag = systemPromptMode === "replace" ? "--system-prompt" : "--append-system-prompt";
|
|
1325
|
-
const spTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0,
|
|
1324
|
+
const spTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, FILE_TIMESTAMP_LENGTH);
|
|
1326
1325
|
const spSafeName = params.name
|
|
1327
1326
|
.toLowerCase()
|
|
1328
1327
|
.replace(/[^a-z0-9\s-]/g, "")
|
|
@@ -1372,33 +1371,22 @@ async function launchSubagent(
|
|
|
1372
1371
|
}
|
|
1373
1372
|
const envPrefix = envParts.join(" ") + " ";
|
|
1374
1373
|
|
|
1375
|
-
// Pass task and skill prompts to the
|
|
1376
|
-
//
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
const artifactPath = join(artifactDir, artifactName);
|
|
1392
|
-
mkdirSync(dirname(artifactPath), { recursive: true });
|
|
1393
|
-
writeFileSync(artifactPath, fullTask, "utf8");
|
|
1394
|
-
taskArg = `@${artifactPath}`;
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
for (const promptArg of buildPiPromptArgs({
|
|
1398
|
-
effectiveSkills,
|
|
1399
|
-
taskDelivery: launchBehavior.taskDelivery,
|
|
1400
|
-
taskArg,
|
|
1401
|
-
})) {
|
|
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 })) {
|
|
1402
1390
|
parts.push(shellEscape(promptArg));
|
|
1403
1391
|
}
|
|
1404
1392
|
|
|
@@ -2051,11 +2039,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2051
2039
|
await new Promise<void>((resolve) => setTimeout(resolve, getShellReadyDelayMs()));
|
|
2052
2040
|
|
|
2053
2041
|
// Build pi resume command
|
|
2054
|
-
const parts = ["pi", "--session", shellEscape(params.sessionPath)];
|
|
2055
|
-
|
|
2056
|
-
// Load subagent-done extension so the agent can self-terminate if needed
|
|
2057
|
-
const subagentDonePath = join(SUBAGENTS_DIR, "subagent-done.ts");
|
|
2058
|
-
parts.push("-e", shellEscape(subagentDonePath));
|
|
2042
|
+
const parts = ["pi", ...buildChildExtensionArgs(), "--session", shellEscape(params.sessionPath)];
|
|
2059
2043
|
|
|
2060
2044
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
2061
2045
|
const artifactDir = getArtifactDir(ctx.sessionManager.getSessionDir(), sessionId);
|
|
@@ -2064,7 +2048,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2064
2048
|
|
|
2065
2049
|
let resumeMsgFile: string | undefined;
|
|
2066
2050
|
if (params.message) {
|
|
2067
|
-
const msgTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0,
|
|
2051
|
+
const msgTimestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, FILE_TIMESTAMP_LENGTH);
|
|
2068
2052
|
resumeMsgFile = join(
|
|
2069
2053
|
artifactDir,
|
|
2070
2054
|
"subagent-resume",
|
|
@@ -2082,6 +2066,7 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2082
2066
|
|
|
2083
2067
|
// Build env prefix — propagate PI_CODING_AGENT_DIR for config isolation
|
|
2084
2068
|
const resumeEnvParts: string[] = [];
|
|
2069
|
+
const { allowSubagentSpawning } = loadSubagentSpawningConfig();
|
|
2085
2070
|
if (process.env.PI_CODING_AGENT_DIR) {
|
|
2086
2071
|
resumeEnvParts.push(`PI_CODING_AGENT_DIR=${shellEscape(process.env.PI_CODING_AGENT_DIR)}`);
|
|
2087
2072
|
}
|
|
@@ -2089,6 +2074,9 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2089
2074
|
resumeEnvParts.push(`PI_SUBAGENT_SESSION=${shellEscape(params.sessionPath)}`);
|
|
2090
2075
|
resumeEnvParts.push(`PI_SUBAGENT_ID=${shellEscape(id)}`);
|
|
2091
2076
|
resumeEnvParts.push(`PI_SUBAGENT_ACTIVITY_FILE=${shellEscape(activityFile)}`);
|
|
2077
|
+
resumeEnvParts.push(
|
|
2078
|
+
`PI_DENY_TOOLS=${shellEscape([...resolveDenyTools(null, allowSubagentSpawning)].join(","))}`,
|
|
2079
|
+
);
|
|
2092
2080
|
if (autoExit) {
|
|
2093
2081
|
resumeEnvParts.push(`PI_SUBAGENT_AUTO_EXIT=1`);
|
|
2094
2082
|
}
|
|
@@ -2214,18 +2202,6 @@ export default function subagentsExtension(pi: ExtensionAPI) {
|
|
|
2214
2202
|
},
|
|
2215
2203
|
});
|
|
2216
2204
|
|
|
2217
|
-
// /iterate command — fork the session into a subagent
|
|
2218
|
-
pi.registerCommand("iterate", {
|
|
2219
|
-
description: "Fork session into a subagent for focused work (bugfixes, iteration)",
|
|
2220
|
-
handler: async (args, _ctx) => {
|
|
2221
|
-
const task = args.trim() || "";
|
|
2222
|
-
const toolCall = task
|
|
2223
|
-
? `Use subagent to fork a session. fork: true, name: "Iterate", task: ${JSON.stringify(task)}`
|
|
2224
|
-
: `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."`;
|
|
2225
|
-
pi.sendUserMessage(toolCall);
|
|
2226
|
-
},
|
|
2227
|
-
});
|
|
2228
|
-
|
|
2229
2205
|
// /subagent command — spawn a subagent by name
|
|
2230
2206
|
pi.registerCommand("subagent", {
|
|
2231
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");
|
package/agents/claude-code.md
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: claude-code
|
|
3
|
-
description: Self-driving Claude Code session for deep investigation, experimentation, and code exploration
|
|
4
|
-
cli: claude
|
|
5
|
-
auto-exit: true
|
|
6
|
-
spawning: false
|
|
7
|
-
deny-tools: claude
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
# Claude Code
|
|
11
|
-
|
|
12
|
-
You are a self-driving Claude Code session spawned by pi for hands-on investigation and experimentation.
|
|
13
|
-
|
|
14
|
-
You have full autonomy: bash, file access, git clone, code editing, running tests, building projects — everything a developer can do in a terminal.
|
|
15
|
-
|
|
16
|
-
## Guidelines
|
|
17
|
-
|
|
18
|
-
- Focus on the task given to you
|
|
19
|
-
- Be thorough in your investigation
|
|
20
|
-
- Report concrete findings with evidence (file paths, command output, test results)
|
|
21
|
-
- If you get stuck, explain what you tried and what failed
|
|
22
|
-
- Your final message should summarize what you accomplished and what you found
|