@muzi3/herdr 0.2.2 → 0.2.3

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 CHANGED
@@ -52,10 +52,19 @@ Herdr 以 [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) 许可发布
52
52
 
53
53
  ### 相对上游的改动
54
54
 
55
- 上游 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。
55
+ 上游 0.4.0 把 `pane run` / `pane send-text` / `pane send-keys` 交给 `execHerdrJson`,而它额外要求 herdr CLI 输出 JSON envelope。但 herdr 0.9.0 上这三条命令**成功时退出码为 0 且 stdout 为空**(0.9.5 复测同),于是每一次写操作都误报 `Expected JSON output from herdr ...` —— 即使命令早已成功下发到目标 pane。实测 14 天内 156 个 pi session 日志,误报次数为 `send-keys` 964、`send-text` 923、`run` 438。
56
56
 
57
57
  这三处改用 `execHerdr`:它只校验退出码,并把 stderr/stdout 上的结构化 herdr 错误经 `parseHerdrError` 抛出,因此真实失败照旧报错,只去掉了那个不成立的「stdout 必须非空」约束。`pane close` / `pane wait-output` / `agent send-keys` 确实返回 envelope,保持 `execHerdrJson`(`close` 的边界由回归测试锁定)。
58
58
 
59
+ ### 与 herdr 版本的对账(当前基线 0.9.5)
60
+
61
+ 工具面是照着 herdr CLI 写死的,herdr 升级后会有静默缺口。已固化的两处:
62
+
63
+ - `agent start --kind` 的枚举必须等于 `herdr agent start --help` 的 possible values(0.9.5 共 24 个)。0.2.2 漏了 `qwen` / `letta` / `muse`,这三个 kind 当时根本起不来;`tests/` 里有对账用例,加新 kind 时同步改。
64
+ - `agent_blocked` 必须与 `agent_prompt_stalled` 反向处理:目标卡在审批/提问弹窗时,herdr 在发送任何输入**之前**就拒绝 prompt,所以这不是「已下发」,重试也不会好 —— 工具会报错并指向 `herdr_agent get` / `read` 先看清阻塞界面。
65
+
66
+ 未覆盖、需要时直连 CLI 的命令族:`herdr worktree`、`api`、`notification`、`session`、`machine`、`agent attach`,以及 `pane focus/swap/move/zoom/resize/process-info/neighbors/edges/rename`。
67
+
59
68
  上游尚未修复,相关讨论:[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`:同时安装两者会重复注册同名工具,需先卸载上游包。
60
69
 
61
70
  ## 安装
@@ -88,7 +97,7 @@ pi install npm:@muzi3/herdr
88
97
 
89
98
  ### 等用户输入时上报 blocked
90
99
 
91
- 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` 统一包装,只在外层触发):
100
+ herdr 的 pi 集成只认 `herdr:blocked` 事件,且 herdr 0.9.0 对 pi 仍没有终端 blocked 识别规则(0.9.5 的 `herdr agent explain` 依旧显示 `rule: none`、`screen_detection_skip_reason: full_lifecycle_hook_authority`,内置规则只有一条 `working_literal`,匹配 `Working...`),所以 `ask_user_question` / confirm 弹窗期间 pane 会一直停在 `working`。本扩展的 blocked 上报段落订阅 pi core 的 `ui_prompt_start` / `ui_prompt_end`(`select`/`confirm`/`input`/`editor`/`custom` 统一包装,只在外层触发):
92
101
 
93
102
  - 等待期发 `herdr:blocked { active: true, label }`,结束后发 `{ active: false }`;label 形如 `等待回答:<问题 header>`(`custom` 无 title,取 `rpiv:ask-user:prompt` 的问题内容)、`等待确认:<title>`。
94
103
  - 契约同 pi-subagents 的 herdr-status bridge:herdr 的 `blockedCount` 是计数式的,切 label 前先发 `active:false` 再发 `active:true`;本扩展严格配对,只占一个计数。
