@michaelxwb/code-flow 0.6.4 → 0.6.5
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 +1 -1
- package/src/adapters/claude/commands/cf-task/archive.md +2 -0
- package/src/adapters/claude/commands/cf-task/start.md +19 -0
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +6 -1
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +2 -0
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +19 -0
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +6 -1
- package/src/adapters/costrict/commands/cf-task/archive.md +2 -0
- package/src/adapters/costrict/commands/cf-task/start.md +19 -0
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +6 -1
- package/src/adapters/opencode/commands/cf-task/archive.md +2 -0
- package/src/adapters/opencode/commands/cf-task/start.md +19 -0
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +6 -1
- package/src/core/code-flow/scripts/cf_spec_context.py +34 -7
- package/src/core/code-flow/scripts/cf_spec_gate.py +2 -0
- package/src/core/code-flow/scripts/cf_spec_metadata.py +34 -3
- package/src/core/code-flow/scripts/cf_spec_verify.py +107 -9
- package/src/core/code-flow/scripts/cf_task_parallel.py +21 -12
- package/src/core/code-flow/scripts/cf_task_runtime.py +12 -7
- package/src/core/code-flow/scripts/cf_task_workflow.py +111 -13
package/package.json
CHANGED
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
|
|
37
37
|
- 需求目录必须存在 `spec-context.yml`;执行 `python3 .code-flow/scripts/cf_spec_context.py refresh --task-dir <需求目录> --root "$PWD" --json`
|
|
38
38
|
- 执行 `python3 .code-flow/scripts/cf_spec_gate.py --task-dir <需求目录> --stage code --json`;required Rule 的 stale/pending/conflict/unverified 任一存在即 FAIL
|
|
39
|
+
- 执行 review 终验:`python3 .code-flow/scripts/cf_task_workflow.py verify-e2e --task-dir <需求目录> --root "$PWD" --json`(聚合需求目录全部 task context 的 `stage: review` verifier + acceptance E2E,按 spec/rule 去重执行一次并写回证据);`decision=pass` 才继续,失败保持阻断
|
|
40
|
+
- 执行 `python3 .code-flow/scripts/cf_spec_gate.py --task-dir <需求目录> --stage review --json`;required Rule 存在 unverified review 证据即 FAIL(先完成 review 终验)
|
|
39
41
|
- 若 `.code-flow/.active-task.json` 仍指向本需求,先执行 `python3 .code-flow/scripts/cf_task_workflow.py finish --root "$PWD" --task-dir "<需求目录>" --task TASK-001 --json`,通过后再 verify-e2e;不得自报 gate_passed 或通过归档绕过 active 状态
|
|
40
42
|
- Context 与 Evidence 随需求目录一并归档,`_session` 仅是可重建投影,不是事实源
|
|
41
43
|
|
|
@@ -209,6 +209,23 @@ python3 .code-flow/scripts/cf_task_parallel.py prepare --root "$PWD" \
|
|
|
209
209
|
4. 完成时在 worktree 内提交全部改动(含任务文件 Checklist/Evidence/Status 更新),提交信息 `cf-task(<TASK-ID>): <标题>`。
|
|
210
210
|
5. 返回摘要:TASK-ID、Status、验收命令及结果、提交 SHA、遗留问题。
|
|
211
211
|
|
|
212
|
+
标准 worker prompt 模板(替换占位符后派发):
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
你在隔离 worktree 中执行 <TASK-ID>:<worktree 绝对路径>。
|
|
216
|
+
1) cd 到 worktree;先读 Spec Session(.code-flow/specs/_session/task-<name>.md)、任务文件的当前 TASK 段落与 design 来源章节;
|
|
217
|
+
2) cf_spec_context.py start → 写验收测试记录 RED → 实现 → GREEN;
|
|
218
|
+
3) 运行 cf_task_workflow.py finish --root "$PWD" --task <TASK-ID> --json;decision=pass 后【再】提交全部改动(含 finish 回写的 Evidence/状态):git add -A && git commit -m "cf-task(<TASK-ID>): <标题>";
|
|
219
|
+
4) 返回摘要:TASK-ID、Status、验收命令与结果、commit SHA、遗留问题。
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
> 提交必须在 finish 之后:finish 通过后会回写 Evidence/状态;collect 默认会自动提交这些回写,但显式提交更清晰。
|
|
223
|
+
|
|
224
|
+
子 agent 中断(取消/超时)时不要重新 prepare:检查该 worktree 的 `git status`、`git log <base>..HEAD` 与 marker——
|
|
225
|
+
- 无改动且无 marker:直接接管,按单任务模式继续;
|
|
226
|
+
- 有未提交改动或已有提交但未 done:在 worktree 内接管收尾(补实现/测试并 finish),再走 4.3 collect;
|
|
227
|
+
- 已 done 且 marker 已清理:直接进入 4.3。
|
|
228
|
+
|
|
212
229
|
平台不支持子 agent 时,不建 worktree,直接走 4.6 串行回退。
|
|
213
230
|
|
|
214
231
|
#### 4.3 收集与校验
|
|
@@ -218,6 +235,7 @@ python3 .code-flow/scripts/cf_task_parallel.py collect --root "$PWD" --run-id <r
|
|
|
218
235
|
```
|
|
219
236
|
|
|
220
237
|
- 每个任务必须 `ok: true`(改动已提交、Status 为 done/verified、marker 已清理、有提交)。
|
|
238
|
+
- `collect` 在任务 done/verified 且 marker 已清理后,会自动提交 finish 回写的 Evidence/状态(`--no-commit` 关闭并回到严格模式);finish 之外的未提交改动仍应人工确认。
|
|
221
239
|
- 任一任务失败:停止本批次,不合并;保留 worktree 并列出失败原因。修复后重跑 collect;确认放弃时执行 4.5 cleanup。
|
|
222
240
|
|
|
223
241
|
#### 4.4 回并主分支
|
|
@@ -240,6 +258,7 @@ git merge --no-ff -m "merge cf-task <TASK-ID>" <branch>
|
|
|
240
258
|
```
|
|
241
259
|
|
|
242
260
|
- 仍出现冲突(主区期间有新合并):按双方意图解决并重跑双方 functional 验收;通过后提交 merge。
|
|
261
|
+
- 状态文件冲突(`spec-context.yml` / 任务 md 的覆盖状态列 / `.acceptance-manifest.json`)按确定性优先级解决:覆盖状态列取并集(两边各自 verified 的行保留 verified);`spec-context.yml` 取已包含全量规则证据的一侧;manifest 多数情况按行自动合并,冲突时以任务文件为准重建后重跑 functional 场景。解决后必须 `cf_spec_context.py refresh` 收敛 hash,并重跑合并双方的 functional 验收与 `cf_spec_gate --stage code --json`,通过后提交 merge。
|
|
243
262
|
- 合并后验收失败:记录合并前 HEAD 并 `git reset --hard <合并前HEAD>`(分支与 worktree 原样保留),回到 4.2 让对应子 agent 修复后重新 collect / rebase / 合并。
|
|
244
263
|
|
|
245
264
|
3. 全部任务合并完成后进入 4.5 清理。
|
|
@@ -9,7 +9,12 @@ globs: []
|
|
|
9
9
|
|
|
10
10
|
## 使用场景
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
本命令是**需求级终验**入口,在所有子任务的 functional 测试通过后统一执行:
|
|
13
|
+
|
|
14
|
+
1. **review 层 verifier**:绑定规范中 `stage: review` 的 required verifier(e2e/acceptance/构建等重型验证)。遍历需求目录全部 task context,按 spec/rule 去重执行一次,证据写回全部相关 context;输出 `executed` / `reused` / `failed` 计数。
|
|
15
|
+
2. **E2E 验收场景**:依赖外部环境(数据库、API、浏览器等)、编码阶段标记 `e2e_deferred` 的场景。
|
|
16
|
+
|
|
17
|
+
失败不写 verified、不反转已 done 的任务状态;归档前必须 `decision=pass`。未声明 review verifier 且无 E2E 场景时为空操作(`reason=nothing_to_verify`)。
|
|
13
18
|
|
|
14
19
|
## 调用方式
|
|
15
20
|
|
|
@@ -37,6 +37,8 @@ description: Archive a completed task file after completeness, correctness, trac
|
|
|
37
37
|
|
|
38
38
|
- 需求目录必须存在 `spec-context.yml`;执行 `python3 .code-flow/scripts/cf_spec_context.py refresh --task-dir <需求目录> --root "$PWD" --json`
|
|
39
39
|
- 执行 `python3 .code-flow/scripts/cf_spec_gate.py --task-dir <需求目录> --stage code --json`;required Rule 的 stale/pending/conflict/unverified 任一存在即 FAIL
|
|
40
|
+
- 执行 review 终验:`python3 .code-flow/scripts/cf_task_workflow.py verify-e2e --task-dir <需求目录> --root "$PWD" --json`(聚合需求目录全部 task context 的 `stage: review` verifier + acceptance E2E,按 spec/rule 去重执行一次并写回证据);`decision=pass` 才继续,失败保持阻断
|
|
41
|
+
- 执行 `python3 .code-flow/scripts/cf_spec_gate.py --task-dir <需求目录> --stage review --json`;required Rule 存在 unverified review 证据即 FAIL(先完成 review 终验)
|
|
40
42
|
- 若 `.code-flow/.active-task.json` 仍指向本需求,先执行 `python3 .code-flow/scripts/cf_task_workflow.py finish --root "$PWD" --task-dir "<需求目录>" --task TASK-001 --json`,通过后再 verify-e2e;不得自报 gate_passed 或通过归档绕过 active 状态
|
|
41
43
|
- Context 与 Evidence 随需求目录一并归档,`_session` 仅是可重建投影,不是事实源
|
|
42
44
|
|
|
@@ -210,6 +210,23 @@ python3 .code-flow/scripts/cf_task_parallel.py prepare --root "$PWD" \
|
|
|
210
210
|
4. 完成时在 worktree 内提交全部改动(含任务文件 Checklist/Evidence/Status 更新),提交信息 `cf-task(<TASK-ID>): <标题>`。
|
|
211
211
|
5. 返回摘要:TASK-ID、Status、验收命令及结果、提交 SHA、遗留问题。
|
|
212
212
|
|
|
213
|
+
标准 worker prompt 模板(替换占位符后派发):
|
|
214
|
+
|
|
215
|
+
```
|
|
216
|
+
你在隔离 worktree 中执行 <TASK-ID>:<worktree 绝对路径>。
|
|
217
|
+
1) cd 到 worktree;先读 Spec Session(.code-flow/specs/_session/task-<name>.md)、任务文件的当前 TASK 段落与 design 来源章节;
|
|
218
|
+
2) cf_spec_context.py start → 写验收测试记录 RED → 实现 → GREEN;
|
|
219
|
+
3) 运行 cf_task_workflow.py finish --root "$PWD" --task <TASK-ID> --json;decision=pass 后【再】提交全部改动(含 finish 回写的 Evidence/状态):git add -A && git commit -m "cf-task(<TASK-ID>): <标题>";
|
|
220
|
+
4) 返回摘要:TASK-ID、Status、验收命令与结果、commit SHA、遗留问题。
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
> 提交必须在 finish 之后:finish 通过后会回写 Evidence/状态;collect 默认会自动提交这些回写,但显式提交更清晰。
|
|
224
|
+
|
|
225
|
+
子 agent 中断(取消/超时)时不要重新 prepare:检查该 worktree 的 `git status`、`git log <base>..HEAD` 与 marker——
|
|
226
|
+
- 无改动且无 marker:直接接管,按单任务模式继续;
|
|
227
|
+
- 有未提交改动或已有提交但未 done:在 worktree 内接管收尾(补实现/测试并 finish),再走 4.3 collect;
|
|
228
|
+
- 已 done 且 marker 已清理:直接进入 4.3。
|
|
229
|
+
|
|
213
230
|
平台不支持子 agent 时,不建 worktree,直接走 4.6 串行回退。
|
|
214
231
|
|
|
215
232
|
#### 4.3 收集与校验
|
|
@@ -219,6 +236,7 @@ python3 .code-flow/scripts/cf_task_parallel.py collect --root "$PWD" --run-id <r
|
|
|
219
236
|
```
|
|
220
237
|
|
|
221
238
|
- 每个任务必须 `ok: true`(改动已提交、Status 为 done/verified、marker 已清理、有提交)。
|
|
239
|
+
- `collect` 在任务 done/verified 且 marker 已清理后,会自动提交 finish 回写的 Evidence/状态(`--no-commit` 关闭并回到严格模式);finish 之外的未提交改动仍应人工确认。
|
|
222
240
|
- 任一任务失败:停止本批次,不合并;保留 worktree 并列出失败原因。修复后重跑 collect;确认放弃时执行 4.5 cleanup。
|
|
223
241
|
|
|
224
242
|
#### 4.4 回并主分支
|
|
@@ -241,6 +259,7 @@ git merge --no-ff -m "merge cf-task <TASK-ID>" <branch>
|
|
|
241
259
|
```
|
|
242
260
|
|
|
243
261
|
- 仍出现冲突(主区期间有新合并):按双方意图解决并重跑双方 functional 验收;通过后提交 merge。
|
|
262
|
+
- 状态文件冲突(`spec-context.yml` / 任务 md 的覆盖状态列 / `.acceptance-manifest.json`)按确定性优先级解决:覆盖状态列取并集(两边各自 verified 的行保留 verified);`spec-context.yml` 取已包含全量规则证据的一侧;manifest 多数情况按行自动合并,冲突时以任务文件为准重建后重跑 functional 场景。解决后必须 `cf_spec_context.py refresh` 收敛 hash,并重跑合并双方的 functional 验收与 `cf_spec_gate --stage code --json`,通过后提交 merge。
|
|
244
263
|
- 合并后验收失败:记录合并前 HEAD 并 `git reset --hard <合并前HEAD>`(分支与 worktree 原样保留),回到 4.2 让对应子 agent 修复后重新 collect / rebase / 合并。
|
|
245
264
|
|
|
246
265
|
3. 全部任务合并完成后进入 4.5 清理。
|
|
@@ -5,7 +5,12 @@ description: Execute deferred E2E scenarios after all functional tests pass
|
|
|
5
5
|
|
|
6
6
|
## 使用场景
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
本命令是**需求级终验**入口,在所有子任务的 functional 测试通过后统一执行:
|
|
9
|
+
|
|
10
|
+
1. **review 层 verifier**:绑定规范中 `stage: review` 的 required verifier(e2e/acceptance/构建等重型验证)。遍历需求目录全部 task context,按 spec/rule 去重执行一次,证据写回全部相关 context;输出 `executed` / `reused` / `failed` 计数。
|
|
11
|
+
2. **E2E 验收场景**:依赖外部环境(数据库、API、浏览器等)、编码阶段标记 `e2e_deferred` 的场景。
|
|
12
|
+
|
|
13
|
+
失败不写 verified、不反转已 done 的任务状态;归档前必须 `decision=pass`。未声明 review verifier 且无 E2E 场景时为空操作(`reason=nothing_to_verify`)。
|
|
9
14
|
|
|
10
15
|
## 调用方式
|
|
11
16
|
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
|
|
37
37
|
- 需求目录必须存在 `spec-context.yml`;执行 `python3 .code-flow/scripts/cf_spec_context.py refresh --task-dir <需求目录> --root "$PWD" --json`
|
|
38
38
|
- 执行 `python3 .code-flow/scripts/cf_spec_gate.py --task-dir <需求目录> --stage code --json`;required Rule 的 stale/pending/conflict/unverified 任一存在即 FAIL
|
|
39
|
+
- 执行 review 终验:`python3 .code-flow/scripts/cf_task_workflow.py verify-e2e --task-dir <需求目录> --root "$PWD" --json`(聚合需求目录全部 task context 的 `stage: review` verifier + acceptance E2E,按 spec/rule 去重执行一次并写回证据);`decision=pass` 才继续,失败保持阻断
|
|
40
|
+
- 执行 `python3 .code-flow/scripts/cf_spec_gate.py --task-dir <需求目录> --stage review --json`;required Rule 存在 unverified review 证据即 FAIL(先完成 review 终验)
|
|
39
41
|
- 若 `.code-flow/.active-task.json` 仍指向本需求,先执行 `python3 .code-flow/scripts/cf_task_workflow.py finish --root "$PWD" --task-dir "<需求目录>" --task TASK-001 --json`,通过后再 verify-e2e;不得自报 gate_passed 或通过归档绕过 active 状态
|
|
40
42
|
- Context 与 Evidence 随需求目录一并归档,`_session` 仅是可重建投影,不是事实源
|
|
41
43
|
|
|
@@ -209,6 +209,23 @@ python3 .code-flow/scripts/cf_task_parallel.py prepare --root "$PWD" \
|
|
|
209
209
|
4. 完成时在 worktree 内提交全部改动(含任务文件 Checklist/Evidence/Status 更新),提交信息 `cf-task(<TASK-ID>): <标题>`。
|
|
210
210
|
5. 返回摘要:TASK-ID、Status、验收命令及结果、提交 SHA、遗留问题。
|
|
211
211
|
|
|
212
|
+
标准 worker prompt 模板(替换占位符后派发):
|
|
213
|
+
|
|
214
|
+
```
|
|
215
|
+
你在隔离 worktree 中执行 <TASK-ID>:<worktree 绝对路径>。
|
|
216
|
+
1) cd 到 worktree;先读 Spec Session(.code-flow/specs/_session/task-<name>.md)、任务文件的当前 TASK 段落与 design 来源章节;
|
|
217
|
+
2) cf_spec_context.py start → 写验收测试记录 RED → 实现 → GREEN;
|
|
218
|
+
3) 运行 cf_task_workflow.py finish --root "$PWD" --task <TASK-ID> --json;decision=pass 后【再】提交全部改动(含 finish 回写的 Evidence/状态):git add -A && git commit -m "cf-task(<TASK-ID>): <标题>";
|
|
219
|
+
4) 返回摘要:TASK-ID、Status、验收命令与结果、commit SHA、遗留问题。
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
> 提交必须在 finish 之后:finish 通过后会回写 Evidence/状态;collect 默认会自动提交这些回写,但显式提交更清晰。
|
|
223
|
+
|
|
224
|
+
子 agent 中断(取消/超时)时不要重新 prepare:检查该 worktree 的 `git status`、`git log <base>..HEAD` 与 marker——
|
|
225
|
+
- 无改动且无 marker:直接接管,按单任务模式继续;
|
|
226
|
+
- 有未提交改动或已有提交但未 done:在 worktree 内接管收尾(补实现/测试并 finish),再走 4.3 collect;
|
|
227
|
+
- 已 done 且 marker 已清理:直接进入 4.3。
|
|
228
|
+
|
|
212
229
|
平台不支持子 agent 时,不建 worktree,直接走 4.6 串行回退。
|
|
213
230
|
|
|
214
231
|
#### 4.3 收集与校验
|
|
@@ -218,6 +235,7 @@ python3 .code-flow/scripts/cf_task_parallel.py collect --root "$PWD" --run-id <r
|
|
|
218
235
|
```
|
|
219
236
|
|
|
220
237
|
- 每个任务必须 `ok: true`(改动已提交、Status 为 done/verified、marker 已清理、有提交)。
|
|
238
|
+
- `collect` 在任务 done/verified 且 marker 已清理后,会自动提交 finish 回写的 Evidence/状态(`--no-commit` 关闭并回到严格模式);finish 之外的未提交改动仍应人工确认。
|
|
221
239
|
- 任一任务失败:停止本批次,不合并;保留 worktree 并列出失败原因。修复后重跑 collect;确认放弃时执行 4.5 cleanup。
|
|
222
240
|
|
|
223
241
|
#### 4.4 回并主分支
|
|
@@ -240,6 +258,7 @@ git merge --no-ff -m "merge cf-task <TASK-ID>" <branch>
|
|
|
240
258
|
```
|
|
241
259
|
|
|
242
260
|
- 仍出现冲突(主区期间有新合并):按双方意图解决并重跑双方 functional 验收;通过后提交 merge。
|
|
261
|
+
- 状态文件冲突(`spec-context.yml` / 任务 md 的覆盖状态列 / `.acceptance-manifest.json`)按确定性优先级解决:覆盖状态列取并集(两边各自 verified 的行保留 verified);`spec-context.yml` 取已包含全量规则证据的一侧;manifest 多数情况按行自动合并,冲突时以任务文件为准重建后重跑 functional 场景。解决后必须 `cf_spec_context.py refresh` 收敛 hash,并重跑合并双方的 functional 验收与 `cf_spec_gate --stage code --json`,通过后提交 merge。
|
|
243
262
|
- 合并后验收失败:记录合并前 HEAD 并 `git reset --hard <合并前HEAD>`(分支与 worktree 原样保留),回到 4.2 让对应子 agent 修复后重新 collect / rebase / 合并。
|
|
244
263
|
|
|
245
264
|
3. 全部任务合并完成后进入 4.5 清理。
|
|
@@ -9,7 +9,12 @@ globs: []
|
|
|
9
9
|
|
|
10
10
|
## 使用场景
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
本命令是**需求级终验**入口,在所有子任务的 functional 测试通过后统一执行:
|
|
13
|
+
|
|
14
|
+
1. **review 层 verifier**:绑定规范中 `stage: review` 的 required verifier(e2e/acceptance/构建等重型验证)。遍历需求目录全部 task context,按 spec/rule 去重执行一次,证据写回全部相关 context;输出 `executed` / `reused` / `failed` 计数。
|
|
15
|
+
2. **E2E 验收场景**:依赖外部环境(数据库、API、浏览器等)、编码阶段标记 `e2e_deferred` 的场景。
|
|
16
|
+
|
|
17
|
+
失败不写 verified、不反转已 done 的任务状态;归档前必须 `decision=pass`。未声明 review verifier 且无 E2E 场景时为空操作(`reason=nothing_to_verify`)。
|
|
13
18
|
|
|
14
19
|
## 调用方式
|
|
15
20
|
|
|
@@ -41,6 +41,8 @@ description: 归档已完成的 task 文件
|
|
|
41
41
|
|
|
42
42
|
- 需求目录必须存在 `spec-context.yml`;执行 `python3 .code-flow/scripts/cf_spec_context.py refresh --task-dir <需求目录> --root "$PWD" --json`
|
|
43
43
|
- 执行 `python3 .code-flow/scripts/cf_spec_gate.py --task-dir <需求目录> --stage code --json`;required Rule 的 stale/pending/conflict/unverified 任一存在即 FAIL
|
|
44
|
+
- 执行 review 终验:`python3 .code-flow/scripts/cf_task_workflow.py verify-e2e --task-dir <需求目录> --root "$PWD" --json`(聚合需求目录全部 task context 的 `stage: review` verifier + acceptance E2E,按 spec/rule 去重执行一次并写回证据);`decision=pass` 才继续,失败保持阻断
|
|
45
|
+
- 执行 `python3 .code-flow/scripts/cf_spec_gate.py --task-dir <需求目录> --stage review --json`;required Rule 存在 unverified review 证据即 FAIL(先完成 review 终验)
|
|
44
46
|
- 若 `.code-flow/.active-task.json` 仍指向本需求,先执行 `python3 .code-flow/scripts/cf_task_workflow.py finish --root "$PWD" --task-dir "<需求目录>" --task TASK-001 --json`,通过后再 verify-e2e;不得自报 gate_passed 或通过归档绕过 active 状态
|
|
45
47
|
- Context 与 Evidence 随需求目录一并归档,`_session` 仅是可重建投影,不是事实源
|
|
46
48
|
|
|
@@ -214,6 +214,23 @@ python3 .code-flow/scripts/cf_task_parallel.py prepare --root "$PWD" \
|
|
|
214
214
|
4. 完成时在 worktree 内提交全部改动(含任务文件 Checklist/Evidence/Status 更新),提交信息 `cf-task(<TASK-ID>): <标题>`。
|
|
215
215
|
5. 返回摘要:TASK-ID、Status、验收命令及结果、提交 SHA、遗留问题。
|
|
216
216
|
|
|
217
|
+
标准 worker prompt 模板(替换占位符后派发):
|
|
218
|
+
|
|
219
|
+
```
|
|
220
|
+
你在隔离 worktree 中执行 <TASK-ID>:<worktree 绝对路径>。
|
|
221
|
+
1) cd 到 worktree;先读 Spec Session(.code-flow/specs/_session/task-<name>.md)、任务文件的当前 TASK 段落与 design 来源章节;
|
|
222
|
+
2) cf_spec_context.py start → 写验收测试记录 RED → 实现 → GREEN;
|
|
223
|
+
3) 运行 cf_task_workflow.py finish --root "$PWD" --task <TASK-ID> --json;decision=pass 后【再】提交全部改动(含 finish 回写的 Evidence/状态):git add -A && git commit -m "cf-task(<TASK-ID>): <标题>";
|
|
224
|
+
4) 返回摘要:TASK-ID、Status、验收命令与结果、commit SHA、遗留问题。
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
> 提交必须在 finish 之后:finish 通过后会回写 Evidence/状态;collect 默认会自动提交这些回写,但显式提交更清晰。
|
|
228
|
+
|
|
229
|
+
子 agent 中断(取消/超时)时不要重新 prepare:检查该 worktree 的 `git status`、`git log <base>..HEAD` 与 marker——
|
|
230
|
+
- 无改动且无 marker:直接接管,按单任务模式继续;
|
|
231
|
+
- 有未提交改动或已有提交但未 done:在 worktree 内接管收尾(补实现/测试并 finish),再走 4.3 collect;
|
|
232
|
+
- 已 done 且 marker 已清理:直接进入 4.3。
|
|
233
|
+
|
|
217
234
|
平台不支持子 agent 时,不建 worktree,直接走 4.6 串行回退。
|
|
218
235
|
|
|
219
236
|
#### 4.3 收集与校验
|
|
@@ -223,6 +240,7 @@ python3 .code-flow/scripts/cf_task_parallel.py collect --root "$PWD" --run-id <r
|
|
|
223
240
|
```
|
|
224
241
|
|
|
225
242
|
- 每个任务必须 `ok: true`(改动已提交、Status 为 done/verified、marker 已清理、有提交)。
|
|
243
|
+
- `collect` 在任务 done/verified 且 marker 已清理后,会自动提交 finish 回写的 Evidence/状态(`--no-commit` 关闭并回到严格模式);finish 之外的未提交改动仍应人工确认。
|
|
226
244
|
- 任一任务失败:停止本批次,不合并;保留 worktree 并列出失败原因。修复后重跑 collect;确认放弃时执行 4.5 cleanup。
|
|
227
245
|
|
|
228
246
|
#### 4.4 回并主分支
|
|
@@ -245,6 +263,7 @@ git merge --no-ff -m "merge cf-task <TASK-ID>" <branch>
|
|
|
245
263
|
```
|
|
246
264
|
|
|
247
265
|
- 仍出现冲突(主区期间有新合并):按双方意图解决并重跑双方 functional 验收;通过后提交 merge。
|
|
266
|
+
- 状态文件冲突(`spec-context.yml` / 任务 md 的覆盖状态列 / `.acceptance-manifest.json`)按确定性优先级解决:覆盖状态列取并集(两边各自 verified 的行保留 verified);`spec-context.yml` 取已包含全量规则证据的一侧;manifest 多数情况按行自动合并,冲突时以任务文件为准重建后重跑 functional 场景。解决后必须 `cf_spec_context.py refresh` 收敛 hash,并重跑合并双方的 functional 验收与 `cf_spec_gate --stage code --json`,通过后提交 merge。
|
|
248
267
|
- 合并后验收失败:记录合并前 HEAD 并 `git reset --hard <合并前HEAD>`(分支与 worktree 原样保留),回到 4.2 让对应子 agent 修复后重新 collect / rebase / 合并。
|
|
249
268
|
|
|
250
269
|
3. 全部任务合并完成后进入 4.5 清理。
|
|
@@ -9,7 +9,12 @@ globs: []
|
|
|
9
9
|
|
|
10
10
|
## 使用场景
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
本命令是**需求级终验**入口,在所有子任务的 functional 测试通过后统一执行:
|
|
13
|
+
|
|
14
|
+
1. **review 层 verifier**:绑定规范中 `stage: review` 的 required verifier(e2e/acceptance/构建等重型验证)。遍历需求目录全部 task context,按 spec/rule 去重执行一次,证据写回全部相关 context;输出 `executed` / `reused` / `failed` 计数。
|
|
15
|
+
2. **E2E 验收场景**:依赖外部环境(数据库、API、浏览器等)、编码阶段标记 `e2e_deferred` 的场景。
|
|
16
|
+
|
|
17
|
+
失败不写 verified、不反转已 done 的任务状态;归档前必须 `decision=pass`。未声明 review verifier 且无 E2E 场景时为空操作(`reason=nothing_to_verify`)。
|
|
13
18
|
|
|
14
19
|
## 调用方式
|
|
15
20
|
|
|
@@ -101,6 +101,7 @@ class RuleBinding:
|
|
|
101
101
|
enforcement: str
|
|
102
102
|
verifier_ref: str
|
|
103
103
|
stage_status: Mapping[str, RuleStageStatus]
|
|
104
|
+
verifier_stage: str = "code"
|
|
104
105
|
|
|
105
106
|
|
|
106
107
|
@dataclass(frozen=True)
|
|
@@ -806,6 +807,7 @@ def _rule_from_data(value: object, path: str) -> RuleBinding:
|
|
|
806
807
|
_string(data.get("enforcement"), "rules[].enforcement", path),
|
|
807
808
|
_string(data.get("verifier_ref"), "rules[].verifier_ref", path),
|
|
808
809
|
parsed,
|
|
810
|
+
_string(data.get("verifier_stage", "code"), "rules[].verifier_stage", path),
|
|
809
811
|
)
|
|
810
812
|
|
|
811
813
|
|
|
@@ -816,6 +818,7 @@ def _rule_data(rule: RuleBinding) -> dict[str, object]:
|
|
|
816
818
|
"text_sha256": rule.text_sha256,
|
|
817
819
|
"enforcement": rule.enforcement,
|
|
818
820
|
"verifier_ref": rule.verifier_ref,
|
|
821
|
+
"verifier_stage": rule.verifier_stage,
|
|
819
822
|
"stage_status": {stage: _stage_data(status) for stage, status in rule.stage_status.items()},
|
|
820
823
|
}
|
|
821
824
|
|
|
@@ -990,7 +993,12 @@ def _binding_from_input(selection: BindingInput) -> SpecBinding:
|
|
|
990
993
|
}
|
|
991
994
|
verifier = verifier_by_rule.get(rule.ref)
|
|
992
995
|
verifier_ref = f"{candidate.spec_id}#{rule.ref}" if verifier is not None else "advisory:none"
|
|
993
|
-
rules.append(
|
|
996
|
+
rules.append(
|
|
997
|
+
RuleBinding(
|
|
998
|
+
rule.ref, rule.text, rule.text_sha256, enforcement, verifier_ref, statuses,
|
|
999
|
+
verifier.stage if verifier is not None else "code",
|
|
1000
|
+
)
|
|
1001
|
+
)
|
|
994
1002
|
return SpecBinding(
|
|
995
1003
|
candidate.spec_id,
|
|
996
1004
|
candidate.path,
|
|
@@ -1003,12 +1011,26 @@ def _binding_from_input(selection: BindingInput) -> SpecBinding:
|
|
|
1003
1011
|
)
|
|
1004
1012
|
|
|
1005
1013
|
|
|
1014
|
+
def _with_declared_stages(rule: RuleBinding, stages: Sequence[str]) -> RuleBinding:
|
|
1015
|
+
"""兼容补齐:绑定旧版本时声明的 stages 缺状态,按声明补 pending。"""
|
|
1016
|
+
missing = [stage for stage in stages if stage not in rule.stage_status]
|
|
1017
|
+
if not missing:
|
|
1018
|
+
return rule
|
|
1019
|
+
statuses = dict(rule.stage_status)
|
|
1020
|
+
for stage in missing:
|
|
1021
|
+
statuses[stage] = RuleStageStatus("pending", (), None, ())
|
|
1022
|
+
return replace(rule, stage_status=statuses)
|
|
1023
|
+
|
|
1024
|
+
|
|
1006
1025
|
def _rule_from_metadata(spec_id: str, metadata: SpecMetadata, rule: SpecRule) -> RuleBinding:
|
|
1007
1026
|
enforcement = "advisory" if metadata.enforcement == "advisory" else rule.enforcement
|
|
1008
1027
|
statuses = {stage: RuleStageStatus("pending", (), None, ()) for stage in metadata.stages}
|
|
1009
|
-
|
|
1010
|
-
verifier_ref = f"{spec_id}#{rule.ref}" if
|
|
1011
|
-
return RuleBinding(
|
|
1028
|
+
verifier = next((item for item in metadata.verifiers if item.rule == rule.ref), None)
|
|
1029
|
+
verifier_ref = f"{spec_id}#{rule.ref}" if verifier is not None else "advisory:none"
|
|
1030
|
+
return RuleBinding(
|
|
1031
|
+
rule.ref, rule.text, rule.text_sha256, enforcement, verifier_ref, statuses,
|
|
1032
|
+
verifier.stage if verifier is not None else "code",
|
|
1033
|
+
)
|
|
1012
1034
|
|
|
1013
1035
|
|
|
1014
1036
|
def bind_specs(context: SpecContext, selections: Sequence[BindingInput]) -> SpecContext:
|
|
@@ -1156,14 +1178,15 @@ def _refresh_existing_rule(
|
|
|
1156
1178
|
binding: SpecBinding, existing: RuleBinding, current: SpecRule, metadata: SpecMetadata
|
|
1157
1179
|
) -> tuple[RuleBinding, Optional[DriftChange]]:
|
|
1158
1180
|
enforcement = "advisory" if metadata.enforcement == "advisory" else current.enforcement
|
|
1159
|
-
|
|
1160
|
-
verifier_ref = f"{binding.spec_id}#{current.ref}" if
|
|
1181
|
+
verifier = next((item for item in metadata.verifiers if item.rule == current.ref), None)
|
|
1182
|
+
verifier_ref = f"{binding.spec_id}#{current.ref}" if verifier is not None else "advisory:none"
|
|
1161
1183
|
updated = replace(
|
|
1162
1184
|
existing,
|
|
1163
1185
|
summary=current.text,
|
|
1164
1186
|
text_sha256=current.text_sha256,
|
|
1165
1187
|
enforcement=enforcement,
|
|
1166
1188
|
verifier_ref=verifier_ref,
|
|
1189
|
+
verifier_stage=verifier.stage if verifier is not None else "code",
|
|
1167
1190
|
)
|
|
1168
1191
|
if existing.text_sha256 == current.text_sha256:
|
|
1169
1192
|
return updated, None
|
|
@@ -1200,6 +1223,7 @@ def _refresh_binding(
|
|
|
1200
1223
|
rule, change = _refresh_existing_rule(binding, prior, current, metadata)
|
|
1201
1224
|
if change is not None:
|
|
1202
1225
|
changes.append(change)
|
|
1226
|
+
rule = _with_declared_stages(rule, metadata.stages)
|
|
1203
1227
|
rule, artifact_changes = _refresh_artifacts(binding.spec_id, rule, artifact_root)
|
|
1204
1228
|
rules.append(rule)
|
|
1205
1229
|
changes.extend(artifact_changes)
|
|
@@ -1571,11 +1595,13 @@ def _status_command(args: argparse.Namespace) -> dict[str, object]:
|
|
|
1571
1595
|
rules = []
|
|
1572
1596
|
for rule in binding.rules:
|
|
1573
1597
|
status = rule.stage_status.get("code")
|
|
1598
|
+
stages = {stage: item.status for stage, item in sorted(rule.stage_status.items())}
|
|
1574
1599
|
rules.append(
|
|
1575
1600
|
{
|
|
1576
1601
|
"ref": f"{binding.spec_id}#{rule.ref}",
|
|
1577
1602
|
"enforcement": rule.enforcement,
|
|
1578
1603
|
"status": status.status if status else "missing",
|
|
1604
|
+
"stages": stages,
|
|
1579
1605
|
"verifier": rule.verifier_ref,
|
|
1580
1606
|
}
|
|
1581
1607
|
)
|
|
@@ -1609,7 +1635,8 @@ def _status_text(data: dict[str, object]) -> str:
|
|
|
1609
1635
|
for binding in data["bindings"]:
|
|
1610
1636
|
lines.append(f"- {binding['spec_id']}({binding['path']})— {binding['status']}")
|
|
1611
1637
|
for rule in binding["rules"]:
|
|
1612
|
-
|
|
1638
|
+
stage_text = " ".join(f"{stage}={value}" for stage, value in rule.get("stages", {}).items())
|
|
1639
|
+
lines.append(f" {stage_text:<24} {rule['ref']}({rule['verifier']})")
|
|
1613
1640
|
return "\n".join(lines)
|
|
1614
1641
|
|
|
1615
1642
|
|
|
@@ -116,6 +116,8 @@ def _binding_issues(
|
|
|
116
116
|
status = rule.stage_status.get(stage)
|
|
117
117
|
if status is None:
|
|
118
118
|
continue
|
|
119
|
+
if stage in ("code", "review") and getattr(rule, "verifier_stage", "code") != stage:
|
|
120
|
+
continue
|
|
119
121
|
if rule.enforcement == "advisory" and status.status == "pending":
|
|
120
122
|
continue
|
|
121
123
|
code = "missing_spec" if binding.status == "missing" else _status_issue(rule, status, now, diff_sha256, allow_cheap_skips)
|
|
@@ -15,6 +15,7 @@ from typing import Mapping, Optional
|
|
|
15
15
|
ALLOWED_STAGES = frozenset(("prd", "design", "plan", "code", "review"))
|
|
16
16
|
ALLOWED_ENFORCEMENT = frozenset(("required", "advisory"))
|
|
17
17
|
ALLOWED_VERIFIERS = frozenset(("document", "regex", "ast", "command", "test", "manual"))
|
|
18
|
+
ALLOWED_VERIFIER_STAGES = frozenset(("code", "review"))
|
|
18
19
|
_ID_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
|
19
20
|
_RULE_ID_RE = re.compile(r"^RULE-[a-z0-9]+(?:-[a-z0-9]+)*-\d{3}$")
|
|
20
21
|
_EXPLICIT_RULE_RE = re.compile(r"^\[(RULE-[a-z0-9-]+-\d{3})\]\s+(.+)$")
|
|
@@ -52,6 +53,8 @@ class SpecVerifier:
|
|
|
52
53
|
rule: str
|
|
53
54
|
type: str
|
|
54
55
|
config: Mapping[str, object]
|
|
56
|
+
stage: str = "code"
|
|
57
|
+
files: tuple[str, ...] = ()
|
|
55
58
|
|
|
56
59
|
|
|
57
60
|
@dataclass(frozen=True)
|
|
@@ -209,7 +212,26 @@ def _validate_checks(meta: Mapping[str, object], path: str, raw: str) -> tuple[M
|
|
|
209
212
|
return tuple(value)
|
|
210
213
|
|
|
211
214
|
|
|
212
|
-
def
|
|
215
|
+
def _parse_verifier_files(value: object, path: str, raw: str, index: int) -> tuple[str, ...]:
|
|
216
|
+
line = _verifier_field_line(raw, index, "files")
|
|
217
|
+
if not isinstance(value, list) or not value:
|
|
218
|
+
raise SpecMetadataError(path, f"verifiers[{index}].files", line, "必须是非空 glob 字符串数组")
|
|
219
|
+
files: list[str] = []
|
|
220
|
+
for item in value:
|
|
221
|
+
if not isinstance(item, str) or not item.strip():
|
|
222
|
+
raise SpecMetadataError(path, f"verifiers[{index}].files", line, "glob 必须是非空字符串")
|
|
223
|
+
text = item.strip().replace("\\", "/")
|
|
224
|
+
if text.startswith("/") or re.match(r"^[A-Za-z]:/", text):
|
|
225
|
+
raise SpecMetadataError(path, f"verifiers[{index}].files", line, f"禁止绝对路径: {item!r}")
|
|
226
|
+
if ".." in text.split("/"):
|
|
227
|
+
raise SpecMetadataError(path, f"verifiers[{index}].files", line, f"禁止 .. 跳出仓库: {item!r}")
|
|
228
|
+
files.append(text)
|
|
229
|
+
return tuple(files)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _parse_verifiers(
|
|
233
|
+
meta: Mapping[str, object], path: str, raw: str, stages: tuple[str, ...]
|
|
234
|
+
) -> tuple[SpecVerifier, ...]:
|
|
213
235
|
value = meta.get("verifiers", [])
|
|
214
236
|
if not isinstance(value, list) or not all(isinstance(item, dict) for item in value):
|
|
215
237
|
raise SpecMetadataError(path, "verifiers", _field_line(raw, "verifiers"), "必须是 mapping 列表")
|
|
@@ -219,6 +241,8 @@ def _parse_verifiers(meta: Mapping[str, object], path: str, raw: str) -> tuple[S
|
|
|
219
241
|
rule = item.get("rule")
|
|
220
242
|
kind = item.get("type")
|
|
221
243
|
config = item.get("config")
|
|
244
|
+
explicit_stage = "stage" in item
|
|
245
|
+
stage = item.get("stage", "code")
|
|
222
246
|
if not isinstance(rule, str) or not _RULE_ID_RE.fullmatch(rule):
|
|
223
247
|
line = _verifier_field_line(raw, index, "rule")
|
|
224
248
|
raise SpecMetadataError(path, f"verifiers[{index}].rule", line, "必须是稳定 RULE ID")
|
|
@@ -228,10 +252,17 @@ def _parse_verifiers(meta: Mapping[str, object], path: str, raw: str) -> tuple[S
|
|
|
228
252
|
if not isinstance(config, dict):
|
|
229
253
|
line = _verifier_field_line(raw, index, "config")
|
|
230
254
|
raise SpecMetadataError(path, f"verifiers[{index}].config", line, "必须是 mapping")
|
|
255
|
+
if explicit_stage and (not isinstance(stage, str) or stage not in ALLOWED_VERIFIER_STAGES):
|
|
256
|
+
line = _verifier_field_line(raw, index, "stage")
|
|
257
|
+
raise SpecMetadataError(path, f"verifiers[{index}].stage", line, f"必须是 {sorted(ALLOWED_VERIFIER_STAGES)} 之一")
|
|
258
|
+
if explicit_stage and stage not in stages:
|
|
259
|
+
line = _verifier_field_line(raw, index, "stage")
|
|
260
|
+
raise SpecMetadataError(path, f"verifiers[{index}].stage", line, f"stage {stage!r} 未在 stages 声明: {', '.join(stages)}")
|
|
261
|
+
files = _parse_verifier_files(item["files"], path, raw, index) if "files" in item else ()
|
|
231
262
|
if rule in seen:
|
|
232
263
|
raise SpecMetadataError(path, "verifiers", _field_line(raw, "verifiers"), f"重复 verifier: {rule}")
|
|
233
264
|
seen.add(rule)
|
|
234
|
-
result.append(SpecVerifier(rule=rule, type=kind, config=config))
|
|
265
|
+
result.append(SpecVerifier(rule=rule, type=kind, config=config, stage=stage, files=files))
|
|
235
266
|
return tuple(result)
|
|
236
267
|
|
|
237
268
|
|
|
@@ -324,7 +355,7 @@ def parse_spec_metadata(content: str, path: str = "<memory>", raw_bytes: Optiona
|
|
|
324
355
|
if owner is not None and (not isinstance(owner, str) or not owner.strip()):
|
|
325
356
|
raise SpecMetadataError(path, "owner", _field_line(raw_frontmatter, "owner"), "必须是非空字符串")
|
|
326
357
|
checks = _validate_checks(meta, path, raw_frontmatter)
|
|
327
|
-
verifiers = _parse_verifiers(meta, path, raw_frontmatter)
|
|
358
|
+
verifiers = _parse_verifiers(meta, path, raw_frontmatter, header.stages)
|
|
328
359
|
body_line_offset = raw_frontmatter.count("\n") + 3
|
|
329
360
|
rules = _parse_rules(body, path, body_line_offset)
|
|
330
361
|
_validate_links(rules, verifiers, path, raw_frontmatter)
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import ast
|
|
7
|
-
from dataclasses import dataclass
|
|
7
|
+
from dataclasses import dataclass, replace
|
|
8
8
|
from datetime import datetime, timezone
|
|
9
9
|
import fnmatch
|
|
10
10
|
import hashlib
|
|
@@ -59,19 +59,94 @@ def _cache_file(root: str) -> Path:
|
|
|
59
59
|
return Path(root) / ".code-flow" / ".verifier-cache.json"
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
def _cache_key(metadata: SpecMetadata, rule: SpecRule, verifier: SpecVerifier, scope: VerificationScope, confirmation: Optional[Mapping[str, object]]) -> str:
|
|
62
|
+
def _cache_key(metadata: SpecMetadata, rule: SpecRule, verifier: SpecVerifier, scope: VerificationScope, confirmation: Optional[Mapping[str, object]], stage: str = "code") -> str:
|
|
63
63
|
payload = {
|
|
64
64
|
"spec": metadata.hashes.file_sha256,
|
|
65
65
|
"rule": rule.text_sha256,
|
|
66
66
|
"verifier": {"type": verifier.type, "config": verifier.config},
|
|
67
67
|
"files": scope.files,
|
|
68
68
|
"diff": scope.diff_sha256,
|
|
69
|
+
"stage": stage,
|
|
69
70
|
"confirmation": confirmation or {},
|
|
70
71
|
"artifact_content": _artifact_fingerprint(verifier, scope),
|
|
71
72
|
}
|
|
72
73
|
return hashlib.sha256(json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str).encode()).hexdigest()
|
|
73
74
|
|
|
74
75
|
|
|
76
|
+
def _normalize_scope_path(value: str) -> str:
|
|
77
|
+
return value.replace("\\", "/").strip()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _matches_scope(path: str, patterns: Sequence[str]) -> bool:
|
|
81
|
+
for pattern in patterns:
|
|
82
|
+
if fnmatch.fnmatchcase(path, pattern):
|
|
83
|
+
return True
|
|
84
|
+
bare = pattern.rstrip("/")
|
|
85
|
+
if not any(char in pattern for char in "*?[") and path.startswith(bare + "/"):
|
|
86
|
+
return True
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _git_tracked_files(root: str) -> tuple[str, ...]:
|
|
91
|
+
try:
|
|
92
|
+
outcome = subprocess.run(
|
|
93
|
+
("git", "ls-files"), cwd=root, text=True, encoding="utf-8", capture_output=True, check=False
|
|
94
|
+
)
|
|
95
|
+
except OSError:
|
|
96
|
+
return ()
|
|
97
|
+
if outcome.returncode != 0:
|
|
98
|
+
return ()
|
|
99
|
+
return tuple(_normalize_scope_path(line) for line in outcome.stdout.splitlines() if line.strip())
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def scoped_files(verifier: SpecVerifier, scope: VerificationScope) -> tuple[str, ...]:
|
|
103
|
+
"""作用域命中的文件集合:git tracked ∪ 任务持有文件,按 verifier.files 过滤。
|
|
104
|
+
|
|
105
|
+
决议仓库级集合(而非仅当前任务 diff)才能让未触及作用域的其他任务命中缓存。
|
|
106
|
+
"""
|
|
107
|
+
if not verifier.files:
|
|
108
|
+
return ()
|
|
109
|
+
candidates = {_normalize_scope_path(item) for item in scope.files if item.strip()}
|
|
110
|
+
candidates.update(_git_tracked_files(scope.root))
|
|
111
|
+
return tuple(sorted(path for path in candidates if _matches_scope(path, verifier.files)))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _scope_fingerprint(root: str, files: Sequence[str]) -> str:
|
|
115
|
+
digest = hashlib.sha256()
|
|
116
|
+
for relative in files:
|
|
117
|
+
path = Path(root) / relative
|
|
118
|
+
digest.update(relative.encode("utf-8"))
|
|
119
|
+
try:
|
|
120
|
+
digest.update(path.read_bytes() if path.is_file() else b"<deleted>")
|
|
121
|
+
except OSError:
|
|
122
|
+
digest.update(b"<unreadable>")
|
|
123
|
+
return digest.hexdigest()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _scoped_cache_key(
|
|
127
|
+
metadata: SpecMetadata,
|
|
128
|
+
rule: SpecRule,
|
|
129
|
+
verifier: SpecVerifier,
|
|
130
|
+
scope: VerificationScope,
|
|
131
|
+
confirmation: Optional[Mapping[str, object]],
|
|
132
|
+
stage: str,
|
|
133
|
+
) -> Optional[str]:
|
|
134
|
+
"""command/test 仅在声明 files 作用域时缓存;未声明保持全量执行。"""
|
|
135
|
+
if verifier.type not in ("command", "test") or not verifier.files:
|
|
136
|
+
return None
|
|
137
|
+
files = scoped_files(verifier, scope)
|
|
138
|
+
payload = {
|
|
139
|
+
"spec": metadata.hashes.file_sha256,
|
|
140
|
+
"rule": rule.text_sha256,
|
|
141
|
+
"verifier": {"type": verifier.type, "config": verifier.config, "files": list(verifier.files)},
|
|
142
|
+
"stage": stage,
|
|
143
|
+
"scope_files": files,
|
|
144
|
+
"scope_sha256": _scope_fingerprint(scope.root, files),
|
|
145
|
+
"confirmation": confirmation or {},
|
|
146
|
+
}
|
|
147
|
+
return hashlib.sha256(json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str).encode()).hexdigest()
|
|
148
|
+
|
|
149
|
+
|
|
75
150
|
def _artifact_fingerprint(verifier: SpecVerifier, scope: VerificationScope) -> Optional[str]:
|
|
76
151
|
if verifier.type != "document":
|
|
77
152
|
return None
|
|
@@ -143,13 +218,32 @@ def _cached_or_run(
|
|
|
143
218
|
verifier: SpecVerifier,
|
|
144
219
|
scope: VerificationScope,
|
|
145
220
|
confirmation: Optional[Mapping[str, object]],
|
|
221
|
+
stage: str = "code",
|
|
146
222
|
) -> VerificationEvidence:
|
|
147
223
|
cacheable = verifier.type in _PURE_VERIFIER_TYPES
|
|
148
|
-
|
|
224
|
+
scoped_key: Optional[str] = None
|
|
225
|
+
if cacheable:
|
|
226
|
+
key = _cache_key(metadata, rule, verifier, scope, confirmation, stage)
|
|
227
|
+
else:
|
|
228
|
+
scoped_key = _scoped_cache_key(metadata, rule, verifier, scope, confirmation, stage)
|
|
229
|
+
if scoped_key is None:
|
|
230
|
+
key = ""
|
|
231
|
+
else:
|
|
232
|
+
cacheable, key = True, scoped_key
|
|
149
233
|
if cacheable:
|
|
150
234
|
cached = _cached_evidence(key)
|
|
151
235
|
if cached is not None:
|
|
152
|
-
|
|
236
|
+
if scoped_key is None:
|
|
237
|
+
return cached
|
|
238
|
+
details = dict(cached.details or {})
|
|
239
|
+
details["cache_reused"] = True
|
|
240
|
+
details["scope_files"] = list(scoped_files(verifier, scope))
|
|
241
|
+
return replace(
|
|
242
|
+
cached,
|
|
243
|
+
executed_at=datetime.now(timezone.utc).isoformat(),
|
|
244
|
+
diff_sha256=scope.diff_sha256,
|
|
245
|
+
details=details,
|
|
246
|
+
)
|
|
153
247
|
current = _evidence(metadata, rule, verifier, scope, confirmation)
|
|
154
248
|
if cacheable and current.status == "verified":
|
|
155
249
|
_save_result_cache(key, current)
|
|
@@ -400,6 +494,7 @@ def _run_all_verifiers(
|
|
|
400
494
|
confirmations: Optional[Mapping[str, Mapping[str, object]]] = None,
|
|
401
495
|
skip_command: bool = False,
|
|
402
496
|
timeout_budget: Optional[float] = None,
|
|
497
|
+
stage: str = "code",
|
|
403
498
|
) -> VerificationResult:
|
|
404
499
|
verifier_by_rule = {item.rule: item for item in metadata.verifiers}
|
|
405
500
|
confirmation_by_rule = confirmations or {}
|
|
@@ -410,12 +505,14 @@ def _run_all_verifiers(
|
|
|
410
505
|
for rule in metadata.rules:
|
|
411
506
|
if rule.enforcement != "required":
|
|
412
507
|
continue
|
|
508
|
+
verifier = verifier_by_rule.get(rule.ref)
|
|
509
|
+
if verifier is not None and verifier.stage != stage:
|
|
510
|
+
continue
|
|
413
511
|
if timeout_budget is not None:
|
|
414
512
|
remaining = timeout_budget - (time.monotonic() - started)
|
|
415
513
|
if remaining <= 0:
|
|
416
514
|
evidence_by_ref[rule.ref] = _budget_evidence(metadata, rule, scope, timeout_budget)
|
|
417
515
|
continue
|
|
418
|
-
verifier = verifier_by_rule.get(rule.ref)
|
|
419
516
|
if verifier is None:
|
|
420
517
|
evidence_by_ref[rule.ref] = _missing_evidence(metadata, rule, scope)
|
|
421
518
|
continue
|
|
@@ -427,11 +524,11 @@ def _run_all_verifiers(
|
|
|
427
524
|
parallel.append((rule, verifier, confirmation))
|
|
428
525
|
continue
|
|
429
526
|
cap = None if timeout_budget is None else max(0.1, timeout_budget - (time.monotonic() - started))
|
|
430
|
-
evidence_by_ref[rule.ref] = _cached_or_run(metadata, rule, verifier, scope, confirmation) if cap is None else _evidence(metadata, rule, verifier, scope, confirmation, cap)
|
|
527
|
+
evidence_by_ref[rule.ref] = _cached_or_run(metadata, rule, verifier, scope, confirmation, stage) if cap is None else _evidence(metadata, rule, verifier, scope, confirmation, cap)
|
|
431
528
|
if parallel:
|
|
432
529
|
with ThreadPoolExecutor(max_workers=min(8, len(parallel))) as pool:
|
|
433
530
|
futures = {
|
|
434
|
-
rule.ref: pool.submit(_cached_or_run, metadata, rule, verifier, scope, confirmation)
|
|
531
|
+
rule.ref: pool.submit(_cached_or_run, metadata, rule, verifier, scope, confirmation, stage)
|
|
435
532
|
for rule, verifier, confirmation in parallel
|
|
436
533
|
}
|
|
437
534
|
evidence_by_ref.update({ref: future.result() for ref, future in futures.items()})
|
|
@@ -442,9 +539,10 @@ def _run_all_verifiers(
|
|
|
442
539
|
|
|
443
540
|
def run_all_verifiers(metadata: SpecMetadata, scope: VerificationScope,
|
|
444
541
|
confirmations: Optional[Mapping[str, Mapping[str, object]]] = None,
|
|
445
|
-
skip_command: bool = False, timeout_budget: Optional[float] = None
|
|
542
|
+
skip_command: bool = False, timeout_budget: Optional[float] = None,
|
|
543
|
+
stage: str = "code") -> VerificationResult:
|
|
446
544
|
with execution_session():
|
|
447
|
-
return _run_all_verifiers(metadata, scope, confirmations, skip_command, timeout_budget)
|
|
545
|
+
return _run_all_verifiers(metadata, scope, confirmations, skip_command, timeout_budget, stage)
|
|
448
546
|
|
|
449
547
|
|
|
450
548
|
def evidence_is_fresh(
|
|
@@ -266,20 +266,12 @@ def prepare(root: Path, task_file: str, tasks: Sequence[str], run_id: Optional[s
|
|
|
266
266
|
"base_head": outcome["base_head"], "worktrees": worktrees}
|
|
267
267
|
|
|
268
268
|
|
|
269
|
-
def _collect_one(root: Path, entry: dict[str, object], base_head: str, task_file: str) -> dict[str, object]:
|
|
269
|
+
def _collect_one(root: Path, entry: dict[str, object], base_head: str, task_file: str, commit: bool = True) -> dict[str, object]:
|
|
270
270
|
task = str(entry["task"])
|
|
271
271
|
path = Path(str(entry["path"]))
|
|
272
272
|
branch = str(entry["branch"])
|
|
273
273
|
if not path.is_dir():
|
|
274
274
|
raise ParallelError("worktree_missing", f"{task}: worktree 不存在: {path}")
|
|
275
|
-
dirty = _status_paths(path)
|
|
276
|
-
if dirty:
|
|
277
|
-
raise ParallelError("worktree_dirty", f"{task}: 尚有未提交改动: {', '.join(dirty[:8])}")
|
|
278
|
-
if (path / MARKER).exists():
|
|
279
|
-
raise ParallelError("active_marker_present", f"{task}: marker 未清理,finish 未完成")
|
|
280
|
-
head = _run_git(path, ("rev-parse", "HEAD")).strip()
|
|
281
|
-
if head == base_head:
|
|
282
|
-
raise ParallelError("no_commits", f"{task}: worktree 无新提交")
|
|
283
275
|
try:
|
|
284
276
|
nodes = parse_task_file(str(path / task_file))
|
|
285
277
|
except ValueError as exc:
|
|
@@ -287,12 +279,28 @@ def _collect_one(root: Path, entry: dict[str, object], base_head: str, task_file
|
|
|
287
279
|
status = next((node.status for node in nodes if node.task_id == task), None)
|
|
288
280
|
if status not in FINISHED_STATUSES:
|
|
289
281
|
raise ParallelError("task_not_finished", f"{task}: Status 为 {status},未通过 Done Gate")
|
|
282
|
+
if (path / MARKER).exists():
|
|
283
|
+
raise ParallelError("active_marker_present", f"{task}: marker 未清理,finish 未完成")
|
|
284
|
+
dirty = _status_paths(path)
|
|
285
|
+
if dirty:
|
|
286
|
+
# finish 会在通过后回写 Evidence/状态;任务已 done 且 marker 已清理时,
|
|
287
|
+
# 这些回写由 collect 自动提交(--no-commit 可关闭,保持严格模式)。
|
|
288
|
+
if not commit:
|
|
289
|
+
raise ParallelError(
|
|
290
|
+
"worktree_dirty",
|
|
291
|
+
f"{task}: 尚有未提交改动: {', '.join(dirty[:8])};可在 worktree 内提交后重跑 collect,或去掉 --no-commit",
|
|
292
|
+
)
|
|
293
|
+
_run_git(path, ("add", "-A"))
|
|
294
|
+
_run_git(path, ("commit", "-q", "-m", f"cf-task({task}): finish 回写验收证据与任务状态"))
|
|
295
|
+
head = _run_git(path, ("rev-parse", "HEAD")).strip()
|
|
296
|
+
if head == base_head:
|
|
297
|
+
raise ParallelError("no_commits", f"{task}: worktree 无新提交")
|
|
290
298
|
files = _run_git(root, ("diff", "--name-only", f"{base_head}..{branch}")).splitlines()
|
|
291
299
|
return {"task": task, "branch": branch, "path": str(path), "head": head,
|
|
292
300
|
"status": status, "files": sorted(item for item in files if item)}
|
|
293
301
|
|
|
294
302
|
|
|
295
|
-
def collect(root: Path, run_id: str, tasks: Optional[Sequence[str]]) -> dict[str, object]:
|
|
303
|
+
def collect(root: Path, run_id: str, tasks: Optional[Sequence[str]], commit: bool = True) -> dict[str, object]:
|
|
296
304
|
root = root.resolve()
|
|
297
305
|
meta = _read_run_meta(root, run_id)
|
|
298
306
|
entries = [item for item in meta.get("worktrees", []) if isinstance(item, dict)]
|
|
@@ -302,7 +310,7 @@ def collect(root: Path, run_id: str, tasks: Optional[Sequence[str]]) -> dict[str
|
|
|
302
310
|
results: list[dict[str, object]] = []
|
|
303
311
|
for entry in entries:
|
|
304
312
|
try:
|
|
305
|
-
results.append(_collect_one(root, entry, str(meta["base_head"]), str(meta["task_file"])))
|
|
313
|
+
results.append(_collect_one(root, entry, str(meta["base_head"]), str(meta["task_file"]), commit))
|
|
306
314
|
except ParallelError as exc:
|
|
307
315
|
results.append({"task": str(entry.get("task", "")), "ok": False, "code": exc.code, "message": exc.message})
|
|
308
316
|
ok = bool(results) and all(item.get("code") is None for item in results)
|
|
@@ -363,6 +371,7 @@ def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) ->
|
|
|
363
371
|
for name in ("collect", "cleanup"):
|
|
364
372
|
sub.choices[name].add_argument("--run-id", required=True)
|
|
365
373
|
sub.choices["collect"].add_argument("--tasks", default="")
|
|
374
|
+
sub.choices["collect"].add_argument("--no-commit", action="store_true")
|
|
366
375
|
sub.choices["cleanup"].add_argument("--force", action="store_true")
|
|
367
376
|
args = parser.parse_args(argv)
|
|
368
377
|
root = Path(args.root).resolve()
|
|
@@ -372,7 +381,7 @@ def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) ->
|
|
|
372
381
|
payload = prepare(root, args.task_file, tasks, args.run_id or None)
|
|
373
382
|
elif args.action == "collect":
|
|
374
383
|
tasks = [item.strip() for item in args.tasks.split(",") if item.strip()] or None
|
|
375
|
-
payload = collect(root, args.run_id, tasks)
|
|
384
|
+
payload = collect(root, args.run_id, tasks, commit=not args.no_commit)
|
|
376
385
|
else:
|
|
377
386
|
payload = cleanup(root, args.run_id, args.force)
|
|
378
387
|
except ParallelError as exc:
|
|
@@ -48,6 +48,7 @@ class DoneResult:
|
|
|
48
48
|
files: tuple[str, ...]
|
|
49
49
|
evidence: tuple[Mapping[str, object], ...]
|
|
50
50
|
message: str = ""
|
|
51
|
+
deferred_review: int = 0
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
def _context_path(task_dir: str) -> str:
|
|
@@ -153,10 +154,11 @@ def _evidence_data(evidence: VerificationEvidence) -> Mapping[str, object]:
|
|
|
153
154
|
|
|
154
155
|
|
|
155
156
|
def _update_rule(rule: RuleBinding, evidence: Mapping[str, object]) -> RuleBinding:
|
|
156
|
-
|
|
157
|
+
stage = rule.verifier_stage or "code"
|
|
158
|
+
if stage not in rule.stage_status:
|
|
157
159
|
return rule
|
|
158
160
|
statuses = dict(rule.stage_status)
|
|
159
|
-
current = statuses[
|
|
161
|
+
current = statuses[stage]
|
|
160
162
|
if current.status in ("not_applicable", "waived"):
|
|
161
163
|
return rule
|
|
162
164
|
status = "verified" if evidence.get("status") == "verified" else "unverified"
|
|
@@ -168,9 +170,9 @@ def _update_rule(rule: RuleBinding, evidence: Mapping[str, object]) -> RuleBindi
|
|
|
168
170
|
(item.get("verifier_ref"), item.get("result_sha256"), item.get("diff_sha256")) == signature
|
|
169
171
|
for item in current.evidence
|
|
170
172
|
):
|
|
171
|
-
statuses[
|
|
173
|
+
statuses[stage] = replace(current, status=status)
|
|
172
174
|
else:
|
|
173
|
-
statuses[
|
|
175
|
+
statuses[stage] = replace(current, status=status, evidence=(*current.evidence, evidence))
|
|
174
176
|
return replace(rule, stage_status=statuses)
|
|
175
177
|
|
|
176
178
|
|
|
@@ -267,9 +269,11 @@ def _run_done_gate(root: str, task_dir: str, cheap: bool = False, budget: Option
|
|
|
267
269
|
diff_hash = _diff_hash(root, scope_result.files)
|
|
268
270
|
phase_timing("done.load_context_and_diff", phase_started)
|
|
269
271
|
all_evidence: list[Mapping[str, object]] = []
|
|
272
|
+
deferred_review = 0
|
|
270
273
|
for binding in context.bindings:
|
|
271
274
|
phase_started = time.monotonic()
|
|
272
275
|
metadata = load_spec_metadata(str(Path(root) / ".code-flow/specs" / binding.path))
|
|
276
|
+
deferred_review += sum(1 for rule in binding.rules if rule.verifier_stage == "review")
|
|
273
277
|
confirmations: dict[str, Mapping[str, object]] = {}
|
|
274
278
|
for rule in binding.rules:
|
|
275
279
|
confirmation = _rule_manual_confirmation(rule)
|
|
@@ -277,7 +281,8 @@ def _run_done_gate(root: str, task_dir: str, cheap: bool = False, budget: Option
|
|
|
277
281
|
confirmations[rule.ref] = confirmation
|
|
278
282
|
remaining = None if budget is None else budget - (time.monotonic() - started)
|
|
279
283
|
result = run_all_verifiers(
|
|
280
|
-
metadata, VerificationScope(root, scope_result.files, diff_hash), confirmations, cheap, remaining
|
|
284
|
+
metadata, VerificationScope(root, scope_result.files, diff_hash), confirmations, cheap, remaining,
|
|
285
|
+
stage="code",
|
|
281
286
|
)
|
|
282
287
|
phase_timing(f"done.verify.{binding.spec_id}", phase_started)
|
|
283
288
|
all_evidence.extend(_evidence_data(item) for item in result.evidence)
|
|
@@ -288,10 +293,10 @@ def _run_done_gate(root: str, task_dir: str, cheap: bool = False, budget: Option
|
|
|
288
293
|
if gate.decision == "pass" and not cheap:
|
|
289
294
|
validation_issue = _run_finish_validation(root, scope_result.files)
|
|
290
295
|
if validation_issue:
|
|
291
|
-
return DoneResult("block", scope_result.files, tuple(all_evidence), validation_issue)
|
|
296
|
+
return DoneResult("block", scope_result.files, tuple(all_evidence), validation_issue, deferred_review)
|
|
292
297
|
phase_timing("done.total", started)
|
|
293
298
|
return DoneResult(gate.decision, scope_result.files, tuple(all_evidence),
|
|
294
|
-
"; ".join(issue.message for issue in gate.errors))
|
|
299
|
+
"; ".join(issue.message for issue in gate.errors), deferred_review)
|
|
295
300
|
|
|
296
301
|
|
|
297
302
|
def run_done_gate(root: str, task_dir: str, cheap: bool = False, budget: Optional[float] = None, include_e2e: bool = False, task_id: str = "") -> DoneResult:
|
|
@@ -11,8 +11,12 @@ import sys
|
|
|
11
11
|
|
|
12
12
|
from cf_acceptance_runner import run_manifest
|
|
13
13
|
from cf_acceptance_schema import load_manifest, validate_execution_baseline, verified_evidence
|
|
14
|
+
from cf_spec_context import ContextError, _git_changes, _run_git, load_context, save_context
|
|
15
|
+
from cf_spec_gate import validate_stage
|
|
16
|
+
from cf_spec_metadata import load_spec_metadata
|
|
17
|
+
from cf_spec_verify import VerificationScope, _git_tracked_files, run_all_verifiers
|
|
14
18
|
from cf_task_index import parse_task_file
|
|
15
|
-
from cf_task_runtime import run_done_gate
|
|
19
|
+
from cf_task_runtime import _apply_evidence, _evidence_data, run_done_gate
|
|
16
20
|
from cf_workflow_service import (FINISHED_STATUSES, _require_marker_task,
|
|
17
21
|
_sync_markdown, block_task, complete_task, locate_task_file, resume_task)
|
|
18
22
|
from cf_workflow_transaction import recover_transition
|
|
@@ -23,7 +27,10 @@ def finish_task(root: str, directory: str, task_file: str, task_id: str) -> dict
|
|
|
23
27
|
gate = run_done_gate(root, directory, task_id=task_id)
|
|
24
28
|
if gate.decision != "pass":
|
|
25
29
|
return {"decision": "block", "reason": gate.message, "evidence": gate.evidence}
|
|
26
|
-
|
|
30
|
+
result = {"decision": "pass", "deferred_review": gate.deferred_review, **complete_task(root, directory, task_file, task_id, True)}
|
|
31
|
+
if gate.deferred_review:
|
|
32
|
+
result["deferred_hint"] = f"{gate.deferred_review} review-layer verifier(s) deferred; run cf-task:verify-e2e for the requirement directory"
|
|
33
|
+
return result
|
|
27
34
|
|
|
28
35
|
|
|
29
36
|
def _demand_tasks(directory: Path) -> list[tuple[Path, str, str]]:
|
|
@@ -44,6 +51,79 @@ def _demand_tasks(directory: Path) -> list[tuple[Path, str, str]]:
|
|
|
44
51
|
return tasks
|
|
45
52
|
|
|
46
53
|
|
|
54
|
+
def _head_token(root: str) -> str:
|
|
55
|
+
try:
|
|
56
|
+
return "head:" + _run_git(root, ("rev-parse", "HEAD")).strip()
|
|
57
|
+
except ContextError:
|
|
58
|
+
return "head:none"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _review_scope(root: str) -> VerificationScope:
|
|
62
|
+
files = set(_git_tracked_files(root))
|
|
63
|
+
try:
|
|
64
|
+
files.update(_git_changes(root))
|
|
65
|
+
except ContextError:
|
|
66
|
+
pass
|
|
67
|
+
return VerificationScope(root, tuple(sorted(files)), _head_token(root))
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def collect_review_bindings(directory: Path) -> dict[str, dict[str, object]]:
|
|
71
|
+
"""收集需求目录下全部 task context 的 review 层绑定。
|
|
72
|
+
|
|
73
|
+
返回 spec_id -> {"path": spec 相对路径, "rules": {rule_ref: [(context_path, context), ...]}}。
|
|
74
|
+
读取失败 fail-closed(报告文件与原因),不允许静默跳过。
|
|
75
|
+
"""
|
|
76
|
+
targets: dict[str, dict[str, object]] = {}
|
|
77
|
+
for ctx_path in sorted(directory.rglob("spec-context.yml")):
|
|
78
|
+
try:
|
|
79
|
+
context = load_context(str(ctx_path))
|
|
80
|
+
except (ContextError, OSError, ValueError) as exc:
|
|
81
|
+
raise ValueError(f"review_context_unreadable: {ctx_path}: {exc}") from exc
|
|
82
|
+
for binding in context.bindings:
|
|
83
|
+
for rule in binding.rules:
|
|
84
|
+
if rule.verifier_stage != "review":
|
|
85
|
+
continue
|
|
86
|
+
entry = targets.setdefault(binding.spec_id, {"path": binding.path, "rules": {}})
|
|
87
|
+
entry["rules"].setdefault(rule.ref, []).append((ctx_path, context))
|
|
88
|
+
return targets
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _run_review_targets(root: str, targets: dict[str, dict[str, object]]) -> dict[str, object]:
|
|
92
|
+
"""执行 review 层 verifier(按 spec/rule 去重),证据写回全部相关 context。"""
|
|
93
|
+
executed = reused = 0
|
|
94
|
+
failures: list[str] = []
|
|
95
|
+
context_paths: set[Path] = set()
|
|
96
|
+
scope = _review_scope(root)
|
|
97
|
+
for spec_id in sorted(targets):
|
|
98
|
+
info = targets[spec_id]
|
|
99
|
+
metadata = load_spec_metadata(str(Path(root) / ".code-flow/specs" / str(info["path"])))
|
|
100
|
+
result = run_all_verifiers(metadata, scope, stage="review")
|
|
101
|
+
by_rule: dict[str, object] = {}
|
|
102
|
+
for evidence in result.evidence:
|
|
103
|
+
rule_ref = evidence.verifier_ref.split("#", 1)[-1]
|
|
104
|
+
by_rule[rule_ref] = evidence
|
|
105
|
+
if evidence.status != "verified":
|
|
106
|
+
failures.append(evidence.verifier_ref)
|
|
107
|
+
elif (evidence.details or {}).get("cache_reused"):
|
|
108
|
+
reused += 1
|
|
109
|
+
else:
|
|
110
|
+
executed += 1
|
|
111
|
+
for rule_ref, entries in info["rules"].items():
|
|
112
|
+
evidence = by_rule.get(rule_ref)
|
|
113
|
+
if evidence is None:
|
|
114
|
+
continue
|
|
115
|
+
payload = (_evidence_data(evidence),)
|
|
116
|
+
for ctx_path, context in entries:
|
|
117
|
+
context_paths.add(ctx_path)
|
|
118
|
+
updated = _apply_evidence(context, payload)
|
|
119
|
+
if updated != context:
|
|
120
|
+
save_context(str(ctx_path), updated)
|
|
121
|
+
for ctx_path in sorted(context_paths):
|
|
122
|
+
gate = validate_stage(load_context(str(ctx_path)), "review", diff_sha256=scope.diff_sha256)
|
|
123
|
+
failures.extend(f"{issue.spec_id}#{issue.rule_ref}" for issue in gate.errors)
|
|
124
|
+
return {"executed": executed, "reused": reused, "failed": sorted(set(failures))}
|
|
125
|
+
|
|
126
|
+
|
|
47
127
|
def verify_e2e(root: str, directory: str) -> dict[str, object]:
|
|
48
128
|
recover_transition(root)
|
|
49
129
|
tasks = _demand_tasks(Path(directory))
|
|
@@ -52,18 +132,36 @@ def verify_e2e(root: str, directory: str) -> dict[str, object]:
|
|
|
52
132
|
return {"decision": "block", "reason": "unfinished_tasks", "tasks": unfinished}
|
|
53
133
|
if (Path(root) / ".code-flow/.active-task.json").exists():
|
|
54
134
|
return {"decision": "block", "reason": "finish_active_task_first"}
|
|
135
|
+
try:
|
|
136
|
+
targets = collect_review_bindings(Path(directory))
|
|
137
|
+
except ValueError as exc:
|
|
138
|
+
return {"decision": "block", "reason": "review_context_unreadable", "detail": str(exc)}
|
|
55
139
|
manifest = Path(directory) / ".acceptance-manifest.json"
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
140
|
+
if not targets and not manifest.exists():
|
|
141
|
+
return {"decision": "pass", "executed": 0, "reused": 0, "failed": [], "reason": "nothing_to_verify"}
|
|
142
|
+
review = _run_review_targets(root, targets) if targets else {"executed": 0, "reused": 0, "failed": []}
|
|
143
|
+
acceptance: dict[str, object] = {"decision": "pass", "results": []}
|
|
144
|
+
if manifest.exists():
|
|
145
|
+
data = load_manifest(manifest)
|
|
146
|
+
validate_execution_baseline(manifest, data)
|
|
147
|
+
bound_task = (manifest.parent / str(data.get("task_file", ""))).resolve()
|
|
148
|
+
if {task.resolve() for task, _, _ in tasks} != {bound_task}:
|
|
149
|
+
return {"decision": "block", "reason": "acceptance_manifest_task_mismatch"}
|
|
150
|
+
required = [row["id"] for row in data["scenarios"] if row.get("kind", "functional") != "e2e"
|
|
151
|
+
and not verified_evidence(row)]
|
|
152
|
+
if required:
|
|
153
|
+
return {"decision": "block", "reason": "functional_or_manual_evidence_missing", "scenarios": required}
|
|
154
|
+
acceptance = run_manifest(str(manifest), root, write_evidence=True, only_e2e=True)
|
|
155
|
+
decision = "pass" if not review["failed"] and acceptance.get("decision") == "pass" else "block"
|
|
156
|
+
result = {
|
|
157
|
+
**acceptance,
|
|
158
|
+
"decision": decision,
|
|
159
|
+
"executed": review["executed"],
|
|
160
|
+
"reused": review["reused"],
|
|
161
|
+
"failed": review["failed"],
|
|
162
|
+
"review": review,
|
|
163
|
+
}
|
|
164
|
+
if decision == "pass":
|
|
67
165
|
for task_file, task_id, _ in tasks:
|
|
68
166
|
_sync_markdown(str(task_file), task_id, "verified")
|
|
69
167
|
return result
|