@michaelxwb/code-flow 0.6.5 → 0.8.1

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 (207) hide show
  1. package/README.md +47 -3
  2. package/docs/USAGE.md +1529 -0
  3. package/docs/acceptance-contract.md +217 -0
  4. package/docs/ci/acceptance-evidence.md +77 -0
  5. package/docs/migrations/runtime-0.7.0.md +41 -0
  6. package/docs/migrations/runtime-0.8.0.md +154 -0
  7. package/docs/migrations/runtime-0.9.0.md +40 -0
  8. package/docs/spec-injection-hardening.md +54 -0
  9. package/docs/spec-injection-performance.json +73 -0
  10. package/docs/spec-optimization.md +12 -0
  11. package/package.json +18 -3
  12. package/src/adapters/claude/CLAUDE.md +1 -1
  13. package/src/adapters/claude/commands/cf-init.md +1 -3
  14. package/src/adapters/claude/commands/cf-spec.md +23 -3
  15. package/src/adapters/claude/commands/cf-stats.md +11 -1
  16. package/src/adapters/claude/commands/cf-sync.md +20 -6
  17. package/src/adapters/claude/commands/cf-task/align.md +25 -3
  18. package/src/adapters/claude/commands/cf-task/archive.md +36 -42
  19. package/src/adapters/claude/commands/cf-task/block.md +15 -2
  20. package/src/adapters/claude/commands/cf-task/graph.md +13 -1
  21. package/src/adapters/claude/commands/cf-task/note.md +13 -1
  22. package/src/adapters/claude/commands/cf-task/plan.md +137 -23
  23. package/src/adapters/claude/commands/cf-task/prd.md +13 -2
  24. package/src/adapters/claude/commands/cf-task/start.md +52 -39
  25. package/src/adapters/claude/commands/cf-task/status.md +12 -0
  26. package/src/adapters/claude/commands/cf-task/verify-e2e.md +68 -35
  27. package/src/adapters/claude/commands/cf-validate.md +30 -5
  28. package/src/adapters/codex/AGENTS.md +1 -1
  29. package/src/adapters/codex/hooks.json +14 -10
  30. package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
  31. package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
  32. package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
  33. package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
  34. package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
  35. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -42
  36. package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
  37. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
  38. package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
  39. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
  40. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
  41. package/src/adapters/codex/skills/cf-task-start/SKILL.md +52 -39
  42. package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
  43. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +69 -35
  44. package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
  45. package/src/adapters/commands.json +115 -0
  46. package/src/adapters/costrict/CLAUDE.md +1 -1
  47. package/src/adapters/costrict/commands/cf-init.md +1 -3
  48. package/src/adapters/costrict/commands/cf-spec.md +23 -3
  49. package/src/adapters/costrict/commands/cf-stats.md +11 -1
  50. package/src/adapters/costrict/commands/cf-sync.md +20 -6
  51. package/src/adapters/costrict/commands/cf-task/align.md +25 -3
  52. package/src/adapters/costrict/commands/cf-task/archive.md +36 -42
  53. package/src/adapters/costrict/commands/cf-task/block.md +15 -2
  54. package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
  55. package/src/adapters/costrict/commands/cf-task/note.md +13 -1
  56. package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
  57. package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
  58. package/src/adapters/costrict/commands/cf-task/start.md +52 -39
  59. package/src/adapters/costrict/commands/cf-task/status.md +12 -0
  60. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +68 -35
  61. package/src/adapters/costrict/commands/cf-validate.md +30 -5
  62. package/src/adapters/install-plan.json +100 -0
  63. package/src/adapters/opencode/AGENTS.md +1 -1
  64. package/src/adapters/opencode/commands/cf-init.md +7 -7
  65. package/src/adapters/opencode/commands/cf-learn.md +6 -6
  66. package/src/adapters/opencode/commands/cf-spec.md +34 -2
  67. package/src/adapters/opencode/commands/cf-stats.md +17 -7
  68. package/src/adapters/opencode/commands/cf-sync.md +21 -6
  69. package/src/adapters/opencode/commands/cf-task/align.md +25 -8
  70. package/src/adapters/opencode/commands/cf-task/archive.md +36 -42
  71. package/src/adapters/opencode/commands/cf-task/block.md +16 -2
  72. package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
  73. package/src/adapters/opencode/commands/cf-task/note.md +14 -1
  74. package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
  75. package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
  76. package/src/adapters/opencode/commands/cf-task/start.md +53 -45
  77. package/src/adapters/opencode/commands/cf-task/status.md +13 -0
  78. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +68 -35
  79. package/src/adapters/opencode/commands/cf-validate.md +35 -9
  80. package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
  81. package/src/adapters/platform-bindings.json +370 -0
  82. package/src/cli.js +369 -316
  83. package/src/core/code-flow/config.yml +4 -1
  84. package/src/core/code-flow/runtime-commands.json +259 -0
  85. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
  86. package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
  87. package/src/core/code-flow/scripts/cf_audit.py +10 -0
  88. package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
  89. package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
  90. package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
  91. package/src/core/code-flow/scripts/cf_feedback.py +5 -51
  92. package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
  93. package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
  94. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
  95. package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
  96. package/src/core/code-flow/scripts/cf_spec_context.py +5 -1741
  97. package/src/core/code-flow/scripts/cf_spec_gate.py +5 -259
  98. package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
  99. package/src/core/code-flow/scripts/cf_stats.py +5 -501
  100. package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
  101. package/src/core/code-flow/scripts/cf_sync.py +5 -135
  102. package/src/core/code-flow/scripts/cf_task_index.py +5 -143
  103. package/src/core/code-flow/scripts/cf_task_parallel.py +5 -389
  104. package/src/core/code-flow/scripts/cf_task_workflow.py +6 -201
  105. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
  106. package/src/core/code-flow/scripts/cf_validation.py +6 -77
  107. package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
  108. package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
  109. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +347 -0
  110. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
  111. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
  112. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
  113. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
  114. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
  115. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
  116. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
  117. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
  118. package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
  119. package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
  120. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
  121. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
  122. package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
  123. package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
  124. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
  125. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
  126. package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
  127. package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
  128. package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
  129. package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
  130. package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
  131. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
  132. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
  133. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
  134. package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
  135. package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
  136. package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
  137. package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
  138. package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
  139. package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
  140. package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
  141. package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
  142. package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
  143. package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
  144. package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
  145. package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
  146. package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
  147. package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
  148. package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
  149. package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
  150. package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
  151. package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
  152. package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
  153. package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
  154. package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
  155. package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
  156. package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
  157. package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
  158. package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
  159. package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
  160. package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
  161. package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
  162. package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
  163. package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
  164. package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
  165. package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
  166. package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
  167. package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
  168. package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
  169. package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
  170. package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
  171. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
  172. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
  173. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
  174. package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +5 -2
  175. package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
  176. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
  177. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
  178. package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
  179. package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
  180. package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +163 -35
  181. package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
  182. package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
  183. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
  184. package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
  185. package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
  186. package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
  187. package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
  188. package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
  189. package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
  190. package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
  191. package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
  192. package/src/core/code-flow/scripts/runtime-resources.json +29 -0
  193. package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
  194. package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
  195. package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
  196. package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
  197. package/src/core/code-flow/validation.yml +9 -3
  198. package/src/migrate/runtime.js +213 -0
  199. package/src/migrate/spec-workflow.js +16 -1
  200. package/src/runtime.js +219 -0
  201. package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
  202. package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
  203. package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
  204. package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
  205. package/src/core/code-flow/scripts/cf_task_state.py +0 -3
  206. package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
  207. /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
