pi-herdr-subagents-lite 0.0.0-stage → 0.2.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.
Files changed (44) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +197 -2
  3. package/README.zh-CN.md +196 -0
  4. package/package.json +41 -4
  5. package/src/bridge/index.ts +24 -0
  6. package/src/config/index.ts +122 -0
  7. package/src/config/schema.ts +147 -0
  8. package/src/extension/auto-exit.ts +35 -0
  9. package/src/extension/index.ts +144 -0
  10. package/src/extension/shutdown.ts +39 -0
  11. package/src/herdr/agent.ts +72 -0
  12. package/src/herdr/cli.ts +87 -0
  13. package/src/herdr/index.ts +35 -0
  14. package/src/herdr/pane.ts +124 -0
  15. package/src/inbox/index.ts +76 -0
  16. package/src/inbox/pump.ts +18 -0
  17. package/src/index.ts +19 -0
  18. package/src/profile/examples.ts +20 -0
  19. package/src/profile/frontmatter.ts +121 -0
  20. package/src/profile/index.ts +20 -0
  21. package/src/profile/layers.ts +122 -0
  22. package/src/run/aggregate.ts +54 -0
  23. package/src/run/artifact.ts +11 -0
  24. package/src/run/close.ts +34 -0
  25. package/src/run/errors.ts +21 -0
  26. package/src/run/find.ts +6 -0
  27. package/src/run/index.ts +107 -0
  28. package/src/run/naming.ts +38 -0
  29. package/src/run/poll.ts +139 -0
  30. package/src/run/resolve.ts +23 -0
  31. package/src/run/result.ts +54 -0
  32. package/src/run/send.ts +99 -0
  33. package/src/run/spawn-args.ts +49 -0
  34. package/src/run/start.ts +135 -0
  35. package/src/run/state.ts +51 -0
  36. package/src/run/truncate.ts +5 -0
  37. package/src/session-file/index.ts +22 -0
  38. package/src/session-file/jsonl.ts +73 -0
  39. package/src/session-file/receipt.ts +25 -0
  40. package/src/tool/errors.ts +22 -0
  41. package/src/tool/index.ts +58 -0
  42. package/src/tool/message.ts +25 -0
  43. package/src/tool/schema.ts +43 -0
  44. package/src/tool/text.ts +31 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 setwhite
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,198 @@
1
- # Temporary Holding Version
1
+ # pi-herdr-subagents-lite
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ English | [简体中文](README.zh-CN.md)
4
+
5
+ A pi extension that runs subagents as separate pi processes in herdr panes. One tool, two actions: `run` starts a subagent, `send` continues it, results come back asynchronously.
6
+
7
+ Experimental; compatibility across versions is not promised. Exact behavior lives in the [v1 contract](docs/specs/0001-v1-contract.md). Project docs are in Chinese.
8
+
9
+ ## Features
10
+
11
+ - **Separate process, human takeover**: each subagent is its own pi process in a herdr pane. Same cwd as the parent opens a tab in the same workspace, otherwise a new workspace. Switch into the pane and take over at any time.
12
+ - **Profile layers**: Markdown + frontmatter under `<agent-dir>/pi-herdr-subagents-lite/profiles/`, plus a project layer under `.pi/pi-herdr-subagents-lite/profiles/` that wins on the same name.
13
+ - **Async, merged results**: `run` returns at once. Each finished child reports one `subagent_result` message; results inside `aggregateWindowMs` merge into one. Oversize results go to an artifact file.
14
+ - **Questions stay with the human**: a child prompt marks its pane `herdr:blocked` and nothing goes to the parent. The human answers in the child pane; no model relays the answer.
15
+ - **No nesting by default, no sandbox**: child tool sets drop this tool unless the profile names it. The profile picks tools; the extension does not restrict writes.
16
+
17
+ ## Requirements
18
+
19
+ herdr `0.9.5`, pi `1.4.0`, Node `>= 22.19`. The parent must run inside a herdr pane, otherwise calls fail with `not_in_herdr`.
20
+
21
+ ## Install
22
+
23
+ Pick one source, then restart pi.
24
+
25
+ ```bash
26
+ pi install npm:pi-herdr-subagents-lite # npm
27
+ pi install git:github.com/setwhite/pi-herdr-subagents-lite # git
28
+ pi install /path/to/pi-herdr-subagents-lite # local checkout
29
+ pi -e /path/to/pi-herdr-subagents-lite/src/index.ts # this process only
30
+ ```
31
+
32
+ Do not pass `-l`: it writes project settings only, so children started in another workspace never load the package and both the inbox pump and the bridge stop working. `pi -e` is single-process too.
33
+
34
+ Upgrade with `pi update --extensions`, remove with `pi remove <source>`. After a restart the tool list shows `subagent` (or your `toolName`).
35
+
36
+ ## Quick start
37
+
38
+ 1. Start `pi` in a herdr pane.
39
+ 2. Write a profile (next section); the file name without `.md` is the name `run` takes.
40
+ 3. Ask the model to call the tool:
41
+
42
+ ```json
43
+ {"action":"run","profile":"explore","task":"Read README.md and answer with its first heading."}
44
+ ```
45
+
46
+ It returns `{"state":"running",...}`, runs in its own tab, and a `[subagent explore-1] done` message arrives later. Do not wait for it in the same turn. Continue that child with:
47
+
48
+ ```json
49
+ {"action":"send","name":"explore-1","message":"Also check the Windows branch."}
50
+ ```
51
+
52
+ ## Profile
53
+
54
+ Global profiles live in `<agent-dir>/pi-herdr-subagents-lite/profiles/*.md`, project profiles in `.pi/pi-herdr-subagents-lite/profiles/*.md` (read once the project is trusted); the project wins on the same name.
55
+
56
+ ```markdown
57
+ ---
58
+ # Listed as "file (name)"; run takes the file name
59
+ name: Explore
60
+ description: "Read-only research: read code, answer questions, change nothing"
61
+
62
+ # Omit to inherit from the parent
63
+ # model: provider/model
64
+ thinking: low
65
+
66
+ # Plain names = strict allowlist; +name / -name adjusts the default set, never mix both
67
+ # Omitted = pi defaults minus this tool (nesting off); "" = empty allowlist
68
+ tools: read, bash
69
+
70
+ # Omitted = pi discovery; "" = no skills; a list = only those
71
+ skills: ""
72
+
73
+ # append (default) / replace: body is appended to or replaces the system prompt
74
+ # task: body is the task text, no identity file
75
+ system-prompt: append
76
+
77
+ # Shut down and close the tab when settled; waits while a prompt is open or the inbox has messages
78
+ auto-exit: true
79
+
80
+ # true = hidden from the guidelines; an explicit run still works
81
+ disable-model-invocation: false
82
+ ---
83
+
84
+ You are a read-only explorer. Answer with file paths and line numbers.
85
+ ```
86
+
87
+ Defaults and error codes: [contract section 2](docs/specs/0001-v1-contract.md#2-profile). A file that fails to parse stays out of the list; `run` returns `profile_invalid` with the file, field and reason.
88
+
89
+ A bundled example ships with the extension. Loading reads `profiles/*.md` only and the example is written only when missing, so copy it one level up:
90
+
91
+ ```bash
92
+ cd ~/.pi/agent/pi-herdr-subagents-lite/profiles && cp examples/domain.md domain.md
93
+ ```
94
+
95
+ ## Configuration
96
+
97
+ One global file: `<agent-dir>/pi-herdr-subagents-lite/config.json`. `<agent-dir>` defaults to `~/.pi/agent` and follows `PI_CODING_AGENT_DIR`. Project config is not read.
98
+
99
+ ### Minimal config
100
+
101
+ A missing file means all defaults. To change one value, write only that value:
102
+
103
+ ```json
104
+ {
105
+ "maxConcurrent": 4
106
+ }
107
+ ```
108
+
109
+ ### Full config
110
+
111
+ Every key with its default. Strict JSON: an unknown key, a wrong type or a bad enum makes the extension fail to load and pi exits naming the field. The `//` lines are comments; delete them before use.
112
+
113
+ ```jsonc
114
+ {
115
+ // Gates tool registration only; the bridge has its own switch
116
+ "enabled": true,
117
+
118
+ // Name the model sees; ^[a-z][a-z0-9_]{0,31}$
119
+ "toolName": "subagent",
120
+
121
+ // Resolved from PATH; set an absolute path to pin a version
122
+ "herdrBin": "herdr",
123
+
124
+ // Live children per parent session; auto-exit false keeps its slot after settling
125
+ "maxConcurrent": 16,
126
+
127
+ // Per result (1024-1048576); beyond it the full text goes to an artifact and the message keeps a truncated head plus the path
128
+ "maxResultChars": 16384,
129
+
130
+ // Ms (0-10000); results inside the window merge into one message, 0 = deliver each immediately
131
+ "aggregateWindowMs": 500,
132
+
133
+ // Parent shutdown reasons that close its child tabs: new / resume / fork / quit / reload; empty = never
134
+ "closeChildrenOn": [],
135
+
136
+ // Child inbox polling interval in ms (50-5000)
137
+ "inboxPollMs": 250,
138
+
139
+ // Parent agent list polling in ms (100-60000); larger saves processes, smaller spots finished children sooner
140
+ "pollIntervalMs": 1000,
141
+
142
+ // Override tool text. Omitted keys keep the built-in text; the values are placeholders, drop the whole block if unused
143
+ "prompts": {
144
+ "description": "One line about what this tool does",
145
+ "snippet": "One line shown in Available tools",
146
+ // Keep a line containing {{profiles}} to keep the profile list
147
+ "guidelines": ["Do not wait for a result in this turn.", "Available profiles:\n{{profiles}}"]
148
+ },
149
+
150
+ // Turns pi prompt events into herdr:blocked, for native notifications
151
+ "bridge": {
152
+ "enabled": true,
153
+ // Which prompt kinds count as blocked
154
+ "blockedOn": ["custom", "select", "confirm", "input", "editor"]
155
+ }
156
+ }
157
+ ```
158
+
159
+ Out-of-range numbers are clamped to the boundary, not rejected. The config is read once, so restart pi after editing.
160
+
161
+ ## Reference projects
162
+
163
+ The design is based on these projects. Thank you.
164
+
165
+ | Project | Borrowed |
166
+ |---|---|
167
+ | [earendil-works/pi](https://github.com/earendil-works/pi) | Extension API, package layout, official examples |
168
+ | [fyang93/pi-herdr-subagents](https://github.com/fyang93/pi-herdr-subagents) | Profile field shapes; integration tests on an isolated herdr session |
169
+ | [modem-dev/pi-herdr-subagents](https://github.com/modem-dev/pi-herdr-subagents) | herdr launch and lifecycle; derived end states |
170
+ | [purboo/pi-durable-subagents](https://github.com/purboo/pi-durable-subagents) | Async delivery, idempotency, report-once results |
171
+
172
+ ## Docs
173
+
174
+ All docs are in Chinese.
175
+
176
+ - [ARCHITECTURE](docs/ARCHITECTURE.md): module boundaries, dependency direction, test seams
177
+ - [GLOSSARY](docs/GLOSSARY.md): the only source for terms
178
+ - [Modules](docs/modules/): usage and trade-offs per module
179
+
180
+ ## Development
181
+
182
+ ```bash
183
+ pnpm install # dependencies
184
+ pnpm check # lint + import boundaries + types + unit tests
185
+ pnpm test # unit tests (vitest)
186
+ pnpm lint # biome
187
+ pnpm typecheck # tsc --noEmit
188
+ pnpm check:imports # dependency direction, per docs/ARCHITECTURE.md
189
+ pnpm test:e2e # real herdr plus real pi children, not part of pnpm check
190
+ ```
191
+
192
+ Run `git config core.hooksPath .githooks` once to run `pnpm check` on every commit.
193
+
194
+ E2E starts an isolated herdr session and agent directory, and skips the suite with a reason when herdr, pi or `auth.json` is missing. `PI_E2E_MODEL` picks another model; `PI_E2E_KEEP=1` keeps the temp directories.
195
+
196
+ ## License
197
+
198
+ MIT, see [LICENSE](LICENSE).
@@ -0,0 +1,196 @@
1
+ # pi-herdr-subagents-lite
2
+
3
+ [English](README.md) | 简体中文
4
+
5
+ pi 扩展:让子代理作为独立 pi 进程跑在 herdr pane 里。一个工具,两个 action:`run` 起子代理,`send` 续聊,结果异步回投。
6
+
7
+ 实验阶段,不保证跨版本兼容。行为以 [v1 契约](docs/specs/0001-v1-contract.md) 为准。
8
+
9
+ ## 功能
10
+
11
+ - **独立进程,可人工接管**:每个子代理是 herdr pane 里独立的 pi 进程。cwd 与父相同则同 workspace 新开 tab,否则新建 workspace。随时切进去接管。
12
+ - **两层 profile**:Markdown + frontmatter,全局 `<agent-dir>/pi-herdr-subagents-lite/profiles/`,项目层 `.pi/pi-herdr-subagents-lite/profiles/`,同名时项目层覆盖。
13
+ - **异步、可合并的结果**:`run` 立即返回。每个子代理收工时投一条 `subagent_result`;落在 `aggregateWindowMs` 内的结果合并成一条。超长结果落 artifact 文件。
14
+ - **反问留在人这边**:子代理弹出对话框时只发出 `herdr:blocked` 事件,父侧什么也收不到。人在子 pane 里回答,不经模型转达。
15
+ - **默认不嵌套,也不是沙箱**:子代理的工具集默认去掉本工具,profile 里点名才授予。工具由 profile 决定,扩展不限制写操作。
16
+
17
+ ## 要求
18
+
19
+ herdr `0.9.5`、pi `1.4.0`、Node `>= 22.19`。父会话必须在 herdr pane 里,否则调用返回 `not_in_herdr`。
20
+
21
+ ## 安装
22
+
23
+ 选一种来源,装完重启 pi。
24
+
25
+ ```bash
26
+ pi install npm:pi-herdr-subagents-lite # npm
27
+ pi install git:github.com/setwhite/pi-herdr-subagents-lite # git
28
+ pi install /path/to/pi-herdr-subagents-lite # 本地源码
29
+ pi -e /path/to/pi-herdr-subagents-lite/src/index.ts # 只对当前进程生效
30
+ ```
31
+
32
+ 不要加 `-l`:它只写项目设置,在别的 workspace 里起的子代理就加载不到,inbox 泵和 bridge 都会失效。`pi -e` 同样是单进程。
33
+
34
+ 升级用 `pi update --extensions`,卸载用 `pi remove <source>`。重启后工具列表里出现 `subagent`(或你配的 `toolName`)。
35
+
36
+ ## 快速开始
37
+
38
+ 1. 在 herdr pane 里启动 `pi`。
39
+ 2. 写一个 profile(下一节);文件名去掉 `.md` 就是 `run` 的寻址名。
40
+ 3. 让模型调用工具:
41
+
42
+ ```json
43
+ {"action":"run","profile":"explore","task":"读 README.md,回答它的第一个标题。"}
44
+ ```
45
+
46
+ 它返回 `{"state":"running",...}`,子代理在自己的 tab 里跑,稍后收到 `[subagent explore-1] done` 消息。不要在同一轮里等结果。续聊那个子代理:
47
+
48
+ ```json
49
+ {"action":"send","name":"explore-1","message":"也看一下 Windows 那条分支。"}
50
+ ```
51
+
52
+ ## Profile
53
+
54
+ 全局 profile 在 `<agent-dir>/pi-herdr-subagents-lite/profiles/*.md`,项目 profile 在 `.pi/pi-herdr-subagents-lite/profiles/*.md`(项目受信任后才读);同名时项目覆盖全局。
55
+
56
+ ```markdown
57
+ ---
58
+ # 列表写作「文件名 (显示名)」;run 取文件名
59
+ name: Explore
60
+ description: 只读调研:读代码、回答问题,不改动任何文件
61
+
62
+ # 省略即继承父会话
63
+ # model: provider/model
64
+ thinking: low
65
+
66
+ # 纯名字 = 严格白名单;+名字 / -名字 在默认集上增删,两者不能混用
67
+ # 省略 = pi 默认集去掉本工具(不嵌套);"" = 空白名单
68
+ tools: read, bash
69
+
70
+ # 省略 = 由 pi 发现;"" = 不加载;列表 = 只加载列出的
71
+ skills: ""
72
+
73
+ # append(默认)/ replace:正文追加到或替换系统提示
74
+ # task:正文就是任务文本,不写身份文件
75
+ system-prompt: append
76
+
77
+ # 收工后自行退出并关掉 tab;有打开的对话框或 inbox 待投递时会等
78
+ auto-exit: true
79
+
80
+ # true = 从工具说明里隐藏;显式 run 仍然可用
81
+ disable-model-invocation: false
82
+ ---
83
+
84
+ 你是一名只读探索者,回答时给出文件路径与行号。
85
+ ```
86
+
87
+ 字段默认值与错误码见 [契约 §2](docs/specs/0001-v1-contract.md#2-profile)。解析失败的文件不进列表;对它 `run` 返回 `profile_invalid`,带文件、字段与原因。
88
+
89
+ 扩展自带一个示例 profile。加载只读 `profiles/*.md`,示例也只在缺失时写入,所以复制一层上去即可:
90
+
91
+ ```bash
92
+ cd ~/.pi/agent/pi-herdr-subagents-lite/profiles && cp examples/domain.md domain.md
93
+ ```
94
+
95
+ ## 配置
96
+
97
+ 只有一个全局文件:`<agent-dir>/pi-herdr-subagents-lite/config.json`。`<agent-dir>` 默认 `~/.pi/agent`,跟随 `PI_CODING_AGENT_DIR`。项目级配置不读。
98
+
99
+ ### 最小配置
100
+
101
+ 文件不存在就全用默认值。只改一项,就只写这一项:
102
+
103
+ ```json
104
+ {
105
+ "maxConcurrent": 4
106
+ }
107
+ ```
108
+
109
+ ### 完整配置
110
+
111
+ 逐项列出,值是默认值。必须是严格 JSON:未知键、类型不对、枚举非法都会让扩展加载失败,pi 报出字段名退出。`//` 行是注释,用之前删掉。
112
+
113
+ ```jsonc
114
+ {
115
+ // 只管工具注册;bridge 有自己的开关
116
+ "enabled": true,
117
+
118
+ // 模型看到的工具名;^[a-z][a-z0-9_]{0,31}$
119
+ "toolName": "subagent",
120
+
121
+ // 从 PATH 解析;写绝对路径可锁定版本
122
+ "herdrBin": "herdr",
123
+
124
+ // 每个父会话并存的子代理数;auto-exit 为 false 的子代理收工后仍占槽
125
+ "maxConcurrent": 16,
126
+
127
+ // 单条结果上限(1024-1048576);超了全文落 artifact,消息里留截断的头和路径
128
+ "maxResultChars": 16384,
129
+
130
+ // 合并窗口,毫秒(0-10000);窗口内的结果并成一条,0 = 逐条立即投递
131
+ "aggregateWindowMs": 500,
132
+
133
+ // 父会话以这些原因退出时关掉子 tab:new / resume / fork / quit / reload;空 = 从不关
134
+ "closeChildrenOn": [],
135
+
136
+ // 子侧 inbox 轮询间隔,毫秒(50-5000)
137
+ "inboxPollMs": 250,
138
+
139
+ // 父侧 agent list 轮询间隔,毫秒(100-60000);调大省进程,调小更早发现收工
140
+ "pollIntervalMs": 1000,
141
+
142
+ // 覆盖工具文案。省略的键用内置文案;下面的值是占位符,不用就整段删掉
143
+ "prompts": {
144
+ "description": "一句话说明这个工具做什么",
145
+ "snippet": "Available tools 里的一句话",
146
+ // 保留含 {{profiles}} 的行才会保留 profile 列表
147
+ "guidelines": ["不要在本轮等待结果。", "可用 profile:\n{{profiles}}"]
148
+ },
149
+
150
+ // 把 pi 的对话框事件转成 herdr:blocked,供原生通知使用
151
+ "bridge": {
152
+ "enabled": true,
153
+ // 哪些对话框算 blocked
154
+ "blockedOn": ["custom", "select", "confirm", "input", "editor"]
155
+ }
156
+ }
157
+ ```
158
+
159
+ 超范围的数字会被夹到边界,不报错。配置只读一次,改完重启 pi。
160
+
161
+ ## 参考项目
162
+
163
+ 设计参考了这些项目,感谢作者。
164
+
165
+ | 项目 | 借鉴内容 |
166
+ |---|---|
167
+ | [earendil-works/pi](https://github.com/earendil-works/pi) | 扩展 API、包布局、官方示例 |
168
+ | [fyang93/pi-herdr-subagents](https://github.com/fyang93/pi-herdr-subagents) | profile 字段形状;隔离 herdr session 的集成测试 |
169
+ | [modem-dev/pi-herdr-subagents](https://github.com/modem-dev/pi-herdr-subagents) | herdr 启动与生命周期;终态归类 |
170
+ | [purboo/pi-durable-subagents](https://github.com/purboo/pi-durable-subagents) | 异步投递、幂等、只上报一次 |
171
+
172
+ ## 文档
173
+
174
+ - [ARCHITECTURE](docs/ARCHITECTURE.md) — 模块边界与 seam,改结构前看
175
+ - [GLOSSARY](docs/GLOSSARY.md) — 术语唯一来源,命名前看
176
+ - [模块](docs/modules/) — 各模块的用法与取舍
177
+
178
+ ## 开发
179
+
180
+ ```bash
181
+ pnpm install # 装依赖
182
+ pnpm check # lint + 导入边界 + 类型 + 单测
183
+ pnpm test # 单测(vitest)
184
+ pnpm lint # biome
185
+ pnpm typecheck # tsc --noEmit
186
+ pnpm check:imports # 依赖方向,按 docs/ARCHITECTURE.md 校验
187
+ pnpm test:e2e # 真 herdr + 真 pi 子进程,不在 pnpm check 里
188
+ ```
189
+
190
+ 跑一次 `git config core.hooksPath .githooks`,之后每次提交都会跑 `pnpm check`。
191
+
192
+ E2E 自己起隔离的 herdr session 与 agent 目录,缺 herdr、pi 或 `auth.json` 时整组跳过并给出原因。`PI_E2E_MODEL` 换模型;`PI_E2E_KEEP=1` 保留临时目录。
193
+
194
+ ## 许可
195
+
196
+ MIT,见 [LICENSE](LICENSE)。
package/package.json CHANGED
@@ -1,6 +1,43 @@
1
1
  {
2
2
  "name": "pi-herdr-subagents-lite",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Run pi subagents in herdr panes.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "setwhite",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/setwhite/pi-herdr-subagents-lite.git"
11
+ },
12
+ "keywords": ["pi-package", "pi-extension", "herdr", "subagent"],
13
+ "files": ["src"],
14
+ "packageManager": "pnpm@12.3.4",
15
+ "engines": {
16
+ "node": ">=22.19.0"
17
+ },
18
+ "pi": {
19
+ "extensions": [
20
+ "./src/index.ts"
21
+ ]
22
+ },
23
+ "scripts": {
24
+ "lint": "biome check --error-on-warnings .",
25
+ "check:imports": "node scripts/check-imports.mjs",
26
+ "typecheck": "tsc --noEmit",
27
+ "test": "vitest run",
28
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
29
+ "check": "pnpm lint && pnpm check:imports && pnpm typecheck && pnpm test"
30
+ },
31
+ "peerDependencies": {
32
+ "@earendil-works/pi-coding-agent": "*",
33
+ "typebox": "*"
34
+ },
35
+ "devDependencies": {
36
+ "@biomejs/biome": "^2.6.2",
37
+ "@earendil-works/pi-coding-agent": "^1.4.0",
38
+ "@types/node": "^26.6.5",
39
+ "typebox": "^1.3.27",
40
+ "typescript": "^7.0.2",
41
+ "vitest": "^5.0.5"
42
+ }
43
+ }
@@ -0,0 +1,24 @@
1
+ import type { BlockedKind, Config } from "../config/index.ts";
2
+
3
+ interface BridgePi {
4
+ on(event: "ui_prompt_start" | "ui_prompt_end", handler: (event: { kind: BlockedKind; title?: string }) => void): void;
5
+ events: { emit(channel: "herdr:blocked", data: { active: boolean; label?: string }): void };
6
+ }
7
+
8
+ export function createBridge(pi: BridgePi, config: Config["bridge"]): void {
9
+ if (!config.enabled) {
10
+ return;
11
+ }
12
+ pi.on("ui_prompt_start", (event) => {
13
+ if (!config.blockedOn.includes(event.kind)) {
14
+ return;
15
+ }
16
+ pi.events.emit("herdr:blocked", { active: true, label: event.title ?? event.kind });
17
+ });
18
+ pi.on("ui_prompt_end", (event) => {
19
+ if (!config.blockedOn.includes(event.kind)) {
20
+ return;
21
+ }
22
+ pi.events.emit("herdr:blocked", { active: false });
23
+ });
24
+ }
@@ -0,0 +1,122 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ BLOCKED_KINDS,
5
+ BRIDGE_KEYS,
6
+ boolean,
7
+ CONFIG_KEYS,
8
+ enumList,
9
+ integer,
10
+ isRaw,
11
+ nonEmptyString,
12
+ object,
13
+ optionalString,
14
+ PROMPT_KEYS,
15
+ type Raw,
16
+ type Scope,
17
+ SHUTDOWN_REASONS,
18
+ stringArray,
19
+ toolName,
20
+ unknownKeys,
21
+ } from "./schema.ts";
22
+
23
+ export { isRaw, type Raw } from "./schema.ts";
24
+
25
+ export type BlockedKind = (typeof BLOCKED_KINDS)[number];
26
+ export type ShutdownReason = (typeof SHUTDOWN_REASONS)[number];
27
+
28
+ interface PromptOverrides {
29
+ description?: string;
30
+ snippet?: string;
31
+ guidelines?: string[];
32
+ }
33
+
34
+ export interface Config {
35
+ enabled: boolean;
36
+ toolName: string;
37
+ herdrBin: string;
38
+ maxConcurrent: number;
39
+ maxResultChars: number;
40
+ aggregateWindowMs: number;
41
+ closeChildrenOn: ShutdownReason[];
42
+ inboxPollMs: number;
43
+ pollIntervalMs: number;
44
+ prompts: PromptOverrides;
45
+ bridge: { enabled: boolean; blockedOn: BlockedKind[] };
46
+ }
47
+
48
+ function promptOverrides(scope: Scope): PromptOverrides {
49
+ const prompts = object(scope, "prompts", PROMPT_KEYS);
50
+ const overrides: PromptOverrides = {};
51
+ for (const key of ["description", "snippet"] as const) {
52
+ const line = optionalString(prompts, key);
53
+ if (line !== undefined) {
54
+ overrides[key] = line;
55
+ }
56
+ }
57
+ const guidelines = stringArray(prompts, "guidelines");
58
+ if (guidelines !== undefined) {
59
+ overrides.guidelines = guidelines;
60
+ }
61
+ return overrides;
62
+ }
63
+
64
+ function parse(raw: Raw, file: string): Config {
65
+ const problems: string[] = [];
66
+ const top: Scope = { raw, prefix: "", problems };
67
+ unknownKeys(top, CONFIG_KEYS);
68
+ const bridge = object(top, "bridge", BRIDGE_KEYS);
69
+
70
+ const config: Config = {
71
+ enabled: boolean(top, "enabled", true),
72
+ toolName: toolName(top),
73
+ herdrBin: nonEmptyString(top, "herdrBin", "herdr"),
74
+ maxConcurrent: integer(top, "maxConcurrent", 16, 1, Number.POSITIVE_INFINITY),
75
+ maxResultChars: integer(top, "maxResultChars", 16384, 1024, 1048576),
76
+ aggregateWindowMs: integer(top, "aggregateWindowMs", 500, 0, 10000),
77
+ closeChildrenOn: enumList(top, "closeChildrenOn", SHUTDOWN_REASONS, []),
78
+ inboxPollMs: integer(top, "inboxPollMs", 250, 50, 5000),
79
+ pollIntervalMs: integer(top, "pollIntervalMs", 1000, 100, 60000),
80
+ prompts: promptOverrides(top),
81
+ bridge: {
82
+ enabled: boolean(bridge, "enabled", true),
83
+ blockedOn: enumList(bridge, "blockedOn", BLOCKED_KINDS, BLOCKED_KINDS),
84
+ },
85
+ };
86
+
87
+ if (problems.length > 0) {
88
+ throw new Error(`invalid config ${file}: ${problems.join("; ")}`);
89
+ }
90
+ return config;
91
+ }
92
+
93
+ function readRaw(file: string): Raw {
94
+ let text: string;
95
+ try {
96
+ text = readFileSync(file, "utf8");
97
+ } catch (error) {
98
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") {
99
+ return {};
100
+ }
101
+ throw error;
102
+ }
103
+ let parsed: unknown;
104
+ try {
105
+ parsed = JSON.parse(text);
106
+ } catch {
107
+ throw new Error(`invalid config ${file}: not valid JSON`);
108
+ }
109
+ if (!isRaw(parsed)) {
110
+ throw new Error(`invalid config ${file}: expected a JSON object`);
111
+ }
112
+ return parsed;
113
+ }
114
+
115
+ export function dataDir(agentDir: string): string {
116
+ return join(agentDir, "pi-herdr-subagents-lite");
117
+ }
118
+
119
+ export function loadConfig(agentDir: string): Config {
120
+ const file = join(dataDir(agentDir), "config.json");
121
+ return parse(readRaw(file), file);
122
+ }