@@ -5,7 +5,7 @@
5
5
  * 上游许可全文见同包根目录 `LICENSE.MIT`,来源与改动说明见 README。
6
6
  *
7
7
  * 相对上游的改动(3 行):`pane run` / `pane send-text` / `pane send-keys` 在
8
- * herdr 0.9.0 上成功时退出码为 0 且 stdout 为空。上游用 execHerdrJson 会额外要求
8
+ * herdr 0.9.0 上成功时退出码为 0 且 stdout 为空(0.9.5 复测同)。上游用 execHerdrJson 会额外要求
9
9
  * JSON envelope,于是每一次写操作都误报 "Expected JSON output from herdr ..."
10
10
  * —— 即使命令早已成功下发到目标 pane。
11
11
  *
@@ -23,6 +23,11 @@
23
23
  * 字面量语义下 `a|b` / `log \$` 永远不可能命中,只会白等满 timeout。
24
24
  * - 命中行包含本进程刚下发的命令文本时标 echoedInput —— 那多半是命令回显行,而不是
25
25
  * 命令产生的输出(回显在命令还没跑完时就已经存在)。
26
+ * - agent_blocked 单独识别:目标卡在审批/提问弹窗时 herdr 会在发送任何输入前就拒绝,
27
+ * 因此不能像 stalled 那样判为“已下发”,也不能靠重试绕过(详见 isPromptBlocked)。
28
+ *
29
+ * 维护:`agent start --kind` 的枚举必须与 `herdr agent start --help` 一致(0.9.5 共 24 个)。
30
+ * herdr 每加一个 agent 就多一个 kind,枚举写死就会让新 kind 起不来 —— tests/ 里有对账用例。
26
31
  */
27
32
 
28
33
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -145,7 +150,10 @@ const AgentKindEnum = StringEnum(
145
150
  "hermes",
146
151
  "kilo",
147
152
  "qodercli",
153
+ "qwen",
154
+ "letta",
148
155
  "maki",
156
+ "muse",
149
157
  ] as const,
150
158
  { description: "Supported coding agent kind and canonical executable" },
151
159
  );
@@ -189,7 +197,8 @@ function herdrErrorCode(error: unknown): string | undefined {
189
197
  * herdr 的 timeout / agent_prompt_stalled 表示“没观测到”,不是失败。
190
198
  * code 是主要判据,文案匹配作为兜底(个别版本可能不带结构化 code)。
191
199
  * stalled 的文案见过两种:旧版 `no observed state change ... state_change_seq remained N`,
192
- * 0.9.0 `no observed working or blocked state within 5000 ms; current status is idle`。
200
+ * 0.9.0 起 `no observed working or blocked state within 5000 ms; current status is idle`
201
+ * (0.9.5 二进制里仍是这段措辞)。
193
202
  */
194
203
  function isHerdrTimeout(error: unknown): boolean {
195
204
  return herdrErrorCode(error) === "timeout" || (error instanceof Error && /timed out waiting for/i.test(error.message));
@@ -202,6 +211,15 @@ function isPromptStalled(error: unknown): boolean {
202
211
  );
203
212
  }
204
213
 
214
+ /**
215
+ * 目标正卡在审批/提问弹窗时,herdr 在**发送任何输入之前**就拒绝 prompt,返回 agent_blocked
216
+ * (见 0.9.5 的 `herdr agent prompt --help` 与 api/agents.rs 的 validate_agent_prompt)。
217
+ * 这和 stalled 相反:它确定没下发,且不该自动重试 —— 必须先看阻塞的界面再由人决定怎么答。
218
+ */
219
+ function isPromptBlocked(error: unknown): boolean {
220
+ return herdrErrorCode(error) === "agent_blocked";
221
+ }
222
+
205
223
  function isAbortError(error: unknown, signal?: AbortSignal): boolean {
206
224
  return signal?.aborted === true || (error instanceof Error && error.message === "Aborted");
207
225
  }
@@ -603,7 +621,9 @@ export default function (pi: ExtensionAPI) {
603
621
  source: Type.Optional(ReadSourceEnum),
604
622
  lines: Type.Optional(Type.Integer({ minimum: 1, description: "Rendered terminal rows to read or search" })),
605
623
  format: Type.Optional(OutputFormatEnum),
606
- raw: Type.Optional(Type.Boolean({ description: "Keep ANSI escapes while matching wait_output" })),
624
+ raw: Type.Optional(
625
+ Type.Boolean({ description: "Keep ANSI escapes while reading (read) or matching (wait_output)" }),
626
+ ),
607
627
  timeout: Type.Optional(Type.Integer({ minimum: 1, description: "Wait timeout in milliseconds; omitted means indefinite" })),
608
628
  }),
