@michaelxwb/code-flow 0.6.5 → 1.0.0

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 (177) hide show
  1. package/README.md +75 -110
  2. package/bin/code-flow.js +12 -0
  3. package/docs/README.md +19 -0
  4. package/docs/USAGE.md +268 -0
  5. package/docs/v1-design.md +1858 -0
  6. package/docs/v1-implementation.md +186 -0
  7. package/lib/adapters.js +126 -0
  8. package/lib/cli.js +258 -0
  9. package/lib/config.js +85 -0
  10. package/lib/doctor.js +283 -0
  11. package/lib/frontmatter.js +42 -0
  12. package/lib/fs.js +141 -0
  13. package/lib/glob.js +23 -0
  14. package/lib/hook.js +99 -0
  15. package/lib/init.js +132 -0
  16. package/lib/learn.js +170 -0
  17. package/lib/locks.js +52 -0
  18. package/lib/markdown.js +13 -0
  19. package/lib/process.js +116 -0
  20. package/lib/render.js +115 -0
  21. package/lib/repo.js +48 -0
  22. package/lib/runs.js +155 -0
  23. package/lib/session.js +40 -0
  24. package/lib/spec.js +80 -0
  25. package/lib/task.js +258 -0
  26. package/lib/taskdoc.js +308 -0
  27. package/lib/toml.js +140 -0
  28. package/lib/verify.js +168 -0
  29. package/lib/worktree.js +930 -0
  30. package/package.json +21 -9
  31. package/templates/adapters/opencode.mjs +65 -0
  32. package/templates/config.json +39 -0
  33. package/templates/instructions.md +13 -0
  34. package/templates/launcher/bridge.cjs +295 -0
  35. package/templates/launcher/hook.cmd +15 -0
  36. package/templates/launcher/hook.ps1 +10 -0
  37. package/templates/launcher/hook.sh +20 -0
  38. package/templates/skills/cf-check.md +19 -0
  39. package/templates/skills/cf-init.md +16 -0
  40. package/templates/skills/cf-spec.md +20 -0
  41. package/templates/skills/cf-task-align.md +19 -0
  42. package/templates/skills/cf-task-archive.md +20 -0
  43. package/templates/skills/cf-task-plan.md +31 -0
  44. package/templates/skills/cf-task-prd.md +14 -0
  45. package/templates/skills/cf-task-start.md +41 -0
  46. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-frontend.md +22 -19
  47. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +36 -28
  48. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +27 -17
  49. package/{src/core/code-flow/specs/shared/prd-template.md → templates/workflow/prd.md} +1 -1
  50. package/templates/workflow/project-guidelines.md +48 -0
  51. package/templates/workflow/scenarios.md +67 -0
  52. package/templates/workflow/task.md +56 -0
  53. package/docs/migrations/spec-workflow-0.6.0.md +0 -42
  54. package/src/adapters/claude/CLAUDE.md +0 -45
  55. package/src/adapters/claude/commands/cf-init.md +0 -264
  56. package/src/adapters/claude/commands/cf-learn.md +0 -291
  57. package/src/adapters/claude/commands/cf-spec.md +0 -67
  58. package/src/adapters/claude/commands/cf-stats.md +0 -86
  59. package/src/adapters/claude/commands/cf-sync.md +0 -25
  60. package/src/adapters/claude/commands/cf-task/align.md +0 -309
  61. package/src/adapters/claude/commands/cf-task/archive.md +0 -161
  62. package/src/adapters/claude/commands/cf-task/block.md +0 -36
  63. package/src/adapters/claude/commands/cf-task/graph.md +0 -80
  64. package/src/adapters/claude/commands/cf-task/note.md +0 -236
  65. package/src/adapters/claude/commands/cf-task/plan.md +0 -363
  66. package/src/adapters/claude/commands/cf-task/prd.md +0 -162
  67. package/src/adapters/claude/commands/cf-task/start.md +0 -304
  68. package/src/adapters/claude/commands/cf-task/status.md +0 -59
  69. package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -115
  70. package/src/adapters/claude/commands/cf-validate.md +0 -75
  71. package/src/adapters/claude/settings.local.json +0 -50
  72. package/src/adapters/codex/AGENTS.md +0 -45
  73. package/src/adapters/codex/config.toml +0 -3
  74. package/src/adapters/codex/hooks.json +0 -50
  75. package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
  76. package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
  77. package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -60
  78. package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -87
  79. package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -30
  80. package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -310
  81. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -162
  82. package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -37
  83. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -81
  84. package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -233
  85. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -324
  86. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -163
  87. package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -305
  88. package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -60
  89. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -111
  90. package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -76
  91. package/src/adapters/costrict/CLAUDE.md +0 -45
  92. package/src/adapters/costrict/commands/cf-init.md +0 -264
  93. package/src/adapters/costrict/commands/cf-learn.md +0 -291
  94. package/src/adapters/costrict/commands/cf-spec.md +0 -67
  95. package/src/adapters/costrict/commands/cf-stats.md +0 -86
  96. package/src/adapters/costrict/commands/cf-sync.md +0 -25
  97. package/src/adapters/costrict/commands/cf-task/align.md +0 -309
  98. package/src/adapters/costrict/commands/cf-task/archive.md +0 -161
  99. package/src/adapters/costrict/commands/cf-task/block.md +0 -36
  100. package/src/adapters/costrict/commands/cf-task/graph.md +0 -80
  101. package/src/adapters/costrict/commands/cf-task/note.md +0 -236
  102. package/src/adapters/costrict/commands/cf-task/plan.md +0 -363
  103. package/src/adapters/costrict/commands/cf-task/prd.md +0 -162
  104. package/src/adapters/costrict/commands/cf-task/start.md +0 -304
  105. package/src/adapters/costrict/commands/cf-task/status.md +0 -59
  106. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -115
  107. package/src/adapters/costrict/commands/cf-validate.md +0 -75
  108. package/src/adapters/costrict/settings.local.json +0 -50
  109. package/src/adapters/opencode/AGENTS.md +0 -45
  110. package/src/adapters/opencode/commands/cf-init.md +0 -281
  111. package/src/adapters/opencode/commands/cf-learn.md +0 -295
  112. package/src/adapters/opencode/commands/cf-spec.md +0 -59
  113. package/src/adapters/opencode/commands/cf-stats.md +0 -90
  114. package/src/adapters/opencode/commands/cf-sync.md +0 -29
  115. package/src/adapters/opencode/commands/cf-task/align.md +0 -314
  116. package/src/adapters/opencode/commands/cf-task/archive.md +0 -166
  117. package/src/adapters/opencode/commands/cf-task/block.md +0 -40
  118. package/src/adapters/opencode/commands/cf-task/graph.md +0 -84
  119. package/src/adapters/opencode/commands/cf-task/note.md +0 -240
  120. package/src/adapters/opencode/commands/cf-task/plan.md +0 -363
  121. package/src/adapters/opencode/commands/cf-task/prd.md +0 -167
  122. package/src/adapters/opencode/commands/cf-task/start.md +0 -309
  123. package/src/adapters/opencode/commands/cf-task/status.md +0 -63
  124. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -115
  125. package/src/adapters/opencode/commands/cf-validate.md +0 -79
  126. package/src/adapters/opencode/opencode.json +0 -3
  127. package/src/adapters/opencode/plugins/code-flow/index.js +0 -213
  128. package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
  129. package/src/cli.js +0 -1023
  130. package/src/core/code-flow/config.yml +0 -158
  131. package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
  132. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -294
  133. package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -192
  134. package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
  135. package/src/core/code-flow/scripts/cf_checks.py +0 -336
  136. package/src/core/code-flow/scripts/cf_core.py +0 -787
  137. package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
  138. package/src/core/code-flow/scripts/cf_feedback.py +0 -56
  139. package/src/core/code-flow/scripts/cf_log.py +0 -154
  140. package/src/core/code-flow/scripts/cf_post_hook.py +0 -196
  141. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -104
  142. package/src/core/code-flow/scripts/cf_scan.py +0 -295
  143. package/src/core/code-flow/scripts/cf_session_state.py +0 -37
  144. package/src/core/code-flow/scripts/cf_spec_context.py +0 -1746
  145. package/src/core/code-flow/scripts/cf_spec_gate.py +0 -264
  146. package/src/core/code-flow/scripts/cf_spec_metadata.py +0 -384
  147. package/src/core/code-flow/scripts/cf_spec_migrate.py +0 -512
  148. package/src/core/code-flow/scripts/cf_spec_resolver.py +0 -347
  149. package/src/core/code-flow/scripts/cf_spec_router.py +0 -244
  150. package/src/core/code-flow/scripts/cf_spec_session.py +0 -119
  151. package/src/core/code-flow/scripts/cf_spec_verify.py +0 -559
  152. package/src/core/code-flow/scripts/cf_stats.py +0 -506
  153. package/src/core/code-flow/scripts/cf_stop_hook.py +0 -381
  154. package/src/core/code-flow/scripts/cf_sync.py +0 -140
  155. package/src/core/code-flow/scripts/cf_task_index.py +0 -148
  156. package/src/core/code-flow/scripts/cf_task_parallel.py +0 -394
  157. package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
  158. package/src/core/code-flow/scripts/cf_task_state.py +0 -3
  159. package/src/core/code-flow/scripts/cf_task_workflow.py +0 -205
  160. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -178
  161. package/src/core/code-flow/scripts/cf_validation.py +0 -81
  162. package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
  163. package/src/core/code-flow/scripts/cf_workflow_transaction.py +0 -96
  164. package/src/core/code-flow/specs/backend/_map.md +0 -56
  165. package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
  166. package/src/core/code-flow/specs/backend/database.md +0 -51
  167. package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
  168. package/src/core/code-flow/specs/backend/logging.md +0 -50
  169. package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
  170. package/src/core/code-flow/specs/frontend/_map.md +0 -52
  171. package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
  172. package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
  173. package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
  174. package/src/core/code-flow/specs/shared/_map.md +0 -57
  175. package/src/core/code-flow/tasks/.gitkeep +0 -0
  176. package/src/core/code-flow/validation.yml +0 -53
  177. package/src/migrate/spec-workflow.js +0 -238
