@zhushanwen/pi-cw-tool 0.4.0 → 0.4.2
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.
|
|
3
|
+
"version": "0.4.2",
|
|
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",
|
package/skills/pi-cw/SKILL.md
CHANGED
|
@@ -5,16 +5,16 @@ description: "cw 递归编排大型多 agent 并行开发任务,适用于需多
|
|
|
5
5
|
|
|
6
6
|
# pi-cw
|
|
7
7
|
|
|
8
|
-
主 agent 发起递归编排:用 cw 建一棵 epic
|
|
8
|
+
主 agent 发起递归编排:用 cw 建一棵 <顶层>->...->wave 的任务树(<顶层> 通常 epic,也可 feature/slice),派第一个 planning-agent 自递归展开整棵树,主 agent 空闲等 steer 唤醒,全树完成后报告用户。
|
|
9
9
|
|
|
10
|
-
> **编排机制**:子 agent 完成时 pi 自动 steer 唤醒父 agent(事件驱动,无轮询)。主 agent 只派第一个
|
|
10
|
+
> **编排机制**:子 agent 完成时 pi 自动 steer 唤醒父 agent(事件驱动,无轮询)。主 agent 只派第一个(根层) planning-agent,**不自己 descend 到子层**。设计依据见 `design-v4.md`(同目录)。
|
|
11
11
|
|
|
12
12
|
## 何时用
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
- 多个
|
|
17
|
-
- 单 agent
|
|
14
|
+
核心判据:**任务需要多 subagent 并行推进 + 上下文隔离**(不是树深——一棵 epic 树也能单 agent 线性走,见 cw-cli)。满足以下场景之一才用 pi-cw:
|
|
15
|
+
- 多个 wave 要 worktree 隔离并行开发
|
|
16
|
+
- 多个 slice/feature 子树要并行展开
|
|
17
|
+
- 单 agent 线性走完整棵树会撑爆上下文(设计 + 实现 + 审查 + 合并全栈),需按层隔离上下文
|
|
18
18
|
|
|
19
19
|
> 单 agent 模式或小任务(改 typo / 单文件 / 明确小 bug)走 `cw-cli` skill,不必建树。
|
|
20
20
|
|
|
@@ -22,6 +22,7 @@ description: "cw 递归编排大型多 agent 并行开发任务,适用于需多
|
|
|
22
22
|
|
|
23
23
|
- 单文件小改、明确的小 bug:直接 edit,或派单个 worker subagent;或走 `cw-cli` skill 单 agent 模式
|
|
24
24
|
- 线性任务、无需多 agent 并行:走 cw 单层 wave 即可,不必建树
|
|
25
|
+
- 能单 agent 线性走完的任务(哪怕要建 epic 树):走 `cw-cli` skill 单 agent 模式,不必上递归编排
|
|
25
26
|
- 纯分析 / 调研 / 设计文档:不写代码不该进 cw 编排
|
|
26
27
|
|
|
27
28
|
## 前置:cw-tool
|
|
@@ -40,26 +41,28 @@ description: "cw 递归编排大型多 agent 并行开发任务,适用于需多
|
|
|
40
41
|
### 1. 建树根
|
|
41
42
|
|
|
42
43
|
```bash
|
|
43
|
-
cw create
|
|
44
|
+
cw create <顶层> --slug <kebab-slug> --objective "<一句话目标,含可验收的完成标准>"
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
|
|
47
|
+
`<顶层>` 通常 epic,但 feature/slice 也能做根——选能覆盖全貌的最小层(选层标准复用 cw-cli skill 的「规模 × 性质」表)。递归编排的额外门槛:**顶层必须会拆出 ≥2 个可并行的下层 unit**;只拆 1 个(无并行价值)或整棵树线性串行即可,走 cw-cli 单 agent 模式更省。
|
|
48
|
+
|
|
49
|
+
拿到根 unit 的 unitId(下文记作 `<根Id>`)。
|
|
47
50
|
|
|
48
51
|
### 2. 派第一个 planning-agent
|
|
49
52
|
|
|
50
53
|
用 `subagent` 工具**后台**派发(`planning-agent` 是 cw-tool 内置的 agent 模板):
|
|
51
54
|
|
|
52
55
|
```
|
|
53
|
-
subagent(action="start", agent="planning-agent", slug="
|
|
54
|
-
task="<背景>这是 cw
|
|
56
|
+
subagent(action="start", agent="planning-agent", slug="<根-slug>-planning", fork=false,
|
|
57
|
+
task="<背景>这是 cw <根层> <根Id> 的层主 agent,目标:<原 objective>。这是递归编排,你会自递归派下层 planning-agent(<根层> 是 epic 派 feature,是 feature 派 slice,是 slice 派 wave)。<目标>先调 cw handoff --unitId <根Id> 拿上下文与 guidance,按 guidance 的派发指导自递归展开并合并子树。<验收>cw status --unitId <根Id> 显示该 <根层> 子树全部 closed。")
|
|
55
58
|
# 不传 model 参数——默认继承主 agent 模型,递归传给所有下层(见「模型派发」)。
|
|
56
59
|
# 用户特别指定时才传 model="provider/modelId",单个 subagent 生效或作为全树根模型。
|
|
57
60
|
```
|
|
58
61
|
|
|
59
62
|
task 三要素:
|
|
60
|
-
-
|
|
61
|
-
- **目标**:入口是 `cw handoff --unitId
|
|
62
|
-
- **验收**:`cw status --unitId
|
|
63
|
+
- **背景**:`<根层>` 的 `<根Id>`、目标、说明这是递归编排(planning-agent 会自递归派下层)
|
|
64
|
+
- **目标**:入口是 `cw handoff --unitId <根Id>`;自递归的每一步按 cw guidance 的派发指导执行
|
|
65
|
+
- **验收**:`cw status --unitId <根Id>` 子树全 `closed`(可查的检查点,禁止"完成""实现该功能"这类不可证伪描述)
|
|
63
66
|
|
|
64
67
|
派发后主 agent 结束当前 turn,进空闲态(session 保活)。
|
|
65
68
|
|
|
@@ -71,7 +74,7 @@ planning-agent 自递归展开(epic->feature->slice->wave),每层 design -> 审
|
|
|
71
74
|
|
|
72
75
|
```bash
|
|
73
76
|
cw status # 全局
|
|
74
|
-
cw frontier --root
|
|
77
|
+
cw frontier --root <根Id> # 看根子树 frontier
|
|
75
78
|
```
|
|
76
79
|
|
|
77
80
|
- 子树全 `closed` -> 进入第 5 步汇报用户
|
|
@@ -86,7 +89,7 @@ cw frontier --root <epicId> # 看 epic 子树 frontier
|
|
|
86
89
|
|
|
87
90
|
- **只派第一个 planning-agent**:主 agent 不自己 descend 到 feature / slice / wave 层。下层派发是 planning-agent 的职责(它调 cw execute 自动建子 unit,并按 guidance 派子 planning-agent / wave-agent)。
|
|
88
91
|
- **靠 cw 查进度,不信自报**:agent 汇报"我做完了"不等于 cw 状态 closed。以 `cw status` / `cw frontier` 为唯一真相。
|
|
89
|
-
- **worktree 隔离**:wave 层用 `worktree: true` 派出(各 wave 独立工作目录,并行不冲突;worktree 与 fork 正交,fork 默认 false);主 agent
|
|
92
|
+
- **worktree 隔离**:wave 层用 `worktree: true` 派出(各 wave 独立工作目录,并行不冲突;worktree 与 fork 正交,fork 默认 false);主 agent 派的根层 planning-agent 不需 worktree(它只编排不写码)。worktree 的合并与清理由 slice 层 planning-agent 派 chain workflow(merge-agent)处理,细节见 planning-agent 模板。
|
|
90
93
|
- **失败恢复靠 L0-L3**:cw gate fail / 审查 must-fix / 方案缺陷 / 父层拆错,各有恢复路径(L0 就地改重审 / L1 cw replan / L2 父 replan 级联 / L3 上报人),定义在 planning-agent 模板与 cw guidance,本 skill 不重复。
|
|
91
94
|
|
|
92
95
|
## 模型派发
|
|
@@ -40,6 +40,24 @@ function createGitRepo(parentDir: string, name: string): string {
|
|
|
40
40
|
return realpathSync(repo);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** 建 bare repo + worktree workspace(xyz-agent 模式:.bare + worktree 子目录)。
|
|
44
|
+
* 返回 realpath 规范化的 worktree 路径。bare repo worktree 内 git-common-dir basename 是 .bare(非 .git),
|
|
45
|
+
* 是 detectRepoWorkspace 加固分支的核心场景(设计文档 §2.4 / 决策 2)。 */
|
|
46
|
+
function createBareRepoWorkspace(parentDir: string, name: string): string {
|
|
47
|
+
const wsRoot = path.join(parentDir, name);
|
|
48
|
+
mkdirSync(wsRoot);
|
|
49
|
+
// 先建普通 seed repo(含初始 commit,bare repo 不能直接 commit)
|
|
50
|
+
const seed = path.join(parentDir, `${name}-seed`);
|
|
51
|
+
mkdirSync(seed);
|
|
52
|
+
execSync("git init -q", { cwd: seed });
|
|
53
|
+
execSync("git -c user.name=test -c user.email=test@test.local commit -q --allow-empty -m init", { cwd: seed });
|
|
54
|
+
// clone --bare 成 .bare,再 worktree add
|
|
55
|
+
execSync(`git clone -q --bare ${seed} .bare`, { cwd: wsRoot });
|
|
56
|
+
const worktree = path.join(wsRoot, "main");
|
|
57
|
+
execSync('git --git-dir=.bare worktree add -q main', { cwd: wsRoot });
|
|
58
|
+
return realpathSync(worktree);
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
afterEach(() => {
|
|
44
62
|
for (const dir of tmpDirs.splice(0)) {
|
|
45
63
|
rmSync(dir, { recursive: true, force: true });
|
|
@@ -93,6 +111,37 @@ describe("detectRepoWorkspace(真实 git)", () => {
|
|
|
93
111
|
});
|
|
94
112
|
});
|
|
95
113
|
|
|
114
|
+
// ── detectRepoWorkspace(bare repo + worktree 模式)──────────────
|
|
115
|
+
|
|
116
|
+
describe("detectRepoWorkspace(bare repo + worktree 模式)", () => {
|
|
117
|
+
it("bare repo worktree(.bare)→ undefined(dirname(.bare)=容器根非 git 目录,不传 --workspace)", () => {
|
|
118
|
+
const base = makeTempDir("cw-bare-");
|
|
119
|
+
const worktree = createBareRepoWorkspace(base, "ws");
|
|
120
|
+
expect(detectRepoWorkspace(worktree)).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("防误用:--is-bare-repository 在 worktree 内返回 false(不可作 bare 判据)", () => {
|
|
124
|
+
const base = makeTempDir("cw-bare-isbare-");
|
|
125
|
+
const worktree = createBareRepoWorkspace(base, "ws");
|
|
126
|
+
const isBare = execSync("git rev-parse --is-bare-repository", { cwd: worktree })
|
|
127
|
+
.toString()
|
|
128
|
+
.trim();
|
|
129
|
+
expect(isBare).toBe("false");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("bare repo worktree:common-dir basename 是 .bare(非 .git)", () => {
|
|
133
|
+
const base = makeTempDir("cw-bare-commondir-");
|
|
134
|
+
const worktree = createBareRepoWorkspace(base, "ws");
|
|
135
|
+
const commonDir = execSync(
|
|
136
|
+
"git -C . rev-parse --path-format=absolute --git-common-dir",
|
|
137
|
+
{ cwd: worktree },
|
|
138
|
+
)
|
|
139
|
+
.toString()
|
|
140
|
+
.trim();
|
|
141
|
+
expect(path.basename(commonDir)).toBe(".bare");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
96
145
|
// ── buildCwArgs 纯函数(workspace 参数)─────────────────────────
|
|
97
146
|
|
|
98
147
|
describe("buildCwArgs(workspace 参数)", () => {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* workspace 门控测试:probeCwCliNormalization 纯函数 + executeCwAction 门控决策。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* 门控语义(cw-cli ADR-0014 store-workspace decoupling):cw-cli 支持 store 内部归一化
|
|
5
5
|
* (probe 版本 >= MIN_CW_CLI_VERSION_FOR_NORMALIZATION)→ write action 不传 --workspace(纯封装);
|
|
6
|
-
* 不支持 → 兜底 ADR-
|
|
6
|
+
* 不支持 → 兜底 cw-cli ADR-0014 的 detectRepoWorkspace + --workspace。只读 action 始终不传(S-3)。
|
|
7
7
|
*
|
|
8
8
|
* detectRepoWorkspace 探测纯函数 + buildCwArgs 构造的测试在 detect-repo-workspace.test.ts,
|
|
9
9
|
* 本文件只测门控决策(probe 三态 × action 两类 × git 环境)。
|
|
@@ -62,6 +62,8 @@ interface GateSpawnerOpts {
|
|
|
62
62
|
versionExitCode?: number;
|
|
63
63
|
/** cw --version spawn 抛异常(模拟 cw 不在 PATH)。 */
|
|
64
64
|
versionThrow?: boolean;
|
|
65
|
+
/** action 调用的返回(默认成功 `{ stdout: "{}", exitCode: 0 }`,覆盖以测失败分支)。 */
|
|
66
|
+
actionResult?: CwSpawnResult;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
@@ -84,7 +86,7 @@ function gateSpawner(opts: GateSpawnerOpts = {}): {
|
|
|
84
86
|
exitCode: opts.versionExitCode ?? 0,
|
|
85
87
|
};
|
|
86
88
|
}
|
|
87
|
-
return { stdout: "{}", stderr: "", exitCode: 0 };
|
|
89
|
+
return opts.actionResult ?? { stdout: "{}", stderr: "", exitCode: 0 };
|
|
88
90
|
});
|
|
89
91
|
return { spawner, calls };
|
|
90
92
|
}
|
|
@@ -109,13 +111,13 @@ describe("probeCwCliNormalization", () => {
|
|
|
109
111
|
|
|
110
112
|
it("版本 === MIN(等号边界)→ supported:true(门控用 >=,锁定等号语义,S-7)", async () => {
|
|
111
113
|
const cwd = makeTempDir("probe-eq-");
|
|
112
|
-
const { spawner } = gateSpawner({ versionStdout: "cw
|
|
114
|
+
const { spawner } = gateSpawner({ versionStdout: "cw 1.6.2" });
|
|
113
115
|
const cap = await probeCwCliNormalization(spawner, cwd);
|
|
114
116
|
expect(cap.supported).toBe(true);
|
|
115
|
-
expect(cap.version).toBe("
|
|
117
|
+
expect(cap.version).toBe("1.6.2");
|
|
116
118
|
});
|
|
117
119
|
|
|
118
|
-
it("版本 < MIN → supported:false
|
|
120
|
+
it("版本 < MIN → supported:false(1.6.2 是归一化首版,1.6.1 不支持)", async () => {
|
|
119
121
|
const cwd = makeTempDir("probe-unsupported-");
|
|
120
122
|
const { spawner } = gateSpawner({ versionStdout: "cw 1.6.1" });
|
|
121
123
|
const cap = await probeCwCliNormalization(spawner, cwd);
|
|
@@ -196,7 +198,7 @@ describe("executeCwAction workspace 门控", () => {
|
|
|
196
198
|
expect(actionCall(calls).args).not.toContain("--workspace");
|
|
197
199
|
});
|
|
198
200
|
|
|
199
|
-
it("TC2: probe 不支持 → write action 兜底传 --workspace(ADR-
|
|
201
|
+
it("TC2: probe 不支持 → write action 兜底传 --workspace(cw-cli ADR-0014 行为)", async () => {
|
|
200
202
|
const base = makeTempDir("gate-tc2-");
|
|
201
203
|
const repo = createGitRepo(base, "repo");
|
|
202
204
|
const { spawner, calls } = gateSpawner({ versionStdout: "cw 1.6.1" });
|
|
@@ -230,4 +232,30 @@ describe("executeCwAction workspace 门控", () => {
|
|
|
230
232
|
await executeCwAction("execute", DEV_ALLOWED, "cw_dev", "u1", {}, spawner, plain);
|
|
231
233
|
expect(actionCall(calls).args).not.toContain("--workspace");
|
|
232
234
|
});
|
|
235
|
+
|
|
236
|
+
it("TC6: 非 git 目录 + probe 不支持 + action 失败(exitCode 1)→ error 含升级指引(degradedNoWorkspace 分支回归)", async () => {
|
|
237
|
+
const plain = makeTempDir("gate-tc6-plain-");
|
|
238
|
+
const { spawner } = gateSpawner({
|
|
239
|
+
versionStdout: "cw 1.6.1",
|
|
240
|
+
actionResult: { stdout: "", stderr: "boom", exitCode: 1 },
|
|
241
|
+
});
|
|
242
|
+
const result = await executeCwAction("execute", DEV_ALLOWED, "cw_dev", "u1", {}, spawner, plain);
|
|
243
|
+
expect(result.ok).toBe(false);
|
|
244
|
+
// 升级指引文案锚点(准则 6:错误指向恢复动作)
|
|
245
|
+
expect(result.error).toContain("cw-cli 版本过低");
|
|
246
|
+
expect(result.error).toContain("@zhushanwen/coding-workflow@latest");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("TC7: git 目录 + probe 不支持 + action 失败 → error 不含升级指引(非降级路径不追加)", async () => {
|
|
250
|
+
const base = makeTempDir("gate-tc7-");
|
|
251
|
+
const repo = createGitRepo(base, "repo");
|
|
252
|
+
const { spawner } = gateSpawner({
|
|
253
|
+
versionStdout: "cw 1.6.1",
|
|
254
|
+
actionResult: { stdout: "", stderr: "boom", exitCode: 1 },
|
|
255
|
+
});
|
|
256
|
+
const result = await executeCwAction("execute", DEV_ALLOWED, "cw_dev", "u1", {}, spawner, repo);
|
|
257
|
+
expect(result.ok).toBe(false);
|
|
258
|
+
expect(result.error).toContain("exit code 1");
|
|
259
|
+
expect(result.error).not.toContain("cw-cli 版本过低");
|
|
260
|
+
});
|
|
233
261
|
});
|
package/src/cw-runner.ts
CHANGED
|
@@ -84,13 +84,19 @@ export function rejectDisallowedAction(
|
|
|
84
84
|
const GIT_PROBE_TIMEOUT_MS = 5000;
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
* 探测 cwd 所属 repo 的主目录(repo 级 workspace
|
|
87
|
+
* 探测 cwd 所属 repo 的主目录(repo 级 workspace),供老 cw-cli(<1.6.2,无 store 归一化)兜底。
|
|
88
88
|
*
|
|
89
89
|
* 用 `git rev-parse --path-format=absolute --git-common-dir` 取 git common dir:
|
|
90
90
|
* 同一 repo 的所有 worktree 返回相同路径,dirname 即 repo 主目录。cw store 键控
|
|
91
|
-
* 从 per-cwd 升级为 repo 级(ADR-
|
|
91
|
+
* 从 per-cwd 升级为 repo 级(cw-cli ADR-0014)后,spawn cw 时附带 --workspace 让 cw
|
|
92
92
|
* 在 repo 主目录解析/共享状态,避免同一 repo 的 worktree 间状态各自为政。
|
|
93
93
|
*
|
|
94
|
+
* **bare repo + worktree 模式(.bare)**:common-dir basename 是 `.bare` 而非 `.git`,
|
|
95
|
+
* dirname 指向 workspace 容器根(非 git 目录)——传给 cw 会让它 fallback 到错误的 store-key
|
|
96
|
+
* (unit not found)。检测到 basename 非 `.git` 时返回 undefined(不传 --workspace),让老 cw-cli
|
|
97
|
+
* 退回 per-cwd store(读写一致但无 repo 级共享)。新 cw-cli(≥1.6.2)自己归一化,门控支持→不调本函数。
|
|
98
|
+
* 不可用 `--is-bare-repository` 判据——worktree 内它永远返回 false(bare 是 .bare 目录本身)。
|
|
99
|
+
*
|
|
94
100
|
* 任何失败(非 git 目录、git 不在 PATH、路径不存在、超时)→ undefined(不抛)。
|
|
95
101
|
*/
|
|
96
102
|
export function detectRepoWorkspace(cwd: string): string | undefined {
|
|
@@ -103,6 +109,11 @@ export function detectRepoWorkspace(cwd: string): string | undefined {
|
|
|
103
109
|
if (result.status !== 0) return undefined;
|
|
104
110
|
const gitCommonDir = result.stdout.trim();
|
|
105
111
|
if (gitCommonDir.length === 0) return undefined;
|
|
112
|
+
// 非标准 git-dir 一律退回 per-cwd(fail-safe):bare repo worktree(basename=.bare,
|
|
113
|
+
// dirname 指向容器根,非 git 目录)、submodule(common-dir=.git/modules/<name>)、
|
|
114
|
+
// --separate-git-dir / GIT_DIR 等。把 dirname 传给 cw 会导致 store-key fallback 错误
|
|
115
|
+
// → unit not found。返回 undefined 退回 per-cwd(读写一致)。
|
|
116
|
+
if (path.basename(gitCommonDir) !== ".git") return undefined;
|
|
106
117
|
return path.dirname(gitCommonDir);
|
|
107
118
|
} catch {
|
|
108
119
|
return undefined;
|
|
@@ -114,14 +125,16 @@ export function detectRepoWorkspace(cwd: string): string | undefined {
|
|
|
114
125
|
*
|
|
115
126
|
* cw-tool 与 cw-cli 是两个独立 npm 包(cw-tool 经 PATH 裸调 cw、零依赖声明),
|
|
116
127
|
* 两包独立升级。cw-tool 退回纯封装前需探测 cw-cli 是否已落地 store 归一化
|
|
117
|
-
* (
|
|
118
|
-
*
|
|
128
|
+
* (cw-cli commit a90e8e8 / 首个 tag v1.6.2:getCwJsonPath 改用 detectCommonDir 归一化
|
|
129
|
+
* 到 git-common-dir),支持则不传 --workspace(纯封装),不支持则兜底 cw-cli ADR-0014
|
|
130
|
+
* 的 detectRepoWorkspace + --workspace。
|
|
119
131
|
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
132
|
+
* 门控激活(1.6.2):cw-cli ≥1.6.2 自我用 detectCommonDir 归一化 store-key,同一 repo 的
|
|
133
|
+
* 所有 worktree(含 bare repo)共享 store,cw-tool 无需传 --workspace。旧值 "99.0.0"(placeholder)
|
|
134
|
+
* 使门控永远判定「不支持」→ 永远走兜底,在 bare repo worktree 下 detectRepoWorkspace 返回容器根
|
|
135
|
+
* → cw 定位到不存在的 store → unit not found。
|
|
123
136
|
*/
|
|
124
|
-
const MIN_CW_CLI_VERSION_FOR_NORMALIZATION = "
|
|
137
|
+
const MIN_CW_CLI_VERSION_FOR_NORMALIZATION = "1.6.2";
|
|
125
138
|
|
|
126
139
|
/** probe 超时(ms):cw --version 卡死时 fail-safe 为「不支持」。 */
|
|
127
140
|
const CW_VERSION_PROBE_TIMEOUT_MS = 5000;
|
|
@@ -170,7 +183,7 @@ function compareSemver(a: number[], b: number[]): number {
|
|
|
170
183
|
* 探测 cw-cli 是否支持 store 内部归一化(门控)。
|
|
171
184
|
*
|
|
172
185
|
* 用 spawner 跑 `cw --version`,parse 版本号与 {@link MIN_CW_CLI_VERSION_FOR_NORMALIZATION}
|
|
173
|
-
* 比较。失败(spawn 失败/parse 不到/超时)→ supported:false(fail-safe,兜底 ADR-
|
|
186
|
+
* 比较。失败(spawn 失败/parse 不到/超时)→ supported:false(fail-safe,兜底 cw-cli ADR-0014 行为)。
|
|
174
187
|
* 进程内 memoize(全局单值,cw 版本 cwd 无关):首次探测后缓存,消除重复 spawn。
|
|
175
188
|
*
|
|
176
189
|
* @param parentSignal 调用方 SDK abort signal,与内部 5s 超时合并转发给 spawner(S-4):
|
|
@@ -185,7 +198,7 @@ export async function probeCwCliNormalization(
|
|
|
185
198
|
|
|
186
199
|
const minVersion = parseCwVersion(MIN_CW_CLI_VERSION_FOR_NORMALIZATION);
|
|
187
200
|
if (!minVersion) {
|
|
188
|
-
//
|
|
201
|
+
// MIN_CW_CLI_VERSION_FOR_NORMALIZATION 本身非法(不应发生)——保守不支持
|
|
189
202
|
const fallback: CwCliCapability = { supported: false, version: undefined, reason: "MIN_CW_CLI_VERSION_FOR_NORMALIZATION 非法" };
|
|
190
203
|
cachedCapability = fallback;
|
|
191
204
|
return fallback;
|
|
@@ -336,16 +349,24 @@ export async function executeCwAction(
|
|
|
336
349
|
const unitIdErr = rejectMissingUnitId(action, unitId);
|
|
337
350
|
if (unitIdErr) return { ok: false, ...base, error: unitIdErr };
|
|
338
351
|
|
|
339
|
-
// workspace
|
|
352
|
+
// workspace 门控(cw-cli ADR-0014 store-workspace decoupling):cw-cli 支持 store 内部归一化
|
|
340
353
|
// (probe 版本 >= MIN_CW_CLI_VERSION_FOR_NORMALIZATION)→ 纯封装不传 --workspace;
|
|
341
|
-
// 不支持 → 兜底 ADR-
|
|
354
|
+
// 不支持 → 兜底 cw-cli ADR-0014 的 detectRepoWorkspace + --workspace(保持向后兼容)。
|
|
342
355
|
// 只读 action 始终不传(S-3:保守避免 cw 子命令拒收未知选项导致 readonly 查询失败)。
|
|
343
356
|
let workspace: string | undefined;
|
|
357
|
+
// 降级标记:老 cw-cli(不支持归一化)+ bare repo / 非 git(detectRepoWorkspace 返回 undefined)。
|
|
358
|
+
// 写动作若失败,错误消息追加升级指引(准则 6:错误指向恢复动作)。
|
|
359
|
+
let degradedNoWorkspace = false;
|
|
344
360
|
if (isReadonlyAction(action)) {
|
|
345
361
|
workspace = undefined;
|
|
346
362
|
} else {
|
|
347
363
|
const capability = await probeCwCliNormalization(spawner, cwd, signal);
|
|
348
|
-
|
|
364
|
+
if (capability.supported) {
|
|
365
|
+
workspace = undefined;
|
|
366
|
+
} else {
|
|
367
|
+
workspace = detectRepoWorkspace(cwd);
|
|
368
|
+
degradedNoWorkspace = workspace === undefined;
|
|
369
|
+
}
|
|
349
370
|
}
|
|
350
371
|
const args = buildCwArgs(action, unitId, opts, workspace);
|
|
351
372
|
const stdinPayload = opts.input !== undefined ? opts.input : undefined;
|
|
@@ -388,7 +409,15 @@ export async function executeCwAction(
|
|
|
388
409
|
if (exitCode !== 0) {
|
|
389
410
|
const parts: string[] = [`exit code ${exitCode ?? "null"}`];
|
|
390
411
|
if (stderr.trim()) parts.push(stderr.trim());
|
|
391
|
-
|
|
412
|
+
let error = parts.join(" | ");
|
|
413
|
+
// 老 cw-cli(<1.6.2)+ 探测不到 repo 级 workspace(非 git 目录 / bare repo worktree /
|
|
414
|
+
// git 不可用):写动作退回 per-cwd store(读写一致但无 repo 级共享),多数情况能跑通;
|
|
415
|
+
// 若仍失败,追加升级指引帮用户切到归一化 cw-cli(准则 6:错误指向恢复动作)。
|
|
416
|
+
// 措辞同时覆盖两种降级原因(非 git 场景不误导为 bare repo 问题)。
|
|
417
|
+
if (degradedNoWorkspace) {
|
|
418
|
+
error += "\n👉 cw-cli 版本过低(<1.6.2 不支持 store-key 归一化),且当前目录未探测到 repo 级 workspace(非 git 目录 / bare repo worktree / git 不可用),写动作退回 per-cwd store(读写一致但无 repo 级共享)。建议升级:npm i -g @zhushanwen/coding-workflow@latest";
|
|
419
|
+
}
|
|
420
|
+
return { ok: false, ...base, error };
|
|
392
421
|
}
|
|
393
422
|
|
|
394
423
|
const data = tryParseJson(stdout);
|