@@ -1,381 +1,11 @@
1
1
  #!/usr/bin/env python3
2
- """Stop Hook: run validation.yml checks on session-edited files (FEAT-03).
3
-
4
- Flow: session edit events (cf_log) → match validator triggers → run commands
5
- serially under a total budget → failures surface as {"decision": "block",
6
- "reason": ...} so the agent fixes them before the session truly ends.
7
-
8
- Protocol notes (deviation from the injection-hook contract, recorded in
9
- TASK-010): the Stop event's feedback channel is decision/reason — there is no
10
- additionalContext for Stop. `stop_hook_active` is respected so an already
11
- continued session is never re-blocked (no loops). All-pass → silent exit 0.
12
- """
13
- import fnmatch
14
- import json
15
- import math
16
- import os
17
- import re
2
+ """Path-frozen entry shim: implementation lives in codeflow.hooks.cf_stop_hook."""
18
3
  import sys
19
- import time
20
-
21
- import cf_log
22
- from cf_exec_base import build_argv, remaining_seconds, run_command, execution_session
23
- from cf_spec_context import load_active_task
24
- from cf_task_runtime import run_done_gate
25
- from cf_acceptance_evidence import line_status, scenario_line
26
- from cf_core import (
27
- _log,
28
- ensure_utf8_io,
29
- load_config,
30
- normalize_path,
31
- resolve_enforcement,
32
- resolve_quality_loop,
33
- resolve_session_id,
34
- timing_log,
35
- )
36
-
37
- TOTAL_BUDGET_SECONDS = 30.0
38
- GATE_BUDGET_SECONDS = 25.0
39
- _BRACE_RE = re.compile(r"\{([^{}]+)\}")
40
- _TASK_SECTION_RE = re.compile(
41
- r"(?ms)^## (TASK-\d+):.*?(?=^## TASK-\d+:|\Z)"
42
- )
43
- _SCENARIO_RE = re.compile(r"\b[SEB]-\d+\b")
44
- _UNVERIFIED_RE = re.compile(r"\b(?:planned|pending|tbd)\b", re.IGNORECASE)
45
-
46
-
47
- def expand_braces(pattern: str) -> list:
48
- """Expand one level of `{a,b}` alternation → list of fnmatch patterns."""
49
- match = _BRACE_RE.search(pattern)
50
- if not match:
51
- return [pattern]
52
- head, tail = pattern[: match.start()], pattern[match.end():]
53
- out = []
54
- for option in match.group(1).split(","):
55
- out.extend(expand_braces(head + option + tail))
56
- return out
57
-
58
-
59
- def trigger_matches(trigger: str, rel_path: str) -> bool:
60
- """fnmatch with brace expansion; `**/` prefix also matches root files."""
61
- rel_path = normalize_path(rel_path)
62
- for pattern in expand_braces(trigger or ""):
63
- candidates = [pattern]
64
- if pattern.startswith("**/"):
65
- candidates.append(pattern[3:])
66
- if any(fnmatch.fnmatch(rel_path, p) for p in candidates):
67
- return True
68
- return False
69
-
70
-
71
- def load_validators(project_root: str) -> list:
72
- path = os.path.join(project_root, ".code-flow", "validation.yml")
73
- if not os.path.exists(path):
74
- return []
75
- try:
76
- import yaml
77
- with open(path, "r", encoding="utf-8") as f:
78
- data = yaml.safe_load(f) or {}
79
- validators = data.get("validators")
80
- return validators if isinstance(validators, list) else []
81
- except Exception as exc:
82
- _log(f"cf_stop_hook validation.yml parse failed: {exc}")
83
- return []
84
-
85
-
86
- def session_edited_files(project_root: str, sid: str) -> list:
87
- files = []
88
- seen = set()
89
- for event in cf_log.read_events(project_root, days=2, events=("edit",)):
90
- if event.get("sid") != sid:
91
- continue
92
- rel = (event.get("data") or {}).get("file")
93
- if rel and rel not in seen:
94
- seen.add(rel)
95
- files.append(rel)
96
- return files
97
-
98
-
99
- def _root_scoped_files(project_root: str, files: list) -> list:
100
- """Drop worktree-internal / outside-root paths.
101
-
102
- Parallel subagents edit another working copy (.code-flow/worktrees/<run>/<TASK>/);
103
- those paths must not trigger this root's validators.
104
- """
105
- root = os.path.realpath(project_root)
106
- scoped = []
107
- for rel in files:
108
- path = normalize_path(rel)
109
- if path.startswith(".code-flow/worktrees/"):
110
- continue
111
- absolute = os.path.realpath(os.path.join(root, path))
112
- if absolute == root or absolute.startswith(root + os.sep):
113
- scoped.append(rel)
114
- return scoped
115
-
116
-
117
- def _stop_validators(project_root: str) -> list:
118
- """Per-stop validators: heavy entries (full tests / e2e suites) are skipped
119
- on every Stop and only run via the explicit `cf-validate` command (or CI)."""
120
- return [
121
- validator
122
- for validator in load_validators(project_root)
123
- if not (isinstance(validator, dict) and validator.get("heavy") is True)
124
- ]
125
-
126
-
127
- def _is_active_task_file(rel_path: str) -> bool:
128
- path = normalize_path(rel_path)
129
- if not path.startswith(".code-flow/tasks/") or "/archived/" in path:
130
- return False
131
- return path.endswith(".md") and not path.endswith((".design.md", ".prd.md"))
132
-
133
-
134
- def _subsection(section: str, heading: str) -> str:
135
- match = re.search(
136
- rf"(?ms)^### {re.escape(heading)}\s*$\n(.*?)(?=^### |^## |\Z)",
137
- section,
138
- )
139
- return match.group(1) if match else ""
140
-
141
-
142
- def _acceptance_gap(task_id: str, section: str, coverage: str) -> str:
143
- status = re.search(r"(?m)^- \*\*Status\*\*: ([^\n]+)", section)
144
- # done = implementation finished; verified = E2E/final acceptance closed.
145
- # Both enter the same contract checks; anything else is still in flight.
146
- if not status or status.group(1).strip() not in ("done", "verified"):
147
- return ""
148
- refs_match = re.search(r"(?m)^- \*\*Acceptance-Refs\*\*: ([^\n]+)", section)
149
- if not refs_match:
150
- return f"{task_id} 缺少 Acceptance-Refs"
151
- refs = refs_match.group(1).strip()
152
- if refs.upper().startswith("N/A"):
153
- return ""
154
- scenarios = sorted(set(_SCENARIO_RE.findall(refs)))
155
- if not scenarios:
156
- return f"{task_id} Acceptance-Refs 未引用 S/E/B 场景"
157
- contract = _subsection(section, "Acceptance Contract")
158
- evidence = _subsection(section, "Acceptance Evidence")
159
- if not contract or not evidence:
160
- return f"{task_id} 缺少 Acceptance Contract 或 Acceptance Evidence"
161
- for scenario in scenarios:
162
- coverage_rows = [line for line in coverage.splitlines() if scenario_line(line, scenario)]
163
- cells = coverage_rows[0].strip().strip("|").split("|") if coverage_rows else []
164
- deferred = status.group(1).strip() == "done" and len(cells) >= 3 and cells[2].strip().lower() == "e2e"
165
- allowed = {"verified", "e2e_deferred"} if deferred else {"verified"}
166
- states = []
167
- for body in (contract, evidence, coverage):
168
- found = [line_status(line, scenario) for line in body.splitlines() if scenario_line(line, scenario)]
169
- states.append(found[-1] if found else "")
170
- if not all(state in allowed for state in states):
171
- return f"{task_id} 的 {scenario} 状态未闭环(planned/pending/TBD/failed 或缺少 verified;仅实现阶段 E2E 可 e2e_deferred)"
172
- return ""
173
-
174
-
175
- def task_acceptance_failures(project_root: str, files: list) -> list:
176
- """Check new-format task files; legacy tasks without coverage are ignored."""
177
- failures = []
178
- for rel_path in files:
179
- if not _is_active_task_file(rel_path):
180
- continue
181
- path = os.path.join(project_root, normalize_path(rel_path))
182
- try:
183
- with open(path, "r", encoding="utf-8") as f:
184
- text = f.read()
185
- except OSError as exc:
186
- _log(f"cf_stop_hook task read failed: {exc}")
187
- continue
188
- if "## Acceptance Coverage" not in text:
189
- continue
190
- coverage = text.split("## Acceptance Coverage", 1)[1].split("## TASK-", 1)[0]
191
- for match in _TASK_SECTION_RE.finditer(text):
192
- detail = _acceptance_gap(match.group(1), match.group(0), coverage)
193
- if detail:
194
- failures.append({
195
- "name": "任务验收契约",
196
- "on_fail": "补齐设计场景的测试映射与执行证据后再完成任务",
197
- "detail": detail,
198
- })
199
- return failures
200
-
201
-
202
- def _validator_failure(validator: dict, detail: str, incomplete: bool = False) -> dict:
203
- result = {"name": validator.get("name", "validator"),
204
- "on_fail": validator.get("on_fail", ""), "detail": detail}
205
- if incomplete:
206
- result["incomplete"] = True
207
- return result
208
-
209
-
210
- def _validator_argv(root: str, validator: dict, matched: list, strict: bool) -> list:
211
- existing = [name for name in matched if os.path.isfile(os.path.join(root, name))]
212
- template = str(validator.get("command", ""))
213
- if "{files}" in template and not existing:
214
- return []
215
- argv = build_argv(template, existing)
216
- if not argv and strict:
217
- raise ValueError("validator command missing")
218
- return argv
219
-
220
-
221
- def _validator_result(root: str, validator: dict, argv: list, sid: str,
222
- deadline: float, strict: bool) -> tuple[list, bool]:
223
- try:
224
- timeout = float(validator.get("timeout", 30000)) / 1000.0
225
- except (ValueError, TypeError):
226
- if strict:
227
- return [_validator_failure(validator, "invalid validator timeout")], False
228
- timeout = 30.0
229
- if not math.isfinite(timeout) or timeout <= 0:
230
- return [_validator_failure(validator, "invalid validator timeout")], False
231
- outcome = run_command(argv, root, timeout, deadline)
232
- passed = outcome["status"] == "ok" and outcome["returncode"] == 0
233
- if outcome["status"] == "deadline_exceeded":
234
- return [_validator_failure(validator, "预算耗尽未执行", True)], True
235
- timed_out = outcome["status"] == "timeout"
236
- detail = (f"超时(>{timeout:.0f}s),未完成" if timed_out else
237
- (str(outcome.get("stdout", "")) + str(outcome.get("stderr", ""))).strip()[-400:])
238
- if outcome["status"] == "spawn_error":
239
- cf_log.degrade(root, "stop_check", f"{validator.get('name')}:{outcome['stderr']}", sid)
240
- if not strict:
241
- return [], False
242
- cf_log.append_event(root, "stop_check",
243
- {"trigger": validator.get("trigger", ""),
244
- "cmd": str(validator.get("command", ""))[:120], "passed": passed}, sid)
245
- return ([] if passed else [_validator_failure(validator, detail)]), timed_out
246
-
247
-
248
- def run_validators(
249
- project_root: str, validators: list, files: list, sid: str,
250
- total_budget: float = TOTAL_BUDGET_SECONDS,
251
- deadline: float = 0.0,
252
- strict: bool = False,
253
- ) -> tuple:
254
- """Match validators, execute argv under one deadline, report unrun work."""
255
- failures, truncated = [], False
256
- deadline = deadline or time.monotonic() + total_budget
257
- for validator in validators:
258
- if not isinstance(validator, dict):
259
- if strict:
260
- failures.append(_validator_failure({}, "invalid validator record"))
261
- continue
262
- matched = [f for f in files if trigger_matches(validator.get("trigger", ""), f)]
263
- if not matched:
264
- continue
265
- remaining = remaining_seconds(deadline)
266
- if remaining is not None and remaining <= 0:
267
- failures.append(_validator_failure(validator, "预算耗尽未执行", True))
268
- truncated = True
269
- continue
270
- try:
271
- argv = _validator_argv(project_root, validator, matched, strict)
272
- except ValueError as exc:
273
- failures.append(_validator_failure(validator, f"validator 配置错误: {exc}"))
274
- continue
275
- if not argv:
276
- continue
277
- errors, incomplete = _validator_result(project_root, validator, argv, sid, deadline, strict)
278
- failures.extend(errors)
279
- truncated = truncated or incomplete
280
- return failures, truncated
281
-
282
-
283
- def _reason_text(failures: list, truncated: bool) -> str:
284
- lines = ["收尾校验未通过(cf-stop):"]
285
- for item in failures:
286
- tag = "(未执行)" if item.get("incomplete") else ""
287
- lines.append(f"✗ {item['name']}{tag}:{item['on_fail']}")
288
- if item["detail"]:
289
- lines.append(f" 输出片段: {item['detail'][:200]}")
290
- if truncated:
291
- lines.append("(总预算 30s 已用尽,以上为已完成部分)")
292
- lines.append("请修复后再结束。")
293
- return "\n".join(lines)
294
-
295
-
296
- def _main() -> None:
297
- try:
298
- ensure_utf8_io()
299
- raw = sys.stdin.read()
300
- if not raw.strip():
301
- return
302
- data = json.loads(raw)
303
- if data.get("stop_hook_active"):
304
- return # 已因本 hook 续跑过一轮,避免循环
305
- project_root = os.getcwd()
306
- sid = resolve_session_id(data)
307
- config = load_config(project_root)
308
- if not config:
309
- return
310
- enforcement = resolve_enforcement(config)
311
- if enforcement == "inject":
312
- return # 轻量模式:只注入不门禁,停止会话不受限
313
- marker = os.path.join(project_root, ".code-flow", ".active-task.json")
314
- has_active = os.path.exists(marker)
315
- files = []
316
- # Single entry deadline constrains the whole chain (Done + validators),
317
- # so long checks end as `incomplete` instead of being killed by the
318
- # host without a verdict.
319
- deadline = time.monotonic() + TOTAL_BUDGET_SECONDS
320
- if has_active:
321
- try:
322
- active = load_active_task(project_root)
323
- task_dir = os.path.join(project_root, active.task_dir)
324
- gate_remaining = deadline - time.monotonic()
325
- done = run_done_gate(project_root, task_dir, cheap=True, budget=min(GATE_BUDGET_SECONDS, max(gate_remaining, 0.01)))
326
- except (OSError, ValueError) as exc:
327
- if enforcement == "required":
328
- payload = {"decision": "block", "reason": f"SPEC_WORKFLOW_BLOCKED: active task is invalid: {exc}"}
329
- sys.stdout.write(json.dumps(payload, ensure_ascii=False))
330
- return
331
- cf_log.append_event(project_root, "stop_check", {"gate": "invalid_active_nonfatal", "error": str(exc)}, sid)
332
- files = []
333
- else:
334
- if done.decision != "pass":
335
- reason = done.message or "当前 TASK required Spec verifier/Evidence 未通过;修复或重新对齐后再 Done。"
336
- if any(item.get("error_code") == "verifier_budget_exhausted" for item in done.evidence):
337
- reason += "(验证预算不足,部分 verifier 未运行,请拆分 TASK 或减少验证命令)"
338
- if enforcement == "required":
339
- payload = {"decision": "block", "reason": reason}
340
- sys.stdout.write(json.dumps(payload, ensure_ascii=False))
341
- return
342
- cf_log.append_event(project_root, "stop_check", {"gate": "blocked_nonfatal", "reason": reason}, sid)
343
- files = list(done.files)
344
- if not resolve_quality_loop(config)["stop_check"]:
345
- return
346
- if not has_active:
347
- files = _root_scoped_files(project_root, session_edited_files(project_root, sid))
348
- if not files:
349
- return
350
- acceptance_failures = task_acceptance_failures(project_root, files)
351
- validators = _stop_validators(project_root)
352
- failures, truncated = run_validators(
353
- project_root, validators, files, sid, deadline=deadline
354
- ) if validators else ([], False)
355
- failures = acceptance_failures + failures
356
- if not failures:
357
- return # 全过或无 validation.yml 且无验收缺口时静默
358
- if enforcement == "warn":
359
- cf_log.append_event(
360
- project_root, "stop_check",
361
- {"failures": [item["name"] for item in failures], "nonfatal": True},
362
- sid,
363
- )
364
- return
365
- payload = {"decision": "block", "reason": _reason_text(failures, truncated)}
366
- sys.stdout.write(json.dumps(payload, ensure_ascii=False))
367
- except Exception as exc:
368
- _log(f"cf_stop_hook error: {exc}")
369
- if locals().get("enforcement") == "required":
370
- sys.stdout.write(json.dumps({"decision": "block", "reason": f"SPEC_WORKFLOW_BLOCKED: {exc}"}, ensure_ascii=False))
371
- return
372
-
373
-
374
- def main() -> None:
375
- with execution_session():
376
- _main()
377
4
 