@@ -1,304 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Active-task scope expansion and hash-bound Done verification."""
3
-
4
- from __future__ import annotations
5
-
6
- from dataclasses import dataclass, replace
7
- import hashlib
8
- import json
9
- from pathlib import Path
10
- import time
11
- from typing import Mapping, Optional
12
-
13
- from cf_spec_context import (
14
- BindingInput,
15
- RuleBinding,
16
- RuleStageStatus,
17
- SpecBinding,
18
- SpecContext,
19
- bind_specs,
20
- current_owned_paths,
21
- load_active_task,
22
- load_context,
23
- pause_active_task,
24
- resync_active_hash,
25
- save_context,
26
- )
27
- from cf_spec_gate import validate_stage
28
- from cf_spec_metadata import load_spec_metadata
29
- from cf_spec_resolver import resolve_candidates, SpecCandidate
30
- from cf_spec_session import context_sha256
31
- from cf_spec_verify import VerificationEvidence, VerificationScope, run_all_verifiers
32
- from cf_core import load_config, phase_timing, resolve_quality_loop
33
- from cf_acceptance_schema import load_manifest, validate_execution_baseline, verified_evidence
34
- from cf_exec_base import execution_session
35
-
36
-
37
- @dataclass(frozen=True)
38
- class ScopeResult:
39
- decision: str
40
- files: tuple[str, ...]
41
- new_specs: tuple[str, ...]
42
- message: str
43
-
44
-
45
- @dataclass(frozen=True)
46
- class DoneResult:
47
- decision: str
48
- files: tuple[str, ...]
49
- evidence: tuple[Mapping[str, object], ...]
50
- message: str = ""
51
- deferred_review: int = 0
52
-
53
-
54
- def _context_path(task_dir: str) -> str:
55
- return str(Path(task_dir) / "spec-context.yml")
56
-
57
-
58
- def _manual_manifest_issue(task_dir: str, owner: str = "") -> str:
59
- path = Path(task_dir) / ".acceptance-manifest.json"
60
- if not path.is_file():
61
- return ""
62
- try:
63
- import json
64
-
65
- data = load_manifest(path)
66
- pending = [
67
- item.get("id", "unknown")
68
- for item in data.get("scenarios", [])
69
- if item.get("kind") == "manual" and not verified_evidence(item)
70
- and (not owner or not isinstance(item.get("owner"), str) or not item.get("owner") or item.get("owner") == owner)
71
- ]
72
- return f"manual 场景未完成: {', '.join(pending)}" if pending else ""
73
- except (OSError, ValueError, TypeError):
74
- return "acceptance manifest invalid"
75
-
76
-
77
- def _acceptance_baseline_issue(task_dir: str, owner: str) -> str:
78
- from cf_workflow_service import locate_task_file
79
- path = Path(task_dir) / ".acceptance-manifest.json"
80
- task = locate_task_file(task_dir, owner)
81
- try:
82
- requires = task is not None and "## Acceptance Coverage" in task.read_text(encoding="utf-8")
83
- if not path.is_file():
84
- return "acceptance_manifest_missing" if requires else ""
85
- data = load_manifest(path)
86
- if requires:
87
- from cf_acceptance_manifest import validate_manifest
88
- valid, reason = validate_manifest(str(task), str(path))
89
- if not valid:
90
- return reason
91
- validate_execution_baseline(path, data)
92
- return ""
93
- except (OSError, ValueError) as exc:
94
- return f"acceptance_manifest_invalid: {exc}"
95
-
96
-
97
- def _required_candidate(candidate: SpecCandidate) -> bool:
98
- metadata = candidate.metadata
99
- return metadata.enforcement == "required" and any(
100
- rule.enforcement == "required" for rule in metadata.rules
101
- )
102
-
103
-
104
- def evaluate_scope(root: str, task_dir: str) -> ScopeResult:
105
- active = load_active_task(root)
106
- files = current_owned_paths(root, active)
107
- context = load_context(_context_path(task_dir))
108
- candidates = resolve_candidates(root, "code", files)
109
- existing = {binding.spec_id for binding in context.bindings}
110
- new = tuple(
111
- candidate
112
- for candidate in candidates
113
- if candidate.scope == "path" and candidate.spec_id not in existing
114
- )
115
- if not new:
116
- return ScopeResult("continue", files, (), "scope unchanged")
117
- selections = tuple(
118
- BindingInput(candidate, "scope:path", f"Git diff matched {','.join(candidate.matched_paths)}")
119
- for candidate in new
120
- )
121
- save_context(_context_path(task_dir), bind_specs(context, selections))
122
- # The toolchain expanded the context itself; re-sync the marker so this
123
- # automatic change does not surface as active_context_drift with no recovery.
124
- resync_active_hash(root, task_dir, context_sha256(load_context(_context_path(task_dir))))
125
- required = tuple(candidate.spec_id for candidate in new if _required_candidate(candidate))
126
- if required:
127
- pause_active_task(root)
128
- message = "新增 required Spec 已暂停 TASK;请选择局部 Plan 或回 Align 更新设计"
129
- return ScopeResult("pause", files, required, message)
130
- return ScopeResult("continue", files, tuple(candidate.spec_id for candidate in new), "advisory scope expanded")
131
-
132
-
133
- def _diff_hash(root: str, files: tuple[str, ...]) -> str:
134
- digest = hashlib.sha256()
135
- for relative in files:
136
- path = Path(root) / relative
137
- digest.update(relative.encode())
138
- digest.update(path.read_bytes() if path.is_file() else b"<deleted>")
139
- return digest.hexdigest()
140
-
141
-
142
- def _evidence_data(evidence: VerificationEvidence) -> Mapping[str, object]:
143
- return {
144
- "verifier_ref": evidence.verifier_ref,
145
- "executed_at": evidence.executed_at,
146
- "status": evidence.status,
147
- "rule_text_sha256": evidence.rule_text_sha256,
148
- "artifact_sha256": evidence.artifact_sha256,
149
- "diff_sha256": evidence.diff_sha256,
150
- "result_sha256": evidence.result_sha256,
151
- "error_code": evidence.error_code,
152
- "details": evidence.details,
153
- }
154
-
155
-
156
- def _update_rule(rule: RuleBinding, evidence: Mapping[str, object]) -> RuleBinding:
157
- stage = rule.verifier_stage or "code"
158
- if stage not in rule.stage_status:
159
- return rule
160
- statuses = dict(rule.stage_status)
161
- current = statuses[stage]
162
- if current.status in ("not_applicable", "waived"):
163
- return rule
164
- status = "verified" if evidence.get("status") == "verified" else "unverified"
165
- if evidence.get("error_code") == "skipped_in_cheap_gate":
166
- signature = (evidence.get("verifier_ref"), evidence.get("result_sha256"), None)
167
- else:
168
- signature = (evidence.get("verifier_ref"), evidence.get("result_sha256"), evidence.get("diff_sha256"))
169
- if any(
170
- (item.get("verifier_ref"), item.get("result_sha256"), item.get("diff_sha256")) == signature
171
- for item in current.evidence
172
- ):
173
- statuses[stage] = replace(current, status=status)
174
- else:
175
- statuses[stage] = replace(current, status=status, evidence=(*current.evidence, evidence))
176
- return replace(rule, stage_status=statuses)
177
-
178
-
179
- def _apply_evidence(context: SpecContext, evidence: tuple[Mapping[str, object], ...]) -> SpecContext:
180
- by_ref = {str(item["verifier_ref"]): item for item in evidence}
181
- bindings: list[SpecBinding] = []
182
- for binding in context.bindings:
183
- rules = tuple(
184
- _update_rule(rule, by_ref[f"{binding.spec_id}#{rule.ref}"])
185
- if f"{binding.spec_id}#{rule.ref}" in by_ref else rule
186
- for rule in binding.rules
187
- )
188
- bindings.append(replace(binding, rules=rules))
189
- return replace(context, bindings=tuple(bindings))
190
-
191
-
192
- def _rule_manual_confirmation(rule: RuleBinding) -> Optional[Mapping[str, object]]:
193
- status = rule.stage_status.get("code")
194
- if status is None or status.decision is None or status.decision.kind != "manual_verification":
195
- return None
196
- decision = status.decision
197
- return {
198
- "reason": decision.reason,
199
- "confirmed_by": decision.confirmed_by,
200
- "confirmed_at": decision.confirmed_at,
201
- "source": decision.source,
202
- }
203
-
204
-
205
- def _run_acceptance(root: str, task_dir: str, owner: str, include_e2e: bool,
206
- deadline: Optional[float], cheap: bool = False) -> str:
207
- baseline_issue = _acceptance_baseline_issue(task_dir, owner)
208
- if baseline_issue:
209
- return baseline_issue
210
- manual_issue = _manual_manifest_issue(task_dir, owner)
211
- if manual_issue:
212
- return manual_issue
213
- if cheap:
214
- # 轻量门禁:只做 manifest 基线与 manual 检查,不执行 functional 场景;
215
- # 场景命令由 finish 的全量 Done Gate 执行。
216
- return ""
217
- manifest_path = Path(task_dir) / ".acceptance-manifest.json"
218
- if manifest_path.is_file():
219
- from cf_acceptance_runner import run_manifest
220
- scenario_result = run_manifest(str(manifest_path), root, write_evidence=True, include_e2e=include_e2e, owner=owner, deadline=deadline)
221
- if scenario_result["decision"] == "block":
222
- reason = scenario_result.get("error") or "acceptance scenario failed"
223
- return f"acceptance scenario failed: {reason}"
224
- return ""
225
-
226
-
227
- def _run_finish_validation(root: str, files: tuple[str, ...]) -> str:
228
- """Full validation.yml (including heavy validators) once per TASK finish.
229
-
230
- Stop-time checks stay cheap; this is the single automatic execution point
231
- for slow suites (task finish `/cf-validate` scope). `quality_loop.finish_check:
232
- false` disables it; projects without validation.yml / package.json scripts are a no-op.
233
- """
234
- if not resolve_quality_loop(load_config(root)).get("finish_check"):
235
- return ""
236
- from cf_validation import validate_files
237
-
238
- try:
239
- result = validate_files(root, files)
240
- except (OSError, ValueError) as exc:
241
- return f"finish validation error: {exc}"
242
- reason = result.get("reason")
243
- if result.get("decision") == "pass" or reason in ("no_changes", "no_validators_configured"):
244
- return ""
245
- failures = result.get("failures")
246
- names = ", ".join(str(item.get("name", "validator")) for item in failures[:3]) if isinstance(failures, list) and failures else "validator"
247
- if result.get("incomplete") and not failures:
248
- return "finish validation 预算耗尽:部分 validator 未执行"
249
- suffix = "(预算耗尽,部分未执行)" if result.get("incomplete") else ""
250
- return f"finish validation failed{suffix}: {names}"
251
-
252
-
253
- def _run_done_gate(root: str, task_dir: str, cheap: bool = False, budget: Optional[float] = None, include_e2e: bool = False, task_id: str = "") -> DoneResult:
254
- active = load_active_task(root)
255
- owner = task_id or active.task_id
256
- if owner != active.task_id or (Path(root) / active.task_dir).resolve() != Path(task_dir).resolve():
257
- return DoneResult("block", (), (), "active_mismatch: task identity does not match marker")
258
- started = time.monotonic()
259
- scope_result = evaluate_scope(root, task_dir)
260
- phase_timing("done.evaluate_scope", started)
261
- if scope_result.decision == "pause":
262
- return DoneResult("block", scope_result.files, (), scope_result.message)
263
- deadline = started + budget if budget is not None else None
264
- issue = _run_acceptance(root, task_dir, owner, include_e2e, deadline, cheap)
265
- if issue:
266
- return DoneResult("block", scope_result.files, (), issue)
267
- phase_started = time.monotonic()
268
- context = load_context(_context_path(task_dir))
269
- diff_hash = _diff_hash(root, scope_result.files)
270
- phase_timing("done.load_context_and_diff", phase_started)
271
- all_evidence: list[Mapping[str, object]] = []
272
- deferred_review = 0
273
- for binding in context.bindings:
274
- phase_started = time.monotonic()
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")
277
- confirmations: dict[str, Mapping[str, object]] = {}
278
- for rule in binding.rules:
279
- confirmation = _rule_manual_confirmation(rule)
280
- if confirmation is not None:
281
- confirmations[rule.ref] = confirmation
282
- remaining = None if budget is None else budget - (time.monotonic() - started)
283
- result = run_all_verifiers(
284
- metadata, VerificationScope(root, scope_result.files, diff_hash), confirmations, cheap, remaining,
285
- stage="code",
286
- )
287
- phase_timing(f"done.verify.{binding.spec_id}", phase_started)
288
- all_evidence.extend(_evidence_data(item) for item in result.evidence)
289
- updated = _apply_evidence(context, tuple(all_evidence))
290
- if updated != context:
291
- save_context(_context_path(task_dir), updated)
292
- gate = validate_stage(updated, "code", diff_sha256=diff_hash, allow_cheap_skips=cheap)
293
- if gate.decision == "pass" and not cheap:
294
- validation_issue = _run_finish_validation(root, scope_result.files)
295
- if validation_issue:
296
- return DoneResult("block", scope_result.files, tuple(all_evidence), validation_issue, deferred_review)
297
- phase_timing("done.total", started)
298
- return DoneResult(gate.decision, scope_result.files, tuple(all_evidence),
299
- "; ".join(issue.message for issue in gate.errors), deferred_review)
300
-
301
-
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:
303
- with execution_session():
304
- return _run_done_gate(root, task_dir, cheap, budget, include_e2e, task_id)
@@ -1,3 +0,0 @@
1
- """Shared lifecycle predicates without loading the workflow runtime."""
2
-
3
- FINISHED_STATUSES = ("done", "verified")
@@ -1,205 +0,0 @@
1
- """Executable task handoffs used by every platform command."""
2
- from __future__ import annotations
3
-
4
- import argparse
5
- import json
6
- import os
7
- import re
8
- from pathlib import Path
9
- from typing import IO, Optional, Sequence
10
- import sys
11
-
12
- from cf_acceptance_runner import run_manifest
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
18
- from cf_task_index import parse_task_file
19
- from cf_task_runtime import _apply_evidence, _evidence_data, run_done_gate
20
- from cf_workflow_service import (FINISHED_STATUSES, _require_marker_task,
21
- _sync_markdown, block_task, complete_task, locate_task_file, resume_task)
22
- from cf_workflow_transaction import recover_transition
23
-
24
-
25
- def finish_task(root: str, directory: str, task_file: str, task_id: str) -> dict[str, object]:
26
- _require_marker_task(root, task_id, directory, task_file)
27
- gate = run_done_gate(root, directory, task_id=task_id)
28
- if gate.decision != "pass":
29
- return {"decision": "block", "reason": gate.message, "evidence": gate.evidence}
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
34
-
35
-
36
- def _demand_tasks(directory: Path) -> list[tuple[Path, str, str]]:
37
- tasks = []
38
- seen = set()
39
- for task_file in sorted(directory.glob("*.md")):
40
- if task_file.name.endswith((".design.md", ".prd.md")):
41
- continue
42
- if not re.search(r"(?m)^## TASK-\d+:", task_file.read_text(encoding="utf-8")):
43
- continue
44
- for node in parse_task_file(str(task_file)):
45
- if node.task_id in seen:
46
- raise ValueError(f"duplicate task id: {node.task_id}")
47
- seen.add(node.task_id)
48
- tasks.append((task_file, node.task_id, node.status))
49
- if not tasks:
50
- raise ValueError("no TASK sections found")
51
- return tasks
52
-
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
-
127
- def verify_e2e(root: str, directory: str) -> dict[str, object]:
128
- recover_transition(root)
129
- tasks = _demand_tasks(Path(directory))
130
- unfinished = [task_id for _, task_id, status in tasks if status not in FINISHED_STATUSES]
131
- if unfinished:
132
- return {"decision": "block", "reason": "unfinished_tasks", "tasks": unfinished}
133
- if (Path(root) / ".code-flow/.active-task.json").exists():
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)}
139
- manifest = Path(directory) / ".acceptance-manifest.json"
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":
165
- for task_file, task_id, _ in tasks:
166
- _sync_markdown(str(task_file), task_id, "verified")
167
- return result
168
-
169
-
170
- def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
171
- parser = argparse.ArgumentParser()
172
- parser.add_argument("action", choices=("finish", "block", "resume", "verify-e2e"))
173
- parser.add_argument("--root", default=os.getcwd())
174
- parser.add_argument("--task-dir", required=True)
175
- parser.add_argument("--task", default="")
176
- parser.add_argument("--reason", default="")
177
- parser.add_argument("--json", action="store_true")
178
- args = parser.parse_args(argv)
179
- try:
180
- root = str(Path(args.root).resolve())
181
- directory = str((Path(root) / args.task_dir).resolve())
182
- if Path(root) not in Path(directory).parents:
183
- raise ValueError("task directory outside project")
184
- recover_transition(root)
185
- if args.action == "verify-e2e":
186
- result = verify_e2e(root, directory)
187
- else:
188
- task = locate_task_file(directory, args.task)
189
- if task is None:
190
- raise ValueError("task file missing or ambiguous")
191
- if args.action == "finish":
192
- result = finish_task(root, directory, str(task), args.task)
193
- elif args.action == "block":
194
- result = block_task(root, directory, str(task), args.task, args.reason)
195
- else:
196
- result = resume_task(root, directory, str(task), args.task)
197
- stdout.write(json.dumps(result, ensure_ascii=False))
198
- return 3 if result.get("decision") == "block" else 0
199
- except (OSError, ValueError) as exc:
200
- stdout.write(json.dumps({"decision": "block", "error": str(exc)}, ensure_ascii=False))
201
- return 2
202
-
203
-
204
- if __name__ == "__main__":
205
- raise SystemExit(main())