@muzi3/dsh-plugin-herdr-tools
v0.2.4
Published
DSH plugin: herdr_layout / herdr_pane / herdr_agent tools — inspect and create Herdr terminal topology, drive ordinary commands in panes, and control coding agents. Port of @muzi3/herdr's pane-agent-tools.
Maintainers
Readme
@muzi3/dsh-plugin-herdr-tools
DSH 插件:把 Herdr 的 pane / agent 操作做成注册式工具。默认挂载是四个
herdr_overview、herdr_layout、herdr_pane、herdr_agent;挂载时传
config.manager: true 则换成八个 manager 动词 —— 与一个手工版参考实现(薄封装 herdr CLI 的 manager 插件,本包是它的产品化版本)
逐名逐参一致,便于两套实现做消融对比。见「两种工具面」。
herdr_layout / herdr_pane / herdr_agent 移植自 pi 包 @muzi3/herdr 的
pane-agent-tools 扩展(该扩展 fork 自 @ogulcancelik/pi-herdr 0.4.0,MIT,
Copyright (c) 2026 Can Celik);herdr_overview 与 agent prompt 的
agent_not_ready 回退是 DSH 侧新增,pi 版没有(见「与 pi 版的差异」)。
为什么需要它
DSH 侧原本只有 herdr-ops 这类 skill 文档:模型每次操作 herdr 都得自己把 CLI
命令拼出来,还要每次重新记住这些坑。文档兜不住这些错误:
pane run成功时 exit 0、stdout 为空,看着像失败,容易被误报;--timeout是 herdr 自带的参数 —— 不要指望 shell 里一定有timeout/gtimeout;wait-output的超时表示「没观测到」,不是命令失败;- 命中行可能是命令回显,而回显在命令跑完之前就已经存在了。
herdr_pane / herdr_agent 把这些判据固化成代码,参数交给 schema 校验;
herdr_overview 再把「一次看清全局」也做成一次调用。
两种工具面:默认 4 个 / manager 8 个
同一个包提供两套互斥的工具面,由挂载时的 config 选:
| 模式 | 开启方式 | 注册的工具 |
|------|----------|-----------|
| 默认 | 不传 config | 恰好 4 个:herdr_overview / herdr_layout / herdr_pane / herdr_agent |
| manager | config.manager: true(env 兜底 DSH_HERDR_TOOLS_MANAGER=1) | 恰好 8 个动词:herdr_overview / herdr_read / herdr_wait / herdr_prompt / herdr_run / herdr_create / herdr_agent_start / herdr_close |
两套不共存:manager 模式下不注册那 4 个通用工具。否则工具面就不是与手工版 1:1,消融实验会比出「模型选错工具」的噪声,而不是两套实现的行为差异。
怎么开:config 是主开关,env 是兜底
Cordis 调用插件的形式是 callback(ctx, config),loader 条目里的 config: 块会作为
apply 的第二个参数传进来。所以:
- insert:
- id: herdr-tools
name: '@muzi3/dsh-plugin-herdr-tools'
config:
manager: trueconfig.manager 是主开关;DSH_HERDR_TOOLS_MANAGER=1 是等价的 env 兜底,两者都认、
config.manager === true 优先。用 env 的好处是不必改 loader 条目 —— 想在同一份配置里
把两套工具面做成可切换的消融开关时更省事。
manager 模式同样只在 herdr 里注册(HERDR_ENV=1 且有 HERDR_PANE_ID),理由
与默认面相同。手工版没有这道门 —— 它在 herdr 外也注册 8 个、靠脚本 exit 3 拒绝;
见下面的「已知差异」。
与手工版的参数对应
| 动词 | 参数(与手工版逐字一致) | 底层 herdr 调用 |
|------|--------------------------|-----------------|
| herdr_overview | limit / workspace | pane current --current + workspace list + agent list |
| herdr_read | target / scope / source / lines / mode / tail | agent read / pane read(agent 视图失败回退 pane,via 说明) |
| herdr_wait | target / until / timeoutMs | agent wait --until --timeout |
| herdr_prompt | target / prompt / wait / force | agent prompt →(agent_not_ready)pane run |
| herdr_run | pane / command | pane run |
| herdr_create | kind / workspace / label / cwd / direction / focus | workspace create / tab create / pane split |
| herdr_agent_start | name / kind / pane / timeoutMs | agent start |
| herdr_close | pane / dryRun | pane get(预览)/ pane close |
要点:是 timeoutMs 不是 timeout;herdr_agent_start.kind 是自由字符串(没有
enum,手工版也没有);herdr_create.direction 缺省时从 pane 几何判断;
herdr_close.dryRun 默认 true(只预览)。统一信封是
{ ok, exitCode, timedOut?, output, note?, via? }。
manager 模式是 DSH 侧新增、pi 版没有:pi 扩展只有拓扑原语,没有这套动词面。
已知差异(manager 模式 vs 手工版)
判据与安全边界对齐,措辞与机制上有这些差异:
- 底层实现不同。手工版
execFile python3 <skill>/scripts/herdr,把 skill 脚本当 第二份真值(带 preflight 门禁);本包直接调herdrCLI,复用本包的execHerdr/execHerdrJson/execHerdrText,不依赖那个脚本。因此output是本包自己的 文本渲染(列表 / 摘要 / 读屏),不是脚本的归一化输出 —— 内容等价、措辞有出入。 exitCode取值域。手工版沿用脚本的0/1/2/3/-1;本包只会产生0/1/2/-1, 没有 3(不在 herdr 里)—— 因为本包在 herdr 外一个工具都不注册,走不到那一步。- herdr 外注册数:手工版始终注册 8 个(调用时 exit 3);本包注册 0 个。
herdr_overview的workspace真的生效。手工版把它透传给脚本,而脚本的layout workspace-list忽略它、agent list也没接 —— 参数等于摆设。本包按workspace_id客户端过滤拓扑段与 agent 段(见下文「herdr_overview的workspace语义」)。这是有意的差异。- 列表截断文案。本包复用默认面那份
capList:[Showing X of Y agents — raise with limit];手工版是... — raise with --limit N]。纯措辞。 herdr_prompt的解析兜底更强。手工版只用一次agent get解析 pane id,解析 不到就拒绝回退;本包复用resolveAgentPaneId,agent get失败后还会扫一遍agent list。更少拒绝、不会更差;解析不出来同样明确报错、绝不猜。- 回退后的成功文案。手工版回退走脚本,
output是Submitted command to pane X; 本包是Submitted prompt to pane X。两者都在via里写明pane run <pane>(agent prompt 不可用:对方是自报 agent,非 herdr 命名 agent)。 - 校验文案。
herdr_agent_start的名字/超时校验、herdr_close的自 pane 拒绝, 手工版来自脚本(中文提示 + 示例),本包是等义短句。判据与exitCode=2一致。 - 本地超时的
output。手工版本地超时会带上已读到的部分 stdout;本包走execHerdr的中止路径拿不到部分输出,output为空、note说明「未观测到 ≠ 失败」、timedOut: true。herdr_wait的 herdr 侧超时两套一致:ok:false+note,timedOut不置位(那是 herdr 的等待上限,不是本地超时)。 herdr_read回退时的via文案:手工版嵌脚本首行错误,本包嵌 herdr 错误 消息首行。结构一致、措辞不同。- 参数描述文案:本包在个别参数上把描述写得更准(如
herdr_create.workspace注明 kind=workspace/pane 时忽略),参数名、默认值与行为不变。
默认工具面:四个工具
| 工具 | 用途 |
|------|------|
| herdr_overview | 舰队快照(只读):调用者自己的位置 + workspace 拓扑 + agent 列表(名称/状态/cwd/pane id)。管别人的第一步 |
| herdr_layout | 查看/创建 workspace、tab、pane 拓扑(current / *_list / *_create / *_focus / pane_layout / pane_split) |
| herdr_pane | 操作裸终端 pane:get / run / read / wait_output / send_text / send_keys / close |
| herdr_agent | 操作 pane 里被识别出的 coding agent:list / get / start / prompt / wait / read / send_keys / focus / rename |
herdr_overview 的 workspace 语义(本包的选择)
workspace 同时约束两段:workspace 拓扑段 和 agent 列表段。给了它就是
「这个 workspace 的视图」,不传才是全机快照。调用者自身的位置段永远不受它约束。
为什么这样选:手工版的参数描述说「只看某个 workspace 的拓扑」,但 agent list 仍
全机返回,与描述不符(其脚本层实际上还把 --workspace 丢掉了,参数等于摆设)。
两条路里我们选了「让 agent list 也按 workspace 过滤」—— 因为 herdr agent list
虽然没有 --workspace 选项,但每条 agent 记录都自带 workspace_id,按它做
客户端精确过滤即可,不需要额外请求、也不靠 pane 反查,语义与描述终于一致。
指定了不存在的 workspace 时,拓扑段和 agent 段都为空,并附一行提示。
只在 herdr 里注册:apply() 检查 HERDR_ENV=1 且有 HERDR_PANE_ID,
不成立就注册零个工具 —— 避免在非 herdr 环境暴露四个必然失败的工具。
与 pi 版的差异
| 项 | pi | DSH |
|---|---|---|
| 参数 schema | TypeBox | DSH 的 ParameterSchemaSpec({type, enum, required}) |
| 输出 | 内联 content blocks | 必填 output.schema + render |
| 执行 | pi.exec | ctx.shell.resolve/run |
| 结果渲染 | renderCall/renderResult | 刻意不实现(见下) |
| herdr_overview | 无此工具 | DSH 新增:一次调用拿到位置 + 拓扑 + agent 列表 |
| agent prompt 的 agent_not_ready 回退 | 无(pi 版直接报错) | DSH 新增:回退成 pane run 键入 |
这两项是 DSH 侧新增,pi 版没有:pi 扩展只暴露拓扑原语,而驱动一支 agent 舰队还需要「一次看清全局」和「够得着 herdr 不代它输入的 agent」这两件事。
两处因 DSH 的 spec 没有 pattern / minimum 关键字,约束移进了函数体:
- agent 名必须匹配
^[a-z][a-z0-9_-]{0,31}$; agent start的 timeout 必须> 3000且<= 300000。
模型可见的文案与 pi 版逐字一致,因为 render 完整保留了原措辞。
presentCall/presentResult 没做:pi 的渲染器画的是 pi 自己的主题,照搬只会
引入第二份真值,而模型侧输出已经完全一致。
保留的加固
不要求 JSON envelope 的写操作:
pane run/send-text/send-keys成功时 stdout 为空。若强制解析 JSON,会在命令已经下发到目标 pane 的情况下误报 "Expected JSON output"。close/wait-output/agent send-keys确实返回 envelope,照常解析。timeout ≠ 失败:
wait_output超时返回matched:false加 pane 尾部输出;agent prompt遇到agent_prompt_stalled返回「已下发(submission accepted), 但 5s 内未观测到 working/blocked」,并明确提示不要重发。正则元字符自动升级:
match含| ( ) [ ] { } ^ $ * + ? \且未显式传regex时按正则处理 —— 字面量语义下a|b永远不可能命中。echoedInput 判据:记录本进程最近一次下发给某 pane 的文本;命中行包含它时 提示这是回显行、命令可能还没跑完。
拒绝关闭自己所在的 pane:
close先比对pane current,相同则报错。agent_not_ready回退到pane run:绝大多数被管理的 agent 是 dsh-tui 这类 自报 agent,不在 herdr 的命名 agent manifest 里,agent prompt会对它们 返回agent_not_ready(is not an active named agent)。命中该错误码时自动 回退成pane run <完整 pane id> <prompt>(文本 + Enter 直接键入)。- 回退前必须先解析 pane id:
pane run只认完整限定 pane id,而target可能是 agent 名。先agent get <target>取.result.agent.pane_id,失败再扫agent list按 pane id / 名字匹配;解析不出来就明确报错,绝不猜一个 pane 去键入(键错终端比失败更糟)。 - 只对错误码
agent_not_ready回退,其它错误照旧上抛。 - 输出里三条路可区分:语义路径
Prompt settled/submitted: ...;回退路径Prompt 已回退为 pane run 键入 <pane> ...;stalled 路径submission accepted ... 不要再下发一次。回退路径同样提示「已键入 ≠ 已开始 干活」,并把该 pane 记进 echoedInput 判据。
- 回退前必须先解析 pane id:
安装与挂载
npm install @muzi3/dsh-plugin-herdr-tools在 profile 的 cordis.patch.yml 里插入一行:
- insert:
- id: herdr-tools
name: '@muzi3/dsh-plugin-herdr-tools'
inject: []只 inject 两个 host 注册表(tools、shell),不发布服务,因此挂在 profile 层
或 agent preset 里都可以。
注意
- pane id 必须完整限定:是
w1B:pF这种,不是侧边栏短名pF;传短名会报pane_not_found。工具从.result.pane.pane_id(agent 则是.result.agent.pane_id)取,一定是完整的 ——agent_not_ready回退也走同一条 解析,解析不出来就报错,不会拿短名去pane run。 isConcurrencySafe一律返回false:工具携带自由文本参数 (command/text/keys/match/workspace),不该并发命中同一个 pane。herdr_overview其实是只读的,但它同样遵守这条统一契约,免得为它单开一份真值。- manager 模式沿用手工版的并发划分:只读的
herdr_overview/herdr_read标为可并发,其余六个(会往 pane 里键入)互斥。注意defineTool会先做参数校验, 参数不合 schema 时一律返回false。
开发
npm testtest/ 用 stub host 驱动 apply(),覆盖 schema 契约、参数对齐与错误归因分支,
不触碰真实的 herdr server。
许可
Apache-2.0 AND MIT:内容来自 Herdr(Apache-2.0)并 fork 了
@ogulcancelik/pi-herdr(MIT,见 LICENSE)。
