# @muzi3/herdr

[Herdr](https://herdr.dev/) 相关的官方 guide skill、pane/agent 控制工具与本地补充扩展打包为一个 pi package。

| 资源 | 类型 | 说明 |
|------|------|------|
| `herdr_layout` / `herdr_pane` / `herdr_agent` | 扩展（fork） | pane/agent 控制工具：工作区 · tab · pane 拓扑，pane 的命令下发/读取/等输出/收发按键，以及 coding agent 的启动、prompt、等待与读取 |
| `herdr-guide` | user-invoked 向导 | 引导用户理解、安装、配置与排查 Herdr（`herdr.dev/agent-guide.md`） |
| `herdr-agent-state` | 扩展 | 通过 Herdr 的 pane socket 上报 agent 状态（working / blocked / idle）及会话引用，供 Herdr 面板展示；并把 pi 的阻塞式交互弹窗（`ui_prompt_start` / `ui_prompt_end`，含 `ask_user_question`）转发为 herdr 的 `herdr:blocked` 事件，使 pane 变为 `blocked`（label 带问题摘要）；桌面通知交给 herdr 自己发 |
| `pi-hot-restart` | 扩展 | 提供 `/pi-hot-restart` 命令：批量重启当前 herdr 里的所有 pi 窗口（跳过 working），并恢复到原 session |

三个扩展**互相独立**：每个扩展独占 `extensions/` 下的一个目录、以 `index.ts` 为入口，互不 import，`package.json` 的 `pi.extensions` 逐条列出入口。一个扩展注册失败不会连带拖垮另外两个，日后给某个扩展加内部模块也不会影响另外两个。

### 扩展结构

```
extensions/
├── herdr-agent-state/index.ts    agent 状态 + blocked 状态上报
├── pane-agent-tools/index.ts     herdr_layout / herdr_pane / herdr_agent
└── pi-hot-restart/index.ts       /pi-hot-restart
```

| 入口 | 内容 | 加载门槛 |
|------|------|----------|
| `herdr-agent-state/index.ts` | agent 状态 + blocked 状态上报（herdr 官方集成正文 + 本包并入的 blocked 段落） | `HERDR_ENV=1` 且 `HERDR_SOCKET_PATH` / `HERDR_PANE_ID` 非空 |
| `pi-hot-restart/index.ts` | `/pi-hot-restart` 命令（本包自研补充） | 无，走 PATH 调 herdr CLI |
| `pane-agent-tools/index.ts` | `herdr_layout` / `herdr_pane` / `herdr_agent`（本地 fork） | `HERDR_ENV=1` 且 `HERDR_PANE_ID` 非空 |

门槛由各扩展自己判断（没有统一 gate），所以在 herdr 之外 `/pi-hot-restart` 仍可用（它只控制本机进程，不需要 pane socket），依赖 pane 的两个扩展则静默跳过。

每个扩展目录的入口都叫 `index.ts`，与 pi 自己的目录式扩展约定一致：扩展目录内可以再放内部模块，`pi.extensions` 只指向入口，不会误加载旁支文件。

`herdr-agent-state/index.ts` 的目录名与 herdr 集成注入的文件同名，对齐上游时可以直接对比。内容分两段：官方集成正文（一直到文件末尾之前）与之后的「本包新增」段落。本包对官方正文只有**一处**改动：官方 `export default` 开头插入一行 `registerBlockedReporting(pi);` —— 该 default export 就是本扩展的唯一入口，blocked 上报只能挂在那里。重新同步上游时（`HERDR_INTEGRATION_VERSION` 从 8 往上升）重取注入文件替换官方正文并补回那一行；`diff` 应只有三处新增、零删除，漏帖那一行会被 `tests/herdr-agent-state.test.ts` 拦下。

## 来源与许可

两者内容均来自 Herdr 官方，上游仓库为 [herdrdev/herdr](https://github.com/herdrdev/herdr)：

- `herdr-guide` 引导 skill —— 官方 [`herdr.dev/agent-guide.md`](https://herdr.dev/agent-guide.md)，教 agent 引导用户理解/配置/排障 Herdr
- `herdr-agent-state` 扩展 —— Herdr 官方 `pi` 集成的 bundled 扩展，本包保留其正文，只做一处改动（在官方 `export default` 开头插入一行 blocked 上报），其余新增都在文件末尾的段落里。`herdr integration install pi` 会把官方版本写入 `~/.pi/agent/extensions/herdr-agent-state.ts`（若设置 `PI_CODING_AGENT_DIR` 则写入 `$PI_CODING_AGENT_DIR/extensions/`）

Herdr 以 [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) 许可发布，本包保留其许可声明，详见 `LICENSE`。

> ⚠️ 本包这份与集成写入的那份**目录名相同、位置不同**（本包 `extensions/herdr-agent-state/index.ts`，集成写 `~/.pi/agent/extensions/herdr-agent-state.ts`）。若已执行过 `herdr integration install pi`，agent 目录里的副本会被 pi 自动加载，与本包同时上报状态（herdr 的 `blockedCount` 会短暂变成 2，然后一起归零）。本包已自带该功能，用本包时不必安装集成；已安装的话请删掉 `~/.pi/agent/extensions/herdr-agent-state.ts`。
>
> 无论装不装集成，agent 目录里那份都由集成管理，重装或更新集成会覆盖它；自定义 hooks 请写成旁边的独立文件而非编辑它（本包这份是 vendored 副本，由本包自己维护）。

`extensions/pane-agent-tools/index.ts`（`herdr_layout` / `herdr_pane` / `herdr_agent`）是**本地 fork**，来源与许可与上面不同：

- 上游：[`@ogulcancelik/pi-herdr`](https://github.com/ogulcancelik/pi-extensions/tree/main/packages/pi-herdr) 0.4.0（[仓库](https://github.com/ogulcancelik/pi-extensions)，MIT，Copyright (c) 2026 Can Celik），许可全文见本包 `LICENSE.MIT`
- 因此本包整体按 `Apache-2.0 AND MIT` 双许可表述：`LICENSE`（Apache-2.0）覆盖 Herdr 官方内容与 `pi-hot-restart.ts`，`LICENSE.MIT` 覆盖 fork 进来的 `pane-agent-tools.ts`

### 相对上游的改动

上游 0.4.0 把 `pane run` / `pane send-text` / `pane send-keys` 交给 `execHerdrJson`，而它额外要求 herdr CLI 输出 JSON envelope。但 herdr 0.9.0 上这三条命令**成功时退出码为 0 且 stdout 为空**，于是每一次写操作都误报 `Expected JSON output from herdr ...` —— 即使命令早已成功下发到目标 pane。实测 14 天内 156 个 pi session 日志，误报次数为 `send-keys` 964、`send-text` 923、`run` 438。

这三处改用 `execHerdr`：它只校验退出码，并把 stderr/stdout 上的结构化 herdr 错误经 `parseHerdrError` 抛出，因此真实失败照旧报错，只去掉了那个不成立的「stdout 必须非空」约束。`pane close` / `pane wait-output` / `agent send-keys` 确实返回 envelope，保持 `execHerdrJson`（`close` 的边界由回归测试锁定）。

上游尚未修复，相关讨论：[issue #22](https://github.com/ogulcancelik/pi-extensions/issues/22)、[issue #40](https://github.com/ogulcancelik/pi-extensions/issues/40)、[PR #27](https://github.com/ogulcancelik/pi-extensions/pull/27)（仅覆盖 `run`，未合并）。本包已不依赖 `@ogulcancelik/pi-herdr`：同时安装两者会重复注册同名工具，需先卸载上游包。

## 安装

```bash
pi install npm:@muzi3/herdr
```

## 用法

- 用户询问 Herdr 是什么、怎么安装/配置/使用，或排查 agent 未识别、快捷键失效等问题时，触发 `herdr-guide` 引导（`/herdr-guide`）
- 触发词：`herdr` / `herdr 安装` / `herdr 配置` / `终端工作区` / `agent 状态检测` 等

### pane / agent 控制工具

三个工具直接驱动 herdr CLI，需在 herdr 集成环境（`HERDR_ENV=1` + `HERDR_PANE_ID`）中加载：

- `herdr_layout` —— 工作区 / tab / pane 的创建、列举与拓扑查询（`current` / `workspace_list` / `pane_split` / `pane_layout` 等）
- `herdr_pane` —— 普通进程的 pane 控制：`run` 下发命令、`read` 读回输出、`wait_output` 等匹配输出、`send_text` / `send_keys` 收发按键、`close` 关闭（拒绝关闭 pi 自身所在 pane）
- `herdr_agent` —— 已识别 coding agent 的 pane：`start` / `prompt` / `wait` / `read` / `send_keys` / `rename` 等

### 批量重启 pi 窗口

在任意 pi 窗口执行 `/pi-hot-restart`，会：

1. 列出当前 herdr 里所有 `pi` 窗口；
2. 跳过 `agent_status === "working"` 的窗口（含当前发起命令的窗口本身）；
3. 弹出确认框，展示将重启的窗口清单与数量；
4. 对每个目标窗口：向 pi 前台进程组发 SIGTERM 优雅退出（session 落盘），等待退出后用 `pi --session <path|id>` 在原 pane 恢复。

### 等用户输入时上报 blocked

herdr 的 pi 集成只认 `herdr:blocked` 事件，且 herdr 0.9.0 对 pi 仍没有终端 blocked 识别规则（内置规则只有一条 `working_literal`，匹配 `Working...`），所以 `ask_user_question` / confirm 弹窗期间 pane 会一直停在 `working`。本扩展的 blocked 上报段落订阅 pi core 的 `ui_prompt_start` / `ui_prompt_end`（`select`/`confirm`/`input`/`editor`/`custom` 统一包装，只在外层触发）：

- 等待期发 `herdr:blocked { active: true, label }`，结束后发 `{ active: false }`；label 形如 `等待回答：<问题 header>`（`custom` 无 title，取 `rpiv:ask-user:prompt` 的问题内容）、`等待确认：<title>`。
- 契约同 pi-subagents 的 herdr-status bridge：herdr 的 `blockedCount` 是计数式的，切 label 前先发 `active:false` 再发 `active:true`；本扩展严格配对，只占一个计数。

桌面通知交给 herdr 自己发（`~/.config/herdr/config.toml` 设 `[ui.toast] delivery = "terminal"`）：通知经终端转交给 Ghostty 显示，**点击横幅会把 Ghostty 窗口调到前面**。注意它只能到窗口粒度 —— herdr 的 pane 是 herdr TUI 内部的概念，点通知不会切到对应 pane；正文也固定为 `<工作区名> · <工作区编号> · <tab 名>`，不含 pane id 与问题原文。早期版本试过自建横幅（`osascript` / `terminal-notifier`），内容能带 pane 与问题原文，但要么不支持点击动作、要么在本机通知权限不稳；点击跳转要能捕获回调的通知器（如 alerter）才可能做到，为避免与 herdr 自己的通知重复弹两份，自建横幅已整体移除。

### 扩展环境变量

扩展需在 Herdr 集成环境中运行，依赖以下环境变量：

| 变量 | 说明 |
|------|------|
| `HERDR_ENV` | 为 `1` 时启用扩展 |
| `HERDR_SOCKET_PATH` | Herdr pane socket 路径 |
| `HERDR_PANE_ID` | 当前 pane id（同时作为扩展的运行门槛） |

未设置时 `herdr-agent-state` 与 `pane-agent-tools` 两个扩展静默跳过（门槛见「扩展结构」），`/pi-hot-restart` 仍可用：它通过 PATH 调用 `herdr` CLI（`agent list` / `pane process-info` / `pane run`），不依赖上述变量。

## 测试

```bash
node --test tests/    # 等价于 npm test
```

`tests/` 下三个文件、共 28 个用例，`node:test` + `node:assert/strict`，无需引入 bun（Node 原生剥离 TS 类型）：

`tests/pane-agent-tools.test.ts` —— 由上游 `index.test.ts`（`bun:test`）改写。除移植的 9 个用例外，额外覆盖本 fork 的 3 行改动：

- `pane run` / `send_text` / `send_keys` 在 stdout 为空时成功，且恰好调用一次 herdr
- stdout 为 banner/ANSI 等非 JSON 内容时不当作协议数据解析
- 结构化 herdr 错误（stderr envelope）与裸非零退出仍然报错，不会被「只校验退出码」吞掉
- `pane close` 仍走 JSON envelope 路径

这组用例对上游写法运行会有 4 个失败、对当前实现 16 个全通过，可用于确认回归网没有失效。

`tests/herdr-agent-state.test.ts` —— 起一个真实 unix socket 充当 pane socket，验证 agent 状态（working / idle）与 blocked 上报（含 label 取值、弹窗结束后的回落）真的落到连接上。用真 socket 而非桩：上报的成败判据本身就在连接上（`sendRequestAttempt` 只有连上并收到对端数据才算送达），桩掉 `net` 等于把被测逻辑一起桩掉。这一组只调 `default export`（扩展唯一入口），所以官方正文里那行 `registerBlockedReporting(pi);` 被漏帖时会直接失败。

`tests/extensions.test.ts` —— 把「三个扩展各自独立」锁成断言：`pi.extensions` 与 `extensions/` 下的扩展目录一一对应（不漏声明、也不留死路径）、每个扩展目录都有 `index.ts` 入口且顶层没有散落的扩展文件、扩展之间没有跨目录 import，并且单独加载每个扩展时只会注册自己的资源（`herdr-agent-state` 只挂状态监听，`pi-hot-restart` 只有 `/pi-hot-restart`，`pane-agent-tools` 只有三个工具）。

## 参考

- 官方文档：<https://herdr.dev/docs/>
- 引导 guide 原文：<https://herdr.dev/agent-guide.md>
- 集成文档（Pi 章节）：<https://herdr.dev/docs/integrations/>
- 上游仓库：<https://github.com/herdrdev/herdr>
- pi-herdr 上游仓库：<https://github.com/ogulcancelik/pi-extensions/tree/main/packages/pi-herdr>
