@zhushanwen/pi-cw-tool 0.4.2 → 0.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-cw-tool",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Pi extension wrapping the `cw` CLI as role-restricted tools (cw_planning / cw_wave / cw_dev / cw_review) with per-tool action whitelists — hard-guarantees no self-review by layer-owner agents.",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -750,4 +750,33 @@ describe("cw 路径解析", () => {
750
750
  expect(result.stderr).toContain("spawn cw ENOENT");
751
751
  expect(result.stdout).toBe("");
752
752
  });
753
+
754
+ it("[worktree-reaper-fix] cwd 不存在 → 不 spawn,exitCode:-1 + 可操作错误(含 cwd 路径与恢复指引)", async () => {
755
+ // worktree 被 orphan reaper 误删后,子进程 cwd 指向虚空。spawn 前检查必须拦截并返回
756
+ // 含完整 cwd + 恢复指引的错误(否则 Node ENOENT 只报 command 名,误导诊断为"node 被卸载")。
757
+ const result = await defaultCwSpawner(
758
+ ["status", "--unitId", "u1"],
759
+ undefined,
760
+ "/nonexistent-cwd-for-reaper-test",
761
+ );
762
+
763
+ expect(result.exitCode).toBe(-1);
764
+ expect(result.stderr).toContain("/nonexistent-cwd-for-reaper-test");
765
+ expect(result.stderr).toContain("worktrees.json");
766
+ // 前置检查拦截:不进入 spawn
767
+ expect(spawnMock).not.toHaveBeenCalled();
768
+ });
769
+
770
+ it("[worktree-reaper-fix] spawn error ENOENT → stderr 拼 cwd 路径(TOCTOU 兜底)", async () => {
771
+ // existsSync 检查通过后目录被删(TOCTOU):error handler 必须兜底拼 cwd。
772
+ const child = makeFakeChild();
773
+ spawnMock.mockImplementation(() => child as unknown as cp.ChildProcess);
774
+ const err = Object.assign(new Error("spawn cw ENOENT"), { code: "ENOENT" });
775
+ queueMicrotask(() => child.emit("error", err));
776
+
777
+ const result = await defaultCwSpawner(["status", "--unitId", "u1"], undefined, "/tmp");
778
+
779
+ expect(result.exitCode).toBe(-1);
780
+ expect(result.stderr).toContain("cwd: /tmp");
781
+ });
753
782
  });
package/src/cw-spawn.ts CHANGED
@@ -6,9 +6,28 @@
6
6
  *
7
7
  * cw 路径解析:spawn 裸命令名 `cw`,由 OS execvp 语义在 `process.env.PATH` 中
8
8
  * 查找(架构约定 #16:禁止写死绝对路径)。env 继承自 process.env,确保 PATH 可用。
9
+ *
10
+ * [worktree-reaper-fix] cwd 可能已被 orphan reaper 清理(pi-subagent-workflow 误删活
11
+ * worktree 后子进程 cwd 指向虚空)。Node spawn 对不存在的 cwd 报 ENOENT,错误消息只含
12
+ * command 名("cw")不含 cwd——2026-08-11 事故中导致 AI 误诊"node 被卸载"。故 spawn 前
13
+ * 检查 cwd 存在性,失败时返回可操作错误(含完整 cwd + 恢复指引)。
9
14
  */
15
+ import { existsSync } from "node:fs";
10
16
  import { spawn } from "node:child_process";
11
17
 
18
+ /**
19
+ * cwd 不存在时的可操作错误文案(错误 → 权威源 worktrees.json → 重试闭环)。
20
+ * @param cwd 不存在的路径
21
+ */
22
+ function cwdMissingError(cwd: string): string {
23
+ return [
24
+ `cwd 不存在:${cwd}`,
25
+ "该 worktree 可能已被 orphan reaper 清理,或子 agent 已结束。",
26
+ "恢复:1) 检查 pi agent 目录下 subagents/worktrees.json 中该 branch 的 pid 是否已补全;",
27
+ " 2) 若子 agent 仍在运行,重新派发(worktree 重建);3) 若已结束,忽略此错误。",
28
+ ].join("\n");
29
+ }
30
+
12
31
  /** cw 子进程执行结果。 */
13
32
  export interface CwSpawnResult {
14
33
  /** stdout 内容(cw action 通常把结果 JSON 输出到 stdout)。 */
@@ -47,6 +66,13 @@ export type CwSpawner = (
47
66
  */
48
67
  export const defaultCwSpawner: CwSpawner = (args, input, cwd, signal) =>
49
68
  new Promise<CwSpawnResult>((resolve) => {
69
+ // [worktree-reaper-fix] spawn 前检查 cwd 存在性(TOCTOU 兜底见下方 error handler)。
70
+ // 与现有 error 路径返回形态一致:resolve + exitCode=-1 + stderr,不 reject。
71
+ if (!existsSync(cwd)) {
72
+ resolve({ stdout: "", stderr: cwdMissingError(cwd), exitCode: -1 });
73
+ return;
74
+ }
75
+
50
76
  const child = spawn("cw", args, {
51
77
  cwd,
52
78
  env: process.env,
@@ -92,8 +118,13 @@ export const defaultCwSpawner: CwSpawner = (args, input, cwd, signal) =>
92
118
  };
93
119
 
94
120
  child.on("error", (err: NodeJS.ErrnoException) => {
95
- // spawn 失败(cw 不在 PATH / 无执行权限等)。exitCode=-1 区分于正常退出码。
96
- finish({ stdout, stderr: `${stderr}\n[spawn error] ${err.message}`, exitCode: -1 });
121
+ // spawn 失败(cw 不在 PATH / 无执行权限 / TOCTOU:检查后 cwd 被删等)。
122
+ // [worktree-reaper-fix] cwd 进错误消息:ENOENT err.message 只有 command 名,
123
+ // 无 cwd 线索会导致误诊(2026-08-11 事故 AI 误判"node 被卸载")。
124
+ // exitCode=-1 区分于正常退出码。
125
+ const errCwd = err.code === "ENOENT" ? `\ncwd: ${cwd}` : "";
126
+ const hint = err.code === "ENOENT" && !existsSync(cwd) ? `\n${cwdMissingError(cwd)}` : "";
127
+ finish({ stdout, stderr: `${stderr}\n[spawn error] ${err.message}${errCwd}${hint}`, exitCode: -1 });
97
128
  });
98
129
  child.on("close", (code: number | null) => {
99
130
  finish({ stdout, stderr, exitCode: code });