5
+ from codeflow.hooks import cf_stop_hook as _impl
378
6
 
379
7
  if __name__ == "__main__":
380
- main()
381
- timing_log("cf_stop_hook")
8
+ _impl.main()
9
+ _impl.timing_log("cf_stop_hook")
10
+ else:
11
+ sys.modules[__name__] = _impl
@@ -1,140 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- """cf-sync: one-command canonical → deploy sync for dual-copy artifacts.
3
-
4
- Pairs (canonical source → deployed live copy), each with an explicit include
5
- prefix list — project-owned trees (specs/, tasks/) and deploy-only files
6
- (config.toml, CLAUDE.md, opencode.json) are never touched:
7
-
8
- src/core/code-flow → .code-flow (scripts/, config.yml, .version, validation.yml, .gitignore)
9
- src/adapters/claude → .claude (commands/, settings.local.json)
10
- src/adapters/codex → .codex (hooks.json)
11
- src/adapters/codex/skills → .agents/skills (everything)
12
- src/adapters/costrict → .costrict (commands/, settings.local.json)
13
- src/adapters/opencode → .opencode (commands/, plugins/)
14
-
15
- `check` (default) reports drift; `sync` copies canonical → deploy. Deploy-only
16
- files are never deleted (use --prune to remove files whose source side no
17
- longer contains them within the include prefixes).
18
- """
19
-
20
- import argparse
21
- import filecmp
22
- import os
23
- from pathlib import Path
24
- import shutil
2
+ """Path-frozen entry shim: implementation lives in codeflow.cli.cf_sync."""
25
3
  import sys
26
- from typing import IO, Optional, Sequence
27
-
28
-
29
- PAIRS = (
30
- ("src/core/code-flow", ".code-flow", ("scripts/", ".version", ".gitignore")),
31
- ("src/adapters/claude", ".claude", ("commands/",)),
32
- ("src/adapters/codex", ".codex", ("hooks.json",)),
33
- ("src/adapters/codex/skills", ".agents/skills", ("*",)),
34
- ("src/adapters/costrict", ".costrict", ("commands/",)),
35
- ("src/adapters/opencode", ".opencode", ("commands/", "plugins/")),
36
- )
37
-
38
- _IGNORED_DIRS = {"__pycache__", ".git", "node_modules", "dist", "build"}
39
-
40
-
41
- def _included(relative: str, prefixes: Sequence[str]) -> bool:
42
- return any(prefix == "*" or relative.startswith(prefix) for prefix in prefixes)
43
-
44
-
45
- def _walk(source: Path) -> list[str]:
46
- files = []
47
- for current, dirs, names in os.walk(source):
48
- dirs[:] = sorted(item for item in dirs if item not in _IGNORED_DIRS)
49
- base = Path(current)
50
- for name in names:
51
- files.append(str((base / name).relative_to(source)))
52
- return sorted(files)
53
-
54
-
55
- def _relative(relative: str) -> str:
56
- return relative.replace("\\", "/")
57
-
58
-
59
- def _pair_diffs(root: Path, source_rel: str, deploy_rel: str, prefixes: Sequence[str]) -> tuple[list[str], list[str], list[str]]:
60
- """Return (differ, missing_in_deploy, deploy_only_within_prefixes)."""
61
- source = root / source_rel
62
- deploy = root / deploy_rel
63
- differ: list[str] = []
64
- missing: list[str] = []
65
- for relative in _walk(source):
66
- if not _included(_relative(relative), prefixes):
67
- continue
68
- source_file = source / relative
69
- deploy_file = deploy / relative
70
- if not deploy_file.exists():
71
- missing.append(relative)
72
- elif not filecmp.cmp(source_file, deploy_file, shallow=False):
73
- differ.append(relative)
74
- deploy_only: list[str] = []
75
- if deploy.is_dir():
76
- deploy_relative = set(_walk(deploy))
77
- source_relative = {_relative(item) for item in _walk(source) if _included(_relative(item), prefixes)}
78
- deploy_only = sorted(item for item in deploy_relative if item not in source_relative and _included(item, prefixes))
79
- return differ, missing, deploy_only
80
-
81
-
82
- def _report(stdout: IO[str], root: Path, verbose: bool) -> int:
83
- drift = 0
84
- for source_rel, deploy_rel, prefixes in PAIRS:
85
- differ, missing, deploy_only = _pair_diffs(root, source_rel, deploy_rel, prefixes)
86
- status = "OK " if not (differ or missing) else "DRIFT"
87
- stdout.write(f"{status} {source_rel} -> {deploy_rel}\n")
88
- for relative in differ:
89
- stdout.write(f" diff {deploy_rel}/{relative}\n")
90
- drift += 1
91
- for relative in missing:
92
- stdout.write(f" miss {deploy_rel}/{relative}\n")
93
- drift += 1
94
- if verbose and deploy_only:
95
- stdout.write(f" only-in-deploy ({len(deploy_only)}):\n")
96
- for relative in deploy_only[:10]:
97
- stdout.write(f" {deploy_rel}/{relative}\n")
98
- return drift
99
-
100
-
101
- def _sync(root: Path, stdout: IO[str]) -> int:
102
- copied = 0
103
- for source_rel, deploy_rel, prefixes in PAIRS:
104
- source = root / source_rel
105
- deploy = root / deploy_rel
106
- for relative in _walk(source):
107
- if not _included(_relative(relative), prefixes):
108
- continue
109
- source_file = source / relative
110
- deploy_file = deploy / relative
111
- if deploy_file.exists() and filecmp.cmp(source_file, deploy_file, shallow=False):
112
- continue
113
- deploy_file.parent.mkdir(parents=True, exist_ok=True)
114
- shutil.copy2(source_file, deploy_file)
115
- stdout.write(f"sync {deploy_rel}/{relative}\n")
116
- copied += 1
117
- return copied
118
-
119
-
120
- def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
121
- parser = argparse.ArgumentParser(prog="cf_sync.py")
122
- parser.add_argument("action", choices=("check", "sync"), nargs="?", default="check")
123
- parser.add_argument("--root", default=".")
124
- parser.add_argument("--verbose", action="store_true")
125
- args = parser.parse_args(argv)
126
- root = Path(args.root).resolve()
127
- if args.action == "check":
128
- drift = _report(stdout, root, args.verbose)
129
- if drift:
130
- stdout.write(f"{drift} file(s) drifted; run `cf_sync.py sync` to deploy.\n")
131
- return 1
132
- stdout.write("all pairs in sync.\n")
133
- return 0
134
- copied = _sync(root, stdout)
135
- stdout.write(f"{copied} file(s) deployed.\n")
136
- return 0
137
4
 
