@michaelxwb/code-flow 0.6.3 → 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.
Files changed (54) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/src/adapters/claude/commands/cf-init.md +1 -0
  4. package/src/adapters/claude/commands/cf-spec.md +7 -3
  5. package/src/adapters/claude/commands/cf-task/archive.md +2 -0
  6. package/src/adapters/claude/commands/cf-task/graph.md +1 -1
  7. package/src/adapters/claude/commands/cf-task/plan.md +2 -2
  8. package/src/adapters/claude/commands/cf-task/start.md +99 -10
  9. package/src/adapters/claude/commands/cf-task/verify-e2e.md +6 -1
  10. package/src/adapters/claude/commands/cf-validate.md +3 -0
  11. package/src/adapters/codex/skills/cf-init/SKILL.md +1 -0
  12. package/src/adapters/codex/skills/cf-spec/SKILL.md +6 -2
  13. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +2 -0
  14. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +1 -1
  15. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +2 -2
  16. package/src/adapters/codex/skills/cf-task-start/SKILL.md +99 -10
  17. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +6 -1
  18. package/src/adapters/codex/skills/cf-validate/SKILL.md +3 -0
  19. package/src/adapters/costrict/commands/cf-init.md +1 -0
  20. package/src/adapters/costrict/commands/cf-spec.md +7 -3
  21. package/src/adapters/costrict/commands/cf-task/archive.md +2 -0
  22. package/src/adapters/costrict/commands/cf-task/graph.md +1 -1
  23. package/src/adapters/costrict/commands/cf-task/plan.md +2 -2
  24. package/src/adapters/costrict/commands/cf-task/start.md +99 -10
  25. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +6 -1
  26. package/src/adapters/costrict/commands/cf-validate.md +3 -0
  27. package/src/adapters/opencode/commands/cf-init.md +9 -4
  28. package/src/adapters/opencode/commands/cf-spec.md +6 -2
  29. package/src/adapters/opencode/commands/cf-task/archive.md +2 -0
  30. package/src/adapters/opencode/commands/cf-task/graph.md +1 -1
  31. package/src/adapters/opencode/commands/cf-task/plan.md +2 -2
  32. package/src/adapters/opencode/commands/cf-task/start.md +99 -10
  33. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +6 -1
  34. package/src/adapters/opencode/commands/cf-validate.md +3 -0
  35. package/src/adapters/opencode/opencode.json +1 -2
  36. package/src/adapters/opencode/plugins/code-flow/index.js +138 -78
  37. package/src/adapters/opencode/plugins/code-flow/package.json +1 -1
  38. package/src/cli.js +51 -1
  39. package/src/core/code-flow/config.yml +2 -1
  40. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +54 -8
  41. package/src/core/code-flow/scripts/cf_core.py +28 -1
  42. package/src/core/code-flow/scripts/cf_post_hook.py +5 -4
  43. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +14 -3
  44. package/src/core/code-flow/scripts/cf_spec_context.py +53 -16
  45. package/src/core/code-flow/scripts/cf_spec_gate.py +28 -4
  46. package/src/core/code-flow/scripts/cf_spec_metadata.py +34 -3
  47. package/src/core/code-flow/scripts/cf_spec_router.py +1 -1
  48. package/src/core/code-flow/scripts/cf_spec_verify.py +107 -9
  49. package/src/core/code-flow/scripts/cf_stop_hook.py +31 -3
  50. package/src/core/code-flow/scripts/cf_task_index.py +5 -4
  51. package/src/core/code-flow/scripts/cf_task_parallel.py +394 -0
  52. package/src/core/code-flow/scripts/cf_task_runtime.py +49 -10
  53. package/src/core/code-flow/scripts/cf_task_workflow.py +111 -13
  54. package/src/core/code-flow/validation.yml +5 -0
@@ -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
- return {"decision": "pass", **complete_task(root, directory, task_file, task_id, True)}
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
- data = load_manifest(manifest)
57
- validate_execution_baseline(manifest, data)
58
- bound_task = (manifest.parent / str(data.get("task_file", ""))).resolve()
59
- if {task.resolve() for task, _, _ in tasks} != {bound_task}:
60
- return {"decision": "block", "reason": "acceptance_manifest_task_mismatch"}
61
- required = [row["id"] for row in data["scenarios"] if row.get("kind", "functional") != "e2e"
62
- and not verified_evidence(row)]
63
- if required:
64
- return {"decision": "block", "reason": "functional_or_manual_evidence_missing", "scenarios": required}
65
- result = run_manifest(str(manifest), root, write_evidence=True, only_e2e=True)
66
- if result["decision"] == "pass":
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
@@ -1,3 +1,6 @@
1
+ # validators 在每次 Stop 按“会话内编辑过的文件”匹配执行(quality_loop.stop_check)。
2
+ # 全量测试 / e2e 等慢命令标 `heavy: true`:Stop 每轮跳过,任务 finish 自动执行
3
+ # 一次,也可显式运行 /cf-validate;避免每个任务都重跑多遍。
1
4
  validators:
2
5
  - name: "Python 语法检查"
3
6
  trigger: "**/*.py"
@@ -33,6 +36,7 @@ validators:
33
36
  trigger: "**/*.{ts,tsx,js,jsx,vue}"
34
37
  command: "npx vitest run --reporter=dot"
35
38
  timeout: 60000
39
+ heavy: true
36
40
  on_fail: "前端测试失败,检查组件渲染和 mock 是否需要更新"
37
41
 
38
42
  - name: "Python 类型检查"
@@ -45,4 +49,5 @@ validators:
45
49
  trigger: "**/*.py"
46
50
  command: "python3 -m pytest --tb=short -q"
47
51
  timeout: 60000
52
+ heavy: true
48
53
  on_fail: "测试失败,检查断言和 mock 是否需要更新"