@michaelxwb/code-flow 0.6.2 → 0.6.4
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 +2 -2
- package/package.json +1 -1
- package/src/adapters/claude/commands/cf-init.md +1 -0
- package/src/adapters/claude/commands/cf-spec.md +7 -3
- package/src/adapters/claude/commands/cf-task/archive.md +5 -5
- package/src/adapters/claude/commands/cf-task/block.md +15 -9
- package/src/adapters/claude/commands/cf-task/graph.md +8 -1
- package/src/adapters/claude/commands/cf-task/note.md +1 -4
- package/src/adapters/claude/commands/cf-task/plan.md +2 -2
- package/src/adapters/claude/commands/cf-task/start.md +96 -25
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +8 -9
- package/src/adapters/claude/commands/cf-validate.md +15 -13
- package/src/adapters/codex/skills/cf-init/SKILL.md +1 -0
- package/src/adapters/codex/skills/cf-spec/SKILL.md +6 -2
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +5 -5
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -9
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +8 -1
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +1 -4
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +2 -2
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +96 -25
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +106 -0
- package/src/adapters/codex/skills/cf-validate/SKILL.md +15 -13
- package/src/adapters/costrict/commands/cf-init.md +1 -0
- package/src/adapters/costrict/commands/cf-spec.md +7 -3
- package/src/adapters/costrict/commands/cf-task/archive.md +5 -5
- package/src/adapters/costrict/commands/cf-task/block.md +15 -9
- package/src/adapters/costrict/commands/cf-task/graph.md +8 -1
- package/src/adapters/costrict/commands/cf-task/note.md +1 -4
- package/src/adapters/costrict/commands/cf-task/plan.md +2 -2
- package/src/adapters/costrict/commands/cf-task/start.md +96 -25
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +8 -9
- package/src/adapters/costrict/commands/cf-validate.md +15 -13
- package/src/adapters/opencode/commands/cf-init.md +9 -4
- package/src/adapters/opencode/commands/cf-spec.md +6 -2
- package/src/adapters/opencode/commands/cf-task/archive.md +5 -5
- package/src/adapters/opencode/commands/cf-task/block.md +15 -9
- package/src/adapters/opencode/commands/cf-task/graph.md +8 -1
- package/src/adapters/opencode/commands/cf-task/note.md +1 -4
- package/src/adapters/opencode/commands/cf-task/plan.md +2 -2
- package/src/adapters/opencode/commands/cf-task/start.md +96 -25
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +8 -9
- package/src/adapters/opencode/commands/cf-validate.md +15 -13
- package/src/adapters/opencode/opencode.json +1 -2
- package/src/adapters/opencode/plugins/code-flow/index.js +176 -94
- package/src/adapters/opencode/plugins/code-flow/package.json +1 -1
- package/src/cli.js +228 -64
- package/src/core/code-flow/config.yml +2 -1
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +100 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +137 -42
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +123 -38
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +83 -0
- package/src/core/code-flow/scripts/cf_core.py +34 -1
- package/src/core/code-flow/scripts/cf_exec_base.py +112 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +5 -4
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +22 -7
- package/src/core/code-flow/scripts/cf_scan.py +8 -6
- package/src/core/code-flow/scripts/cf_spec_context.py +197 -63
- package/src/core/code-flow/scripts/cf_spec_gate.py +26 -4
- package/src/core/code-flow/scripts/cf_spec_resolver.py +7 -2
- package/src/core/code-flow/scripts/cf_spec_router.py +3 -1
- package/src/core/code-flow/scripts/cf_spec_session.py +3 -2
- package/src/core/code-flow/scripts/cf_spec_verify.py +42 -31
- package/src/core/code-flow/scripts/cf_stats.py +41 -9
- package/src/core/code-flow/scripts/cf_stop_hook.py +128 -61
- package/src/core/code-flow/scripts/cf_task_index.py +148 -0
- package/src/core/code-flow/scripts/cf_task_parallel.py +385 -0
- package/src/core/code-flow/scripts/cf_task_runtime.py +98 -29
- package/src/core/code-flow/scripts/cf_task_state.py +3 -0
- package/src/core/code-flow/scripts/cf_task_workflow.py +107 -0
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +9 -5
- package/src/core/code-flow/scripts/cf_validation.py +81 -0
- package/src/core/code-flow/scripts/cf_workflow_service.py +366 -0
- package/src/core/code-flow/scripts/cf_workflow_transaction.py +96 -0
- package/src/core/code-flow/validation.yml +5 -0
|
@@ -10,7 +10,8 @@ description: 激活子任务并开始编码
|
|
|
10
10
|
## 输入
|
|
11
11
|
|
|
12
12
|
- `/cf-task:start <file> TASK-001` — 激活指定文件中的单个子任务
|
|
13
|
-
- `/cf-task:start <file>` — 激活文件内所有可执行的 draft
|
|
13
|
+
- `/cf-task:start <file>` — 激活文件内所有可执行的 draft 子任务;批次内独立任务默认并行派发子 agent
|
|
14
|
+
- `/cf-task:start <file> --serial` — 整文件模式强制串行执行,不建 worktree
|
|
14
15
|
|
|
15
16
|
其中 `<file>` 为 `.code-flow/tasks/` 下的文件名,可省略日期目录前缀和 `.md` 后缀。
|
|
16
17
|
|
|
@@ -27,8 +28,8 @@ description: 激活子任务并开始编码
|
|
|
27
28
|
用 Read 读取任务文件,定位 `## TASK-xxx` 段落:
|
|
28
29
|
|
|
29
30
|
**状态检查**:Status 必须为 `draft`。若为其他状态:
|
|
30
|
-
- `in-progress` →
|
|
31
|
-
- `done` →
|
|
31
|
+
- `in-progress` → 校验现有 marker 的需求目录、TASK-ID 与 Context;匹配且为 active 才继续步骤 2,跳过重复 Start。paused/blocked 先解除原因并调用 resume;marker 缺失或不匹配先 doctor,不能仅改 Markdown。
|
|
32
|
+
- `done` / `verified` → 实现已完成;done 的 E2E 可为 e2e_deferred,转 verify-e2e 终验。发现缺口时报告并显式重新规划修复任务,不直接把终态改成 in-progress。
|
|
32
33
|
- `blocked` → 提示"任务被阻塞",列出 Notes 中的阻塞原因,结束
|
|
33
34
|
|
|
34
35
|
**#NOTES 检查**:扫描该子任务段落全文(Description、Checklist 等)
|
|
@@ -37,7 +38,7 @@ description: 激活子任务并开始编码
|
|
|
37
38
|
|
|
38
39
|
**依赖检查**:读取 `Depends` 字段
|
|
39
40
|
- 对每个依赖的 TASK-ID,在同文件中查找其 Status
|
|
40
|
-
- 所有依赖必须为 `done`
|
|
41
|
+
- 所有依赖必须为 `done` 或 `verified`
|
|
41
42
|
- 未满足 → 输出:`前置检查失败:以下依赖未完成\n- TASK-001: in-progress\n- TASK-003: draft`
|
|
42
43
|
|
|
43
44
|
**验收契约检查**:
|
|
@@ -65,12 +66,12 @@ description: 激活子任务并开始编码
|
|
|
65
66
|
|
|
66
67
|
在改状态或生产代码前,顺序固定且不得跳步:
|
|
67
68
|
|
|
68
|
-
1. 调用 `cf_spec_context.py start --task-dir ... --root ... --task ... --task-file ... --json`,由单个进程按 refresh → active start → session 顺序执行 Start Gate;stdin JSON 传入逐路径确认的 `owned_paths`。stale/
|
|
69
|
+
1. 调用 `cf_spec_context.py start --task-dir ... --root ... --task ... --task-file ... --json`,由单个进程按 refresh → active start → session 顺序执行 Start Gate;stdin JSON 传入逐路径确认的 `owned_paths`。stale/conflict、依赖未闭合、已有/损坏 marker、未归属 diff 或 hash 不一致立即阻断。禁止先 start 再 refresh,避免 active marker 在编码前自行漂移。前置硬门禁(blocked / #NOTES / 依赖)由 workflow service 在改状态前强制执行。用户已确认内容时,Design/Plan 的 pending 不单独阻止激活;不新增阶段状态门禁。
|
|
69
70
|
2. 从命令返回值读取 refresh 后的 Context hash、active 状态和 session 输出路径;该命令只根据当前 TASK 的 `Spec-Refs`、Source 与 Acceptance Contract 覆盖写入 `.code-flow/specs/_session/task-<name>.md`,禁止重新 catalog 或猜测规则。
|
|
70
|
-
3.
|
|
71
|
-
4. 在修改任何生产代码前,为每个 Acceptance-Ref 填写测试文件、包含场景 ID
|
|
72
|
-
5.
|
|
73
|
-
6. 新功能或缺陷修复先执行一次测试并记录 RED:失败命令、失败用例和与预期缺陷对应的失败原因。纯重构或已有行为补测无法 RED
|
|
71
|
+
3. Start 返回成功时,workflow service 已通过可恢复事务同步 Status、started log 和 active marker;不要再手动改状态。失败保留原状态,按返回原因恢复。
|
|
72
|
+
4. 在修改任何生产代码前,为每个 Acceptance-Ref 填写测试文件、包含场景 ID 的测试用例名和可单独执行的命令(E2E 只登记,不在本阶段执行)
|
|
73
|
+
5. 先编写验收测试(E2E 只编写并登记)。E2E 测试必须经过契约声明的真实边界,不得用 mock 绕过 Store、Resolver、Builder、Renderer、Browser 等指定组件
|
|
74
|
+
6. 新功能或缺陷修复先执行一次测试并记录 RED:失败命令、失败用例和与预期缺陷对应的失败原因。纯重构或已有行为补测无法 RED 时,记录原因,不得伪造失败。E2E 场景不执行 RED,登记为 e2e_deferred 留给 verify-e2e
|
|
74
75
|
|
|
75
76
|
RED 证据写入 `Acceptance Evidence`:
|
|
76
77
|
|
|
@@ -88,11 +89,10 @@ RED 证据写入 `Acceptance Evidence`:
|
|
|
88
89
|
|
|
89
90
|
### 3.2 GREEN 与验收证据
|
|
90
91
|
|
|
91
|
-
1.
|
|
92
|
-
2.
|
|
93
|
-
3.
|
|
94
|
-
4.
|
|
95
|
-
5. 测试文件存在但未被测试框架收集、命令未实际执行、只有场景 ID 没有关键断言,都视为未验证
|
|
92
|
+
1. 执行 functional 验收和受影响范围的回归测试;E2E 只登记场景、断言、真实边界和命令,RED/GREEN 都不在编码阶段执行,统一留给 verify-e2e。
|
|
93
|
+
2. 对每个预期结果记录断言位置与 fixture/构造路径;环境未就绪的错误不得冒充有效 RED,明确记录尚未验证。
|
|
94
|
+
3. 由 runner 写入最新状态和运行历史;不要覆盖原契约、RED 证据或历史失败。functional 必须 verified,已登记的 E2E 在实现阶段允许 e2e_deferred。
|
|
95
|
+
4. 测试未收集、命令未实际执行或缺少关键断言,都不算验证完成。失败重跑必须使旧 verified 失效。
|
|
96
96
|
|
|
97
97
|
### 3.5 TASK-bound Spec Session
|
|
98
98
|
|
|
@@ -110,16 +110,18 @@ RED 证据写入 `Acceptance Evidence`:
|
|
|
110
110
|
|
|
111
111
|
只有同时满足以下条件才能自动完成:
|
|
112
112
|
- 所有 checklist 项均为 `[x]`
|
|
113
|
-
-
|
|
113
|
+
- functional/manual 在契约、证据和覆盖表中均为 `verified`;仅 E2E 可为 `e2e_deferred`,仍不得遗留未登记的 `planned` / `pending` / `TBD`
|
|
114
114
|
- 测试层级未低于 design,关键真实边界没有被 mock 绕过
|
|
115
|
-
-
|
|
115
|
+
- 每个预期结果都有具体断言位置,functional 验收命令和回归测试均已实际通过;E2E 留到终验执行
|
|
116
116
|
- `manual` 场景已有用户确认和可复核记录
|
|
117
117
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
118
|
+
满足后,执行唯一收尾入口:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
python3 .code-flow/scripts/cf_task_workflow.py finish --root "$PWD" --task-dir "<需求目录>" --task TASK-001 --json
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
该命令先校验完整任务身份与锁定 manifest,再执行 Done Gate(acceptance 场景 + spec verifiers + 全量 validation.yml,含 heavy);通过后以可恢复事务更新 done、Log、Updated 并清理 marker。只有 `decision=pass` 才输出完成并启动下一 TASK。禁止手动设置 done 或传入自报的 gate_passed 绕过验证。
|
|
123
125
|
|
|
124
126
|
任一验收条件不满足时保持 `in-progress`,明确列出缺口,不能标记为 `done`。
|
|
125
127
|
|
|
@@ -185,14 +187,81 @@ Spec 同步提示:
|
|
|
185
187
|
- TASK-004: #NOTES 未解决
|
|
186
188
|
- TASK-005: 依赖 TASK-004 (blocked)
|
|
187
189
|
|
|
188
|
-
|
|
190
|
+
开始执行(批次内默认并行派发子 agent,预检失败自动回退串行)...
|
|
189
191
|
```
|
|
190
192
|
|
|
191
|
-
### 4.
|
|
193
|
+
### 4. 执行批次
|
|
194
|
+
|
|
195
|
+
对每个批次执行;批次内仅 1 个 TASK、传入 `--serial`、或并行预检失败时,走 4.6 串行回退。
|
|
196
|
+
|
|
197
|
+
#### 4.1 并行预检与 worktree 准备
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
python3 .code-flow/scripts/cf_task_parallel.py prepare --root "$PWD" \
|
|
201
|
+
--task-file "<任务文件相对路径>" --tasks TASK-001,TASK-003 --json
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- 返回 `{"ok": true, "run_id": ..., "worktrees": [...]}` 才可并行;`ok: false` 时打印 `code`/`message` 并回退串行,禁止手工建 worktree/分支。
|
|
205
|
+
- 预检内容:git 仓库、`.code-flow/.gitignore` 含 `worktrees/`、主 worktree 无 active marker、tracked 工作区干净。命令会自动提交仅位于当前需求目录内的流程产物(任务文件等);需求目录以外存在未提交改动时拒绝并行,提示用户先提交或收起。
|
|
206
|
+
|
|
207
|
+
#### 4.2 派发子 agent(平台自适应)
|
|
208
|
+
|
|
209
|
+
若当前平台提供子 agent/Task 派发能力,为本批次每个 TASK 各派发一个子 agent(同一批并发不超过 3,超出时按 TASK 顺序拆成多轮)。子 agent prompt 必须包含 worktree 绝对路径、TASK-ID、任务文件相对路径,并声明以下硬性要求:
|
|
210
|
+
|
|
211
|
+
1. 进入 worktree:所有命令 `cd <worktree>` 执行,文件读写使用该 worktree 内路径。
|
|
212
|
+
2. 按本命令"单任务模式"步骤 1-4 完成该 TASK:`cf_spec_context.py start` → functional RED → 实现 → functional GREEN(E2E 只登记)→ `cf_task_workflow.py finish --root "<worktree>"`。
|
|
213
|
+
3. 平台 hook 注入绑定主工作区;子 agent 必须显式读取 Spec Session(`.code-flow/specs/_session/task-*.md`)与详设章节,不得依赖自动注入。
|
|
214
|
+
4. 完成时在 worktree 内提交全部改动(含任务文件 Checklist/Evidence/Status 更新),提交信息 `cf-task(<TASK-ID>): <标题>`。
|
|
215
|
+
5. 返回摘要:TASK-ID、Status、验收命令及结果、提交 SHA、遗留问题。
|
|
216
|
+
|
|
217
|
+
平台不支持子 agent 时,不建 worktree,直接走 4.6 串行回退。
|
|
218
|
+
|
|
219
|
+
#### 4.3 收集与校验
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
python3 .code-flow/scripts/cf_task_parallel.py collect --root "$PWD" --run-id <run_id> --json
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
- 每个任务必须 `ok: true`(改动已提交、Status 为 done/verified、marker 已清理、有提交)。
|
|
226
|
+
- 任一任务失败:停止本批次,不合并;保留 worktree 并列出失败原因。修复后重跑 collect;确认放弃时执行 4.5 cleanup。
|
|
227
|
+
|
|
228
|
+
#### 4.4 回并主分支
|
|
192
229
|
|
|
193
|
-
|
|
230
|
+
按 TASK-ID 先后顺序逐个回并;先 rebase 再合并,让冲突只在任务自己的 worktree 内解决。
|
|
194
231
|
|
|
195
|
-
|
|
232
|
+
1. 取主工作区当前分支名(主工作区执行 `git rev-parse --abbrev-ref HEAD`,记为 <主分支>),在任务 worktree 内对齐:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
cd <worktree> && git rebase <主分支>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
- rebase 冲突:读取冲突现场 + 本任务详设章节与 Acceptance Contract,合并双方意图(不是二选一);无法调和(设计要求互斥)→ `git rebase --abort`,保留现场与分支,列出矛盾点叫停交用户决策,不得擅自删除一方实现。
|
|
239
|
+
- rebase 成功后重跑该任务的 functional 验收命令(E2E 留给 verify-e2e);失败则在 worktree 内修复并提交,再执行 4.3 collect 复核。
|
|
240
|
+
|
|
241
|
+
2. 回到主工作区合并(此时应无冲突):
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
git merge --no-ff -m "merge cf-task <TASK-ID>" <branch>
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
- 仍出现冲突(主区期间有新合并):按双方意图解决并重跑双方 functional 验收;通过后提交 merge。
|
|
248
|
+
- 合并后验收失败:记录合并前 HEAD 并 `git reset --hard <合并前HEAD>`(分支与 worktree 原样保留),回到 4.2 让对应子 agent 修复后重新 collect / rebase / 合并。
|
|
249
|
+
|
|
250
|
+
3. 全部任务合并完成后进入 4.5 清理。
|
|
251
|
+
|
|
252
|
+
- E2E 验收留给 verify-e2e,不在本步骤执行。
|
|
253
|
+
|
|
254
|
+
#### 4.5 清理
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
python3 .code-flow/scripts/cf_task_parallel.py cleanup --root "$PWD" --run-id <run_id> --json
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
清理 worktree;已合入的分支自动删除,未合入的保留供排查。worktree 存在未提交改动时 cleanup 会拒绝,先人工确认再决定处理方式。全部批次完成后进入步骤 5。
|
|
261
|
+
|
|
262
|
+
#### 4.6 串行回退
|
|
263
|
+
|
|
264
|
+
未启用并行、批次仅 1 个任务或预检失败时,对批次内可激活子任务执行单任务模式步骤 3-4(详设已在步骤 2 加载,无需重复读取),完成一个后再解锁下一个。
|
|
196
265
|
|
|
197
266
|
### 5. 输出摘要与文档同步检查
|
|
198
267
|
|
|
@@ -217,3 +286,5 @@ Spec 同步提示:
|
|
|
217
286
|
|
|
218
287
|
运行 /cf-learn --map 可自动更新导航地图。
|
|
219
288
|
```
|
|
289
|
+
|
|
290
|
+
3. 若需求目录已无 `draft` / `in-progress` / `blocked` 子任务,提示:运行 `/cf-task:verify-e2e <需求目录>` 执行延迟的 E2E 终验。
|
|
@@ -21,10 +21,10 @@ E2E 测试依赖外部环境(数据库、API、浏览器等),在编码阶
|
|
|
21
21
|
|
|
22
22
|
### 1. 检查前置条件
|
|
23
23
|
|
|
24
|
-
确认需求目录下所有子任务状态为 `done
|
|
24
|
+
确认需求目录下所有子任务状态为 `done`(实现完成)或 `verified`(终验已闭环):
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
rg "
|
|
27
|
+
rg "^- \*\*Status\*\*:" <需求目录>/*.md
|
|
28
28
|
```
|
|
29
29
|
|
|
30
30
|
如有 `in-progress` 或 `blocked` 任务,提示用户先完成。
|
|
@@ -53,20 +53,19 @@ E2E 场景需要以下环境:
|
|
|
53
53
|
用户确认后,执行:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
python3 .code-flow/scripts/
|
|
57
|
-
--
|
|
58
|
-
--root . \
|
|
59
|
-
--include-e2e \
|
|
60
|
-
--write-evidence
|
|
56
|
+
python3 .code-flow/scripts/cf_task_workflow.py verify-e2e \
|
|
57
|
+
--task-dir "<需求目录>" --root "$PWD" --json
|
|
61
58
|
```
|
|
62
59
|
|
|
60
|
+
该入口重新检查所有 TASK、锁定 manifest 和 functional/manual 最新证据,内部以 `--only-e2e` 调用 runner;只有全通过才把任务提升为 verified。不得手动改状态。
|
|
61
|
+
|
|
63
62
|
### 4. 报告结果
|
|
64
63
|
|
|
65
64
|
解析执行结果:
|
|
66
65
|
|
|
67
66
|
```json
|
|
68
67
|
{
|
|
69
|
-
"decision": "
|
|
68
|
+
"decision": "block",
|
|
70
69
|
"results": [
|
|
71
70
|
{"id": "E-01", "kind": "e2e", "status": "passed"},
|
|
72
71
|
{"id": "E-02", "kind": "e2e", "status": "failed", "exit_code": 1}
|
|
@@ -79,7 +78,7 @@ python3 .code-flow/scripts/cf_acceptance_runner.py \
|
|
|
79
78
|
|
|
80
79
|
### 5. 更新任务状态
|
|
81
80
|
|
|
82
|
-
|
|
81
|
+
确认命令已将需求目录下所有任务 Status 更新为 `verified`,并保留原契约与运行历史。无需再次编辑状态。
|
|
83
82
|
|
|
84
83
|
## 失败处理
|
|
85
84
|
|
|
@@ -8,7 +8,7 @@ description: 根据变更文件自动匹配并执行验证规则
|
|
|
8
8
|
|
|
9
9
|
## 输入
|
|
10
10
|
|
|
11
|
-
- `/project:cf-validate` —
|
|
11
|
+
- `/project:cf-validate` — 自动获取任务基线或 Git 全部变更文件
|
|
12
12
|
- `/project:cf-validate src/Foo.tsx` — 验证指定文件
|
|
13
13
|
- `/project:cf-validate --files=src/a.ts,src/b.ts` — 验证多个文件
|
|
14
14
|
|
|
@@ -19,10 +19,10 @@ description: 根据变更文件自动匹配并执行验证规则
|
|
|
19
19
|
用 Bash 执行:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
|
|
22
|
+
python3 .code-flow/scripts/cf_validation.py --root "$PWD" --scope-only --json
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
有 active TASK 时使用冻结基线以来的全部任务变更(含已提交文件);无 active TASK 时包含暂存、未暂存和未跟踪文件。用户指定路径时用 `--files "src/a.py" "src/b.py"` 原样传递;若结果确实为空才输出“无变更需要验证”。
|
|
26
26
|
|
|
27
27
|
### 2. 读取验证规则
|
|
28
28
|
|
|
@@ -36,19 +36,21 @@ validators:
|
|
|
36
36
|
trigger: "**/*.{ts,tsx}"
|
|
37
37
|
command: "npx tsc --noEmit"
|
|
38
38
|
timeout: 30000
|
|
39
|
+
heavy: false # true = Stop 每轮跳过,finish 与 /cf-validate 执行
|
|
39
40
|
on_fail: "修复建议"
|
|
40
41
|
```
|
|
41
42
|
|
|
43
|
+
全量测试 / e2e 等慢命令标 `heavy: true`:Stop 每轮跳过,**任务 finish 自动执行一次**,也可显式运行 `/cf-validate`(或 CI),避免拖慢每个任务。
|
|
44
|
+
|
|
42
45
|
### 3. 匹配并执行验证
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
- 未匹配 → 跳过该规则
|
|
47
|
+
执行统一验证入口:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
python3 .code-flow/scripts/cf_validation.py --root "$PWD" --json
|
|
51
|
+
```
|
|
50
52
|
|
|
51
|
-
|
|
53
|
+
显式范围追加 `--files "src/a.py" "src/b.py"`。程序匹配 trigger、解析 argv 并展开 `{files}`,不拼接 shell 命令;同一运行内相同 argv/cwd/timeout 去重,跨运行不复用。删除文件仍触发全局测试,但不传给文件级编译器。每条 timeout 仍按配置的毫秒值生效,总预算耗尽返回 incomplete/block。
|
|
52
54
|
|
|
53
55
|
### 4. 汇总结果
|
|
54
56
|
|
|
@@ -65,13 +67,13 @@ validators:
|
|
|
65
67
|
|
|
66
68
|
## 安全设计
|
|
67
69
|
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
+
- `{files}` 直接展开为 argv 参数;带空格、引号的路径仍是一个参数,不手工拼接引号或执行 shell
|
|
71
|
+
- 范围来自任务基线、Git 状态(包括 untracked)或用户显式指定的路径
|
|
70
72
|
- 用户指定的路径必须位于项目根目录内
|
|
71
73
|
|
|
72
74
|
## 异常处理
|
|
73
75
|
|
|
74
76
|
- validation.yml 不存在 → 尝试检测 package.json scripts 中的 test/lint 命令
|
|
75
77
|
- 命令执行超时 → 输出超时提示,建议增大 timeout 或缩小验证范围
|
|
76
|
-
-
|
|
78
|
+
- 命令不存在或配置无效 → 返回 block 并提示所缺依赖,不当作验证通过
|
|
77
79
|
- 无变更文件 → 输出"无变更需要验证"
|
|
@@ -1,12 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { appendFileSync, mkdirSync } from "fs";
|
|
4
4
|
|
|
5
|
+
// Note: intentionally no `import { Plugin } from "@opencode/plugin"`.
|
|
6
|
+
// The server does not auto-provide that package for local plugins, and
|
|
7
|
+
// `Plugin.define` is only an identity wrapper — a plain default export
|
|
8
|
+
// with `id` + `setup` satisfies the v2 plugin schema dependency-free,
|
|
9
|
+
// so `code-flow init` works offline without npm install.
|
|
10
|
+
|
|
5
11
|
const SCRIPT_DIR = ".code-flow/scripts";
|
|
12
|
+
const STOP_CHECK_TIMEOUT = 35000;
|
|
13
|
+
const HOOK_TIMEOUT = 5000;
|
|
6
14
|
|
|
7
|
-
// Per-session
|
|
15
|
+
// Per-session queued feedback. Single map, append-only: idle/stop-check and
|
|
16
|
+
// post-check feedback accumulate here and are consumed by the next context
|
|
17
|
+
// hook. Never overwrite — a prompt hook arriving between idle and context
|
|
18
|
+
// must not drop queued stop-check feedback.
|
|
8
19
|
const sessionContext = new Map();
|
|
9
20
|
|
|
21
|
+
export function mergePending(existing, incoming) {
|
|
22
|
+
if (!existing) return incoming;
|
|
23
|
+
if (!incoming) return existing;
|
|
24
|
+
return existing + "\n\n" + incoming;
|
|
25
|
+
}
|
|
26
|
+
|
|
10
27
|
function debugLog(projectRoot, msg) {
|
|
11
28
|
if (process.env.CF_DEBUG !== "1") return;
|
|
12
29
|
try {
|
|
@@ -21,111 +38,176 @@ function pythonPath(projectRoot, script) {
|
|
|
21
38
|
return join(projectRoot, SCRIPT_DIR, script);
|
|
22
39
|
}
|
|
23
40
|
|
|
24
|
-
function callHook(projectRoot, script, input, timeout =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
function callHook(projectRoot, script, input, timeout = HOOK_TIMEOUT) {
|
|
42
|
+
// Async child process: never block the plugin event thread. opencode's
|
|
43
|
+
// idle event cannot interrupt a turn, so stop-check failures queue into
|
|
44
|
+
// sessionContext for the next context hook (documented platform gap
|
|
45
|
+
// vs. blocking Stop hooks).
|
|
46
|
+
return new Promise((resolve) => {
|
|
47
|
+
const child = execFile(
|
|
48
|
+
"python3",
|
|
49
|
+
[pythonPath(projectRoot, script)],
|
|
50
|
+
{ cwd: projectRoot, timeout, maxBuffer: 1024 * 1024 },
|
|
51
|
+
(error, stdout, stderr) => {
|
|
52
|
+
if (error) {
|
|
53
|
+
debugLog(projectRoot, `callHook ${script} failed: ${error.message || stderr}`);
|
|
54
|
+
resolve(null);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const text = (stdout || "").trim();
|
|
58
|
+
if (!text) {
|
|
59
|
+
resolve(null);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
resolve(JSON.parse(text));
|
|
64
|
+
} catch (e) {
|
|
65
|
+
debugLog(projectRoot, `callHook ${script} bad JSON: ${e.message}`);
|
|
66
|
+
resolve(null);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
if (input !== undefined && child.stdin) {
|
|
71
|
+
child.stdin.write(JSON.stringify(input));
|
|
72
|
+
child.stdin.end();
|
|
36
73
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function queueFeedback(sessionID, text) {
|
|
78
|
+
if (!sessionID || !text) return;
|
|
79
|
+
sessionContext.set(sessionID, mergePending(sessionContext.get(sessionID), text));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function consumeFeedback(sessionID) {
|
|
83
|
+
const pending = sessionContext.get(sessionID);
|
|
84
|
+
if (pending) sessionContext.delete(sessionID);
|
|
85
|
+
return pending;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toolInputFilePath(input) {
|
|
89
|
+
if (!input || typeof input !== "object") return "";
|
|
90
|
+
const args = input;
|
|
91
|
+
return args.filePath || args.file_path || args.path || "";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function runStopCheck(projectRoot, sessionID) {
|
|
95
|
+
const result = await callHook(projectRoot, "cf_stop_hook.py", { session_id: sessionID }, STOP_CHECK_TIMEOUT);
|
|
96
|
+
if (result?.reason) {
|
|
97
|
+
// idle 无法阻断,校验失败排队到下一轮 context hook
|
|
98
|
+
queueFeedback(sessionID, result.reason);
|
|
99
|
+
debugLog(projectRoot, `stop-check feedback queued`);
|
|
43
100
|
}
|
|
44
101
|
}
|
|
45
102
|
|
|
46
|
-
function
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
103
|
+
async function registerPromptHook(ctx, projectRoot) {
|
|
104
|
+
await ctx.session.hook("prompt", async (event) => {
|
|
105
|
+
const promptText = event.prompt?.text || "";
|
|
106
|
+
if (!promptText) return;
|
|
107
|
+
debugLog(projectRoot, `prompt sid=${event.sessionID} prompt_len=${promptText.length}`);
|
|
108
|
+
const result = await callHook(projectRoot, "cf_user_prompt_hook.py", {
|
|
109
|
+
prompt: promptText,
|
|
110
|
+
session_id: event.sessionID,
|
|
111
|
+
});
|
|
112
|
+
const additional = result?.hookSpecificOutput?.additionalContext;
|
|
113
|
+
if (additional) {
|
|
114
|
+
// Append, never overwrite: queued stop-check feedback survives.
|
|
115
|
+
queueFeedback(event.sessionID, additional);
|
|
116
|
+
debugLog(projectRoot, `hook matched — context ${additional.length} chars cached`);
|
|
117
|
+
} else {
|
|
118
|
+
debugLog(projectRoot, `hook returned no context`);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function registerToolHook(ctx, projectRoot) {
|
|
124
|
+
await ctx.tool.hook("execute.after", async (event) => {
|
|
125
|
+
const tool = String(event.tool || "").toLowerCase();
|
|
126
|
+
if (!["edit", "write", "multiedit", "patch"].includes(tool)) return;
|
|
127
|
+
const filePath = toolInputFilePath(event.input);
|
|
128
|
+
if (!filePath) return;
|
|
129
|
+
const result = await callHook(projectRoot, "cf_post_hook.py", {
|
|
130
|
+
tool_name: tool === "write" ? "Write" : "Edit",
|
|
131
|
+
tool_input: { file_path: filePath },
|
|
132
|
+
session_id: event.sessionID || "",
|
|
133
|
+
});
|
|
134
|
+
const additional = result?.hookSpecificOutput?.additionalContext;
|
|
135
|
+
if (additional) {
|
|
136
|
+
// 反馈排队,下一轮 context hook 注入(opencode 无法当轮插话)
|
|
137
|
+
queueFeedback(event.sessionID, additional);
|
|
138
|
+
debugLog(projectRoot, `post-check feedback queued ${additional.length} chars`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
52
141
|
}
|
|
53
142
|
|
|
54
|
-
|
|
55
|
-
|
|
143
|
+
async function registerContextHook(ctx) {
|
|
144
|
+
await ctx.session.hook("context", (event) => {
|
|
145
|
+
const pending = consumeFeedback(event.sessionID);
|
|
146
|
+
if (pending) {
|
|
147
|
+
event.system.push({ type: "text", text: pending });
|
|
148
|
+
debugLog(ctx.location.directory, `context — injected ${pending.length} chars`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
56
152
|
|
|
153
|
+
// 子会话(子 agent / worktree 并行任务)的 idle 不得触发主工作区 stop-check:
|
|
154
|
+
// 子会话的编辑发生在自己的工作区,主区既无 active marker 也没有对应文件,
|
|
155
|
+
// 触发只会重复执行主区 validators(全量测试)。用 session.created.parentID 标记子会话。
|
|
156
|
+
export function createSessionRegistry() {
|
|
157
|
+
const children = new Set();
|
|
57
158
|
return {
|
|
58
|
-
event
|
|
59
|
-
if (
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
159
|
+
observe(event) {
|
|
160
|
+
if (!event || typeof event.type !== "string") return null;
|
|
161
|
+
const sid = event.data?.sessionID || "";
|
|
162
|
+
if (!sid) return null;
|
|
163
|
+
if (event.type === "session.created") {
|
|
164
|
+
if (event.data?.parentID) children.add(sid);
|
|
165
|
+
return { type: "created", sid };
|
|
63
166
|
}
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
input.event?.properties?.sessionID ||
|
|
67
|
-
input.event?.properties?.info?.id || "";
|
|
68
|
-
const result = callHook(projectRoot, "cf_stop_hook.py", { session_id: sid }, 35000);
|
|
69
|
-
if (result?.reason) {
|
|
70
|
-
// idle 无法阻断,校验失败排队到下一轮 system prompt
|
|
71
|
-
const pending = sessionContext.get(sid);
|
|
72
|
-
sessionContext.set(sid, pending ? pending + "\n\n" + result.reason : result.reason);
|
|
73
|
-
debugLog(projectRoot, `stop-check feedback queued`);
|
|
74
|
-
}
|
|
167
|
+
if (event.type === "session.idle") {
|
|
168
|
+
return { type: "idle", sid, child: children.has(sid) };
|
|
75
169
|
}
|
|
170
|
+
return null;
|
|
76
171
|
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
77
174
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
result.hookSpecificOutput.additionalContext
|
|
95
|
-
);
|
|
96
|
-
} else {
|
|
97
|
-
debugLog(projectRoot, `hook returned no context`);
|
|
175
|
+
function startEventSubscription(ctx, projectRoot) {
|
|
176
|
+
const controller = new AbortController();
|
|
177
|
+
const registry = createSessionRegistry();
|
|
178
|
+
void (async () => {
|
|
179
|
+
try {
|
|
180
|
+
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
|
|
181
|
+
const observed = registry.observe(event);
|
|
182
|
+
if (!observed) continue;
|
|
183
|
+
if (observed.type === "created") {
|
|
184
|
+
debugLog(projectRoot, `session.created sid=${observed.sid}`);
|
|
185
|
+
sessionContext.delete(observed.sid);
|
|
186
|
+
} else if (observed.child) {
|
|
187
|
+
debugLog(projectRoot, `session.idle sid=${observed.sid} (child) — stop-check skipped`);
|
|
188
|
+
} else {
|
|
189
|
+
await runStopCheck(projectRoot, observed.sid);
|
|
190
|
+
}
|
|
98
191
|
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const tool = String(input?.tool || "").toLowerCase();
|
|
103
|
-
if (!["edit", "write", "multiedit", "patch"].includes(tool)) return;
|
|
104
|
-
const args = output?.args || input?.args || {};
|
|
105
|
-
const filePath = args.filePath || args.file_path || args.path;
|
|
106
|
-
if (!filePath) return;
|
|
107
|
-
const result = callHook(projectRoot, "cf_post_hook.py", {
|
|
108
|
-
tool_name: tool === "write" ? "Write" : "Edit",
|
|
109
|
-
tool_input: { file_path: filePath },
|
|
110
|
-
session_id: input?.sessionID || "",
|
|
111
|
-
});
|
|
112
|
-
const ctx = result?.hookSpecificOutput?.additionalContext;
|
|
113
|
-
if (ctx) {
|
|
114
|
-
// 反馈排队,下一轮 system.transform 注入(opencode 无法当轮插话)
|
|
115
|
-
const sid = input?.sessionID || "";
|
|
116
|
-
const pending = sessionContext.get(sid);
|
|
117
|
-
sessionContext.set(sid, pending ? pending + "\n\n" + ctx : ctx);
|
|
118
|
-
debugLog(projectRoot, `post-check feedback queued ${ctx.length} chars`);
|
|
192
|
+
} catch (err) {
|
|
193
|
+
if (err?.name !== "AbortError") {
|
|
194
|
+
debugLog(projectRoot, `event subscription ended: ${err?.message || err}`);
|
|
119
195
|
}
|
|
120
|
-
}
|
|
196
|
+
}
|
|
197
|
+
})();
|
|
198
|
+
return () => controller.abort();
|
|
199
|
+
}
|
|
121
200
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
201
|
+
const CodeFlowPlugin = {
|
|
202
|
+
id: "code-flow",
|
|
203
|
+
async setup(ctx) {
|
|
204
|
+
const projectRoot = ctx.location.directory;
|
|
205
|
+
await registerPromptHook(ctx, projectRoot);
|
|
206
|
+
await registerToolHook(ctx, projectRoot);
|
|
207
|
+
await registerContextHook(ctx);
|
|
208
|
+
const stopSubscription = startEventSubscription(ctx, projectRoot);
|
|
209
|
+
return () => stopSubscription();
|
|
210
|
+
},
|
|
131
211
|
};
|
|
212
|
+
|
|
213
|
+
export default CodeFlowPlugin;
|