609
629
  async execute(_toolCallId, params, signal, onUpdate, _ctx) {
@@ -629,6 +649,7 @@ export default function (pi: ExtensionAPI) {
629
649
  const args = ["pane", "read", params.pane, "--source", params.source || "recent-unwrapped"];
630
650
  if (params.lines != null) args.push("--lines", String(params.lines));
631
651
  if (params.format) args.push("--format", params.format);
652
+ if (params.raw) args.push("--raw");
632
653
  const output = await execHerdrText(args, signal);
633
654
  return {
634
655
  content: [{ type: "text", text: formatOutput(output) }],
@@ -775,6 +796,7 @@ export default function (pi: ExtensionAPI) {
775
796
  "Use herdr_agent for recognized coding agents. Use herdr_layout to create an available shell pane first; herdr_agent start never creates or moves terminal layout.",
776
797
  "For normal helper work, use herdr_layout pane_split, then herdr_agent start, herdr_agent prompt with wait enabled, and herdr_agent read. Use herdr_pane only for ordinary processes or intentional raw terminal control.",
777
798
  "Treat herdr_agent idle and done as ready states, blocked as requiring inspection or input, and unknown as uncertain rather than completed. CLI reads do not mark done work as seen.",
799
+ "When a prompt is rejected with agent_blocked, nothing was sent and retrying will fail the same way: inspect the blocking UI with get and read, and ask the operator before answering it.",
778
800
  "If herdr_agent read cannot recover a full alternate-screen response after increasing lines, ask the agent to write its complete response to a temporary Markdown file and return the path, then read that file directly.",
779
801
  ],
780
802
  parameters: Type.Object({
@@ -860,6 +882,13 @@ export default function (pi: ExtensionAPI) {
860
882
  try {
861
883
  response = await execHerdrJson<{ result: { agent: AgentInfo } }>(args, signal);
862
884
  } catch (error) {
885
+ if (isPromptBlocked(error)) {
886
+ const herdrMessage = error instanceof Error ? error.message : String(error);
887
+ throw new Error(
888
+ `${params.target} 处于 blocked(等待审批或回答),herdr 在发送任何输入前就拒绝了本次 prompt:${herdrMessage}\n` +
889
+ "先用 herdr_agent get / read 看清阻塞的界面,向用户确认怎么答,再用 herdr_agent prompt 或 herdr_pane send_keys 回应;不要重复下发。",
890
+ );
891
+ }
863
892
  if (!isPromptStalled(error)) throw error;
864
893
  stalledState = /status is (\w+)/.exec(error instanceof Error ? error.message : "")?.[1];
865
894
  response = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muzi3/herdr",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "description": "Herdr for pi: three independent extensions (agent + blocked state reporting, /pi-hot-restart, and the herdr_layout / herdr_pane / herdr_agent pane control tools forked from @ogulcancelik/pi-herdr) plus the herdr-guide skill.",
5
5
  "keywords": [
6
6
  "pi-package",