5
+ from codeflow.cli import cf_sync as _impl
138
6
 
139
7
  if __name__ == "__main__":
140
- raise SystemExit(main())
8
+ raise SystemExit(_impl.main())
9
+ else:
10
+ sys.modules[__name__] = _impl
@@ -1,148 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- """Programmatic task-file index: parse TASK statuses/depends, compute DAG batches.
3
-
4
- Single worktrees allow one active TASK at a time; batches list *independently
5
- developable* tasks (no dependency path between them). `cf-task:start` may
6
- dispatch batch tasks in their own worktrees for parallel subagents — the
7
- single-active constraint stays per worktree.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import argparse
13
- import json
14
- import re
2
+ """Path-frozen entry shim: implementation lives in codeflow.cli.cf_task_index."""
15
3
  import sys
16
- from dataclasses import dataclass
17
- from pathlib import Path
18
- from typing import IO, Mapping, Optional, Sequence
19
-
20
-
21
- from cf_task_state import FINISHED_STATUSES as _DONE_STATUSES
22
-
23
-
24
- @dataclass(frozen=True)
25
- class TaskNode:
26
- task_id: str
27
- status: str
28
- depends: tuple[str, ...]
29
-
30
-
31
- def parse_task_file(task_file: str) -> tuple[TaskNode, ...]:
32
- """Parse every `## TASK-xxx` section into status/depends nodes.
33
-
34
- Unknown statuses are preserved verbatim (e.g. `verified` from the E2E
35
- flow) — the parser never rejects them, completion checks treat done and
36
- verified as finished.
37
- """
38
- try:
39
- text = Path(task_file).read_text(encoding="utf-8")
40
- except (OSError, UnicodeError) as exc:
41
- raise ValueError(f"task_read_error: {exc}") from exc
42
- nodes: list[TaskNode] = []
43
- matches = list(re.finditer(r"(?m)^## (TASK-\d+):[^\n]*$", text))
44
- for index, match in enumerate(matches):
45
- end = matches[index + 1].start() if index + 1 < len(matches) else len(text)
46
- section = text[match.start():end]
47
- status_match = re.search(r"(?m)^- \*\*Status\*\*:[ \t]*([^\n]+)", section)
48
- status = status_match.group(1).strip() if status_match else "draft"
49
- depends_match = re.search(r"(?m)^- \*\*Depends\*\*:[ \t]*([^\n]*)", section)
50
- depends: tuple[str, ...] = ()
51
- if depends_match:
52
- depends = tuple(
53
- part.strip()
54
- for part in re.split(r"[,,\s]+", depends_match.group(1).strip())
55
- if re.fullmatch(r"TASK-\d+", part.strip())
56
- )
57
- nodes.append(TaskNode(match.group(1), status, depends))
58
- if not nodes:
59
- raise ValueError("no TASK sections found")
60
- return tuple(nodes)
61
-
62
-
63
- def task_batches(nodes: Sequence[TaskNode]) -> list[list[str]]:
64
- """Topological batches over unfinished TASKs; unknown deps and cycles raise."""
65
- ids = {node.task_id for node in nodes}
66
- for node in nodes:
67
- unknown = [dep for dep in node.depends if dep not in ids]
68
- if unknown:
69
- raise ValueError(f"unknown depends: {node.task_id} -> {', '.join(unknown)}")
70
- pending = {node.task_id for node in nodes if node.status not in _DONE_STATUSES}
71
- finished = {node.task_id for node in nodes if node.status in _DONE_STATUSES}
72
- deps = {node.task_id: set(node.depends) for node in nodes}
73
- batches: list[list[str]] = []
74
- while pending:
75
- ready = sorted(task for task in pending if deps[task] <= finished)
76
- if not ready:
77
- raise ValueError(f"dependency cycle among: {', '.join(sorted(pending))}")
78
- batches.append(ready)
79
- finished.update(ready)
80
- pending.difference_update(ready)
81
- return batches
82
-
83
-
84
- def dependency_components(nodes: Sequence[TaskNode]) -> list[list[str]]:
85
- """Connected dependency components; different components are independent."""
86
- parent = {node.task_id: node.task_id for node in nodes}
87
-
88
- def find(task: str) -> str:
89
- while parent[task] != task:
90
- parent[task] = parent[parent[task]]
91
- task = parent[task]
92
- return task
93
-
94
- for node in nodes:
95
- for dep in node.depends:
96
- left, right = find(node.task_id), find(dep)
97
- if left != right:
98
- parent[left] = right
99
- groups: dict[str, list[str]] = {}
100
- for node in nodes:
101
- groups.setdefault(find(node.task_id), []).append(node.task_id)
102
- return [sorted(members) for members in groups.values()]
103
-
104
-
105
- def independent_groups(nodes: Sequence[TaskNode]) -> list[list[str]]:
106
- """Compatibility alias: groups are independent of each other, not within."""
107
- return dependency_components(nodes)
108
-
109
-
110
- def index_data(task_file: str) -> dict[str, object]:
111
- nodes = parse_task_file(task_file)
112
- return {
113
- "tasks": [
114
- {"id": node.task_id, "status": node.status, "depends": list(node.depends)}
115
- for node in nodes
116
- ],
117
- "batches": task_batches(nodes),
118
- "independent_groups": independent_groups(nodes),
119
- "dependency_components": dependency_components(nodes),
120
- "note": "batches 组内无依赖;dependency_components 仅组间独立(independent_groups 为兼容别名);单 worktree 一次仅激活一个 TASK,批次内独立任务可由 cf-task:start 在各 worktree 并行派发",
121
- }
122
-
123
-
124
- def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
125
- parser = argparse.ArgumentParser(prog="cf_task_index.py")
126
- parser.add_argument("--task-file", required=True)
127
- parser.add_argument("--dag", action="store_true", help="输出拓扑批次与独立分组")
128
- parser.add_argument("--json", action="store_true")
129
- args = parser.parse_args(argv)
130
- try:
131
- data = index_data(args.task_file)
132
- except (OSError, ValueError) as exc:
133
- stdout.write(json.dumps({"ok": False, "error": str(exc)}, ensure_ascii=False))
134
- return 2
135
- if args.json or args.dag:
136
- stdout.write(json.dumps(data, ensure_ascii=False))
137
- else:
138
- lines = ["TASK 索引:"]
139
- for item in data["tasks"]:
140
- lines.append(f" - {item['id']}: {item['status']} (依赖: {', '.join(item['depends']) or '无'})")
141
- for index, batch in enumerate(data["batches"], 1):
142
- lines.append(f"批次 {index}(可独立开发): {', '.join(batch)}")
143
- stdout.write("\n".join(lines) + "\n")
144
- return 0
145
4
 
5
+ from codeflow.cli import cf_task_index as _impl
146
6
 
147
7
  if __name__ == "__main__":
148
- raise SystemExit(main())
8
+ raise SystemExit(_impl.main())
9
+ else:
10
+ sys.modules[__name__] = _impl