@michaelxwb/code-flow 0.6.4 → 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 -40
  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 +67 -35
  25. package/src/adapters/claude/commands/cf-task/status.md +12 -0
  26. package/src/adapters/claude/commands/cf-task/verify-e2e.md +72 -34
  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 -40
  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 +67 -35
  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 +73 -34
  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 -40
  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 +67 -35
  59. package/src/adapters/costrict/commands/cf-task/status.md +12 -0
  60. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +72 -34
  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 -40
  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 +68 -41
  77. package/src/adapters/opencode/commands/cf-task/status.md +13 -0
  78. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +72 -34
  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 -1714
  97. package/src/core/code-flow/scripts/cf_spec_gate.py +5 -257
  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 -380
  104. package/src/core/code-flow/scripts/cf_task_workflow.py +6 -103
  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} +37 -3
  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} +252 -26
  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 -299
  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,192 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- """Execute manifest scenarios with deterministic ordering and evidence."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import json
8
- from pathlib import Path
2
+ """Path-frozen entry shim: implementation lives in codeflow.cli.cf_acceptance_runner."""
9
3
  import sys
10
- from typing import IO, Mapping, Optional, Sequence
11
-
12
- from cf_exec_base import execution_key, run_command, invalidate_executions
13
- from cf_acceptance_schema import load_manifest, validate_execution_baseline
14
- from cf_acceptance_evidence import persist_results
15
-
16
-
17
- def _run(
18
- item: Mapping[str, object], root: Path, include_e2e: bool = False, deadline: Optional[float] = None,
19
- only_e2e: bool = False,
20
- ) -> dict[str, object]:
21
- scenario_id = str(item.get("id", "unknown"))
22
- kind = str(item.get("kind", "functional"))
23
- if only_e2e and kind != "e2e":
24
- return {"id": scenario_id, "kind": kind, "status": "not_included"}
25
- if kind == "manual":
26
- return {"id": scenario_id, "kind": kind, "status": "manual_pending"}
27
- if kind == "e2e" and not include_e2e and not only_e2e:
28
- return {"id": scenario_id, "kind": kind, "status": "e2e_deferred"}
29
- command = item.get("command")
30
- if not isinstance(command, list) or not command or not all(isinstance(value, str) for value in command):
31
- return {"id": scenario_id, "kind": kind, "status": "not_configured"}
32
- timeout = item.get("timeout", 60)
33
- timeout_value = float(timeout) if isinstance(timeout, (int, float)) else 60.0
34
- cwd = item.get("cwd", ".") if isinstance(item.get("cwd", "."), str) else "."
35
- outcome = run_command(command, str(root / cwd), timeout_value, deadline)
36
- if outcome["status"] == "deadline_exceeded":
37
- return {"id": scenario_id, "kind": kind, "status": "incomplete", "error": "deadline_exceeded"}
38
- if outcome["status"] == "timeout":
39
- return {"id": scenario_id, "kind": kind, "status": "failed", "error": "timeout",
40
- "output": str(outcome.get("stdout", ""))}
41
- if outcome["status"] == "spawn_error":
42
- return {"id": scenario_id, "kind": kind, "status": "failed", "error": str(outcome.get("stderr", ""))}
43
- if outcome["returncode"] == 0:
44
- return {"id": scenario_id, "kind": kind, "status": "passed", "exit_code": 0}
45
- output = (str(outcome.get("stdout", "")) + str(outcome.get("stderr", ""))).strip()[-4000:]
46
- return {"id": scenario_id, "kind": kind, "status": "failed", "exit_code": outcome["returncode"], "output": output}
47
-
48
-
49
- def _ordered(items: list[Mapping[str, object]]) -> list[Mapping[str, object]]:
50
- by_id = {str(item.get("id")): item for item in items}
51
- pending = set(by_id)
52
- ordered: list[Mapping[str, object]] = []
53
- while pending:
54
- ready: list[str] = []
55
- for item_id in by_id:
56
- if item_id not in pending:
57
- continue
58
- deps = by_id[item_id].get("depends_on", [])
59
- dep_ids = [str(dep) for dep in deps] if isinstance(deps, list) else []
60
- unknown = [dep for dep in dep_ids if dep not in by_id]
61
- if unknown:
62
- raise ValueError(f"acceptance scenario dependency missing: {', '.join(unknown)}")
63
- if all(dep not in pending for dep in dep_ids):
64
- ready.append(item_id)
65
- if not ready:
66
- raise ValueError("acceptance scenario dependency cycle")
67
- ordered.extend(by_id[item_id] for item_id in ready)
68
- pending.difference_update(ready)
69
- return ordered
70
-
71
-
72
- def _executable(items: list[Mapping[str, object]]) -> bool:
73
- """Draft completeness vs execution gate: at least one scenario must be
74
- executable — a functional scenario with a registered command, or a
75
- manual / e2e scenario with its own confirmation/deferral path. Zero
76
- executable scenarios must block execution acceptance, never pass it."""
77
- for item in items:
78
- command = item.get("command")
79
- if isinstance(command, list) and command and all(isinstance(value, str) for value in command):
80
- return True
81
- if str(item.get("kind", "")) in ("manual", "e2e"):
82
- return True
83
- return False
84
-
85
-
86
- def _owned(items: list[Mapping[str, object]], owner: str) -> list[Mapping[str, object]]:
87
- """Keep scenarios owned by the active TASK; ownerless legacy scenarios stay
88
- included so old manifests keep working. Cross-TASK depends_on edges are
89
- pruned: single-TASK ordering is enforced here, whole-demand ordering at
90
- the final (archive/E2E) gate."""
91
- if not owner:
92
- return items
93
- kept = [
94
- item for item in items
95
- if not isinstance(item.get("owner"), str) or not item.get("owner") or item.get("owner") == owner
96
- ]
97
- kept_ids = {str(item.get("id")) for item in kept}
98
- pruned: list[Mapping[str, object]] = []
99
- for item in kept:
100
- if not isinstance(item, dict):
101
- pruned.append(item)
102
- continue
103
- deps = item.get("depends_on")
104
- if isinstance(deps, list):
105
- item = {**item, "depends_on": [dep for dep in deps if str(dep) in kept_ids]}
106
- pruned.append(item)
107
- return pruned
108
-
109
-
110
- def _run_unique(
111
- ordered: list[Mapping[str, object]], root: Path, include_e2e: bool, deadline: Optional[float] = None,
112
- only_e2e: bool = False,
113
- ) -> list[dict[str, object]]:
114
- """Reuse identical execution semantics within a dependency-free batch."""
115
- cache: dict[str, Mapping[str, object]] = {}
116
- results: list[dict[str, object]] = []
117
- for item in ordered:
118
- scenario_id = str(item.get("id", "unknown"))
119
- command = item.get("command")
120
- key: Optional[str] = None
121
- if isinstance(command, list) and command and all(isinstance(value, str) for value in command):
122
- key = execution_key(command, str(root / str(item.get("cwd", "."))), float(item.get("timeout", 60)))
123
- key += json.dumps([item.get("kind", "functional"), include_e2e, only_e2e])
124
- if item.get("depends_on"):
125
- cache.clear()
126
- invalidate_executions()
127
- if key is not None and key in cache:
128
- shared = dict(cache[key])
129
- shared["id"] = scenario_id
130
- results.append(shared)
131
- continue
132
- result = _run(item, root, include_e2e, deadline, only_e2e)
133
- if key is not None:
134
- cache[key] = result
135
- results.append(result)
136
- return results
137
-
138
-
139
- def run_manifest(
140
- manifest_file: str,
141
- root: str,
142
- write_evidence: bool = False,
143
- include_e2e: bool = False,
144
- owner: str = "",
145
- deadline: Optional[float] = None,
146
- only_e2e: bool = False,
147
- ) -> dict[str, object]:
148
- data = load_manifest(Path(manifest_file))
149
- validate_execution_baseline(Path(manifest_file), data)
150
- scenarios = data["scenarios"]
151
- _ordered(scenarios) # Validate the complete DAG before owner filtering.
152
- items = _owned(scenarios, owner)
153
- ordered = _ordered(items)
154
- results = _run_unique(ordered, Path(root), include_e2e or only_e2e, deadline, only_e2e)
155
- if write_evidence:
156
- persist_results(Path(manifest_file), data, results)
157
- if not _executable(items):
158
- return {"decision": "block", "results": results, "error": "no_executable_scenarios"}
159
- allowed = ("passed", "manual_pending", "e2e_deferred", "not_included")
160
- decision = "pass" if all(item["status"] in allowed for item in results) else "block"
161
- error = ""
162
- if decision == "block":
163
- pending = sorted({str(item["id"]) for item in results if item["status"] == "incomplete"})
164
- failed = sorted({str(item["id"]) for item in results if item["status"] not in (*allowed, "incomplete")})
165
- error = ",".join((["incomplete:" + ",".join(pending)] if pending else []) + (["failed:" + ",".join(failed)] if failed else [])) or "acceptance scenario failed"
166
- outcome: dict[str, object] = {"decision": decision, "results": results}
167
- if error:
168
- outcome["error"] = error
169
- return outcome
170
-
171
-
172
- def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
173
- parser = argparse.ArgumentParser()
174
- parser.add_argument("--manifest", required=True)
175
- parser.add_argument("--root", required=True)
176
- parser.add_argument("--write-evidence", action="store_true")
177
- parser.add_argument("--include-e2e", action="store_true", help="Execute E2E scenarios (deferred by default)")
178
- parser.add_argument("--only-e2e", action="store_true", help="Execute only E2E scenarios; functional/manual are reported not_included")
179
- parser.add_argument("--owner", default="", help="Only execute scenarios owned by this TASK (plus ownerless legacy ones)")
180
- parser.add_argument("--deadline", type=float, default=0.0, help="Absolute monotonic deadline propagated from the entry gate (0 = unbounded)")
181
- args = parser.parse_args(argv)
182
- try:
183
- result = run_manifest(args.manifest, args.root, args.write_evidence, args.include_e2e, args.owner, args.deadline or None, args.only_e2e)
184
- stdout.write(json.dumps(result, ensure_ascii=False))
185
- return 0 if result["decision"] == "pass" else 3
186
- except (OSError, ValueError, json.JSONDecodeError) as exc:
187
- stdout.write(json.dumps({"decision": "block", "error": str(exc)}, ensure_ascii=False))
188
- return 2
189
4
 
5
+ from codeflow.cli import cf_acceptance_runner as _impl
190
6
 
191
7
  if __name__ == "__main__":
192
- raise SystemExit(main())
8
+ raise SystemExit(_impl.main())
9
+ else:
10
+ sys.modules[__name__] = _impl
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ """Path-frozen entry shim: implementation lives in codeflow.cli.cf_audit."""
3
+ import sys
4
+
5
+ from codeflow.cli import cf_audit as _impl
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(_impl.main())
9
+ else:
10
+ sys.modules[__name__] = _impl
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ """Path-frozen entry shim: implementation lives in codeflow.hooks.cf_codex_hook."""
3
+ import sys
4
+
5
+ from codeflow.hooks import cf_codex_hook as _impl
6
+
7
+ if __name__ == "__main__":
8
+ _impl.main()
9
+ else:
10
+ sys.modules[__name__] = _impl
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ """Path-frozen entry shim: implementation lives in codeflow.cli.cf_design_lint."""
3
+ import sys
4
+
5
+ from codeflow.cli import cf_design_lint as _impl
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(_impl.main())
9
+ else:
10
+ sys.modules[__name__] = _impl
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ """Path-frozen entry shim: implementation lives in codeflow.cli.cf_evidence_import."""
3
+ import sys
4
+
5
+ from codeflow.cli import cf_evidence_import as _impl
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(_impl.main())
9
+ else:
10
+ sys.modules[__name__] = _impl
@@ -1,56 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- """cf_feedback: mark a check as false positive (FEAT-02 对话即反馈).
3
-
4
- The violation feedback text tells the user "误报可直接告诉我"; the agent then
5
- runs this CLI on the user's behalf — no new slash command, no config editing.
6
-
7
- Usage: python3 cf_feedback.py ignore <check-id>
8
- Exit codes: 0 = recorded / 1 = usage error / 2 = unknown check id.
9
- stdout: one JSON line {"check_id", "fp_count", "hit_count", "disabled"}.
10
- """
11
- import json
12
- import os
2
+ """Path-frozen entry shim: implementation lives in codeflow.cli.cf_feedback."""
13
3
  import sys
14
4
 
15
- from cf_checks import load_check_state, load_spec_checks, record_false_positive
16
- from cf_core import build_effective_mapping, load_config, normalize_spec_entry
17
- from cf_log import append_event
18
-
19
-
20
- def known_check_ids(project_root: str) -> set:
21
- """All check ids: declared in any configured spec, or already in state."""
22
- ids = set(load_check_state(project_root).keys())
23
- config = load_config(project_root)
24
- mapping = build_effective_mapping(
25
- project_root, (config or {}).get("path_mapping") or {}
26
- )
27
- specs_root = os.path.join(project_root, ".code-flow", "specs")
28
- for domain_cfg in mapping.values():
29
- for entry in (domain_cfg or {}).get("specs") or []:
30
- rel = normalize_spec_entry(entry).get("path")
31
- if not rel:
32
- continue
33
- checks, _ = load_spec_checks(os.path.join(specs_root, rel))
34
- ids.update(check["id"] for check in checks)
35
- return ids
36
-
37
-
38
- def main(argv: list) -> int:
39
- if len(argv) != 2 or argv[0] != "ignore":
40
- sys.stderr.write("usage: cf_feedback.py ignore <check-id>\n")
41
- return 1
42
- check_id = argv[1]
43
- project_root = os.getcwd()
44
- if check_id not in known_check_ids(project_root):
45
- sys.stderr.write(f"unknown check id: {check_id}\n")
46
- return 2
47
- append_event(
48
- project_root, "false_positive", {"check_id": check_id}, "cf-feedback"
49
- )
50
- result = record_false_positive(project_root, check_id)
51
- sys.stdout.write(json.dumps(result, ensure_ascii=False) + "\n")
52
- return 0
53
-
5
+ from codeflow.cli import cf_feedback as _impl
54
6
 
55
7
  if __name__ == "__main__":
56
- sys.exit(main(sys.argv[1:]))
8
+ sys.exit(_impl.main(sys.argv[1:]))
9
+ else:
10
+ sys.modules[__name__] = _impl
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ """Path-frozen entry shim: implementation lives in codeflow.hooks.cf_opencode_event."""
3
+ import sys
4
+
5
+ from codeflow.hooks import cf_opencode_event as _impl
6
+
7
+ if __name__ == "__main__":
8
+ _impl.main()
9
+ else:
10
+ sys.modules[__name__] = _impl
@@ -1,196 +1,11 @@
1
1
  #!/usr/bin/env python3
2
- """PostToolUse Hook: compliance feedback after Edit/Write (FEAT-01).
3
-
4
- Flow (spec-quality-loop.design.md §3.4):
5
- edited file → matched domains → aggregate frontmatter checks of their
6
- constraint specs → run against on-disk content → feedback via
7
- additionalContext (advice only, never blocking) → violation events.
8
-
9
- Protocol invariants (RULE-01/02):
10
- - always exit 0, stdout is JSON-only or empty (no-op);
11
- - violations only warn — there is no error severity;
12
- - same check+file reported once per session (RISK-05);
13
- - any internal failure degrades silently (stderr + degrade event).
14
-
15
- Input (stdin): {"tool_name", "tool_input": {"file_path"}, "session_id"}
16
- Output: {"hookSpecificOutput": {"hookEventName": "PostToolUse",
17
- "additionalContext": "..."}} or empty.
18
- """
19
- import json
20
- import os
2
+ """Path-frozen entry shim: implementation lives in codeflow.hooks.cf_post_hook."""
21
3
  import sys
22
4
 
23
- import cf_log
24
- from cf_checks import (
25
- load_check_state,
26
- load_spec_checks,
27
- record_hits,
28
- run_checks,
29
- save_check_state,
30
- )
31
- from cf_core import (
32
- _log,
33
- build_effective_mapping,
34
- effective_project_root,
35
- ensure_utf8_io,
36
- is_code_file,
37
- load_config,
38
- match_domains,
39
- normalize_path,
40
- normalize_spec_entry,
41
- resolve_enforcement,
42
- resolve_quality_loop,
43
- resolve_session_id,
44
- timing_log,
45
- )
46
-
47
- FEEDBACK_HINT = '如认为误报,直接告诉我"这是误报",我会标记忽略。'
48
- _REPORTED_KEY = "_reported"
49
-
50
-
51
- def _collect_checks(project_root: str, domains: list, mapping: dict) -> list:
52
- """Aggregate checks of all constraint specs in matched domains.
53
-
54
- Each check is annotated with its source spec path. Checks apply whenever
55
- their own `files` glob matches — independent of injection tag gating.
56
- """
57
- specs_root = os.path.join(project_root, ".code-flow", "specs")
58
- collected = []
59
- seen_specs = set()
60
- for domain in domains:
61
- for entry in (mapping.get(domain) or {}).get("specs") or []:
62
- cfg = normalize_spec_entry(entry)
63
- rel = cfg.get("path")
64
- if not rel or rel in seen_specs or not cfg.get("tags"):
65
- continue
66
- seen_specs.add(rel)
67
- checks, _ = load_spec_checks(os.path.join(specs_root, rel))
68
- for check in checks:
69
- item = dict(check)
70
- item["spec"] = rel
71
- collected.append(item)
72
- return collected
73
-
74
-
75
- def _session_reported(state: dict, sid: str) -> set:
76
- session = state.get(_REPORTED_KEY) or {}
77
- if session.get("sid") != sid:
78
- return set()
79
- return set(session.get("keys") or [])
80
-
81
-
82
- def _save_reported(project_root: str, state: dict, sid: str, keys: set) -> None:
83
- state[_REPORTED_KEY] = {"sid": sid, "keys": sorted(keys)}
84
- save_check_state(project_root, state)
85
-
86
-
87
- def _feedback_text(violations: list) -> str:
88
- lines = ["## Spec 合规反馈 (auto-check)", ""]
89
- for v in violations:
90
- lines.append(f"⚠ {v['message']}(规则: {v['spec']}#{v['check_id']})")
91
- lines.append(f" 违规行 {v['line_no']}: {v['line']}")
92
- lines.append("")
93
- lines.append("请修正以上违规后再继续。" + FEEDBACK_HINT)
94
- return "\n".join(lines)
95
-
96
-
97
- def main() -> None:
98
- try:
99
- ensure_utf8_io()
100
- raw = sys.stdin.read()
101
- if not raw.strip():
102
- return
103
- data = json.loads(raw)
104
- tool_name = data.get("tool_name", "")
105
- file_path = (data.get("tool_input") or {}).get("file_path", "")
106
- if tool_name not in {"Edit", "Write", "MultiEdit"}:
107
- return
108
- if not isinstance(file_path, str) or not file_path:
109
- return
110
-
111
- session_root = os.getcwd()
112
- abs_path = file_path if os.path.isabs(file_path) else os.path.join(session_root, file_path)
113
- # 并行子 agent 在 .code-flow/worktrees/... 内编辑时路由到该 worktree 的规格与状态
114
- project_root = effective_project_root(session_root, abs_path)
115
- config = load_config(project_root)
116
- if not config:
117
- return
118
- if resolve_enforcement(config) == "inject":
119
- return
120
- quality_loop = resolve_quality_loop(config)
121
- if not quality_loop["post_check"]:
122
- return
123
-
124
- rel_path = normalize_path(os.path.relpath(abs_path, project_root))
125
- sid = resolve_session_id(data)
126
- cf_log.append_event(project_root, "edit", {"file": rel_path, "tool": tool_name}, sid)
127
- if not is_code_file(rel_path, config.get("quality_loop") or {}):
128
- return
129
- mapping = build_effective_mapping(
130
- project_root, config.get("path_mapping") or {}
131
- )
132
- domains = match_domains(rel_path, mapping)
133
- if not domains:
134
- return
135
-
136
- checks = _collect_checks(project_root, domains, mapping)
137
- if not checks:
138
- return
139
-
140
- try:
141
- with open(abs_path, "r", encoding="utf-8") as f:
142
- content = f.read()
143
- except Exception:
144
- return
145
-
146
- state = load_check_state(project_root)
147
- violations, skipped = run_checks(checks, rel_path, content, state)
148
- for item in skipped:
149
- cf_log.degrade(
150
- project_root, "post_check",
151
- f"{item['check_id']}:{item['reason']}", sid,
152
- )
153
- if not violations:
154
- return
155
-
156
- spec_by_check = {c["id"]: c["spec"] for c in checks}
157
- reported = _session_reported(state, sid)
158
- fresh = []
159
- for v in violations:
160
- key = f"{v['check_id']}|{v['file']}"
161
- if key in reported:
162
- continue
163
- v["spec"] = spec_by_check.get(v["check_id"], "")
164
- fresh.append(v)
165
- reported.add(key)
166
- if not fresh:
167
- return
168
-
169
- # hit_count feeds the false-positive rate denominator (RULE-04)
170
- record_hits(project_root, sorted({v["check_id"] for v in fresh}))
171
- state = load_check_state(project_root)
172
- _save_reported(project_root, state, sid, reported)
173
-
174
- for v in fresh:
175
- cf_log.append_event(
176
- project_root, "violation",
177
- {"check_id": v["check_id"], "spec": v["spec"],
178
- "file": v["file"], "severity": v["severity"]},
179
- sid,
180
- )
181
-
182
- payload = {
183
- "hookSpecificOutput": {
184
- "hookEventName": "PostToolUse",
185
- "additionalContext": _feedback_text(fresh),
186
- }
187
- }
188
- sys.stdout.write(json.dumps(payload, ensure_ascii=False))
189
- except Exception as exc:
190
- _log(f"cf_post_hook error: {exc}")
191
- return
192
-
5
+ from codeflow.hooks import cf_post_hook as _impl
193
6
 
194
7
  if __name__ == "__main__":
195
- main()
196
- timing_log("cf_post_hook")
8
+ _impl.main()
9
+ _impl.timing_log("cf_post_hook")
10
+ else:
11
+ sys.modules[__name__] = _impl
@@ -1,104 +1,11 @@
1
1
  #!/usr/bin/env python3
2
- """PreToolUse hook: advisory Context/scope guidance on Edit/Write/MultiEdit.
3
-
4
- Hot-path policy: never deny the tool; inject context or guidance instead.
5
- Hard enforcement lives at the checkpoints (start / done / stop), not here.
6
- """
7
-
8
- import json
9
- import os
10
- from pathlib import Path
2
+ """Path-frozen entry shim: implementation lives in codeflow.hooks.cf_pre_tool_hook."""
11
3
  import sys
12
4
 
13
- import cf_log
14
- from cf_core import (
15
- _log,
16
- effective_project_root,
17
- ensure_utf8_io,
18
- load_config,
19
- normalize_path,
20
- resolve_enforcement,
21
- resolve_session_id,
22
- timing_log,
23
- )
24
- from cf_session_state import load_session_state, save_session_state
25
- from cf_spec_context import injection_version, load_active_task, load_context
26
- from cf_spec_resolver import resolve_candidate_headers
27
- from cf_spec_router import RouterError, route_prompt
28
-
29
- _WARN_TEXT = "⚠ Spec Workflow 校验未通过(warn 模式):{message} — 建议运行 cf-spec doctor 检查。"
30
-
31
-
32
- def _active_expansion(root: str, relative: str) -> tuple[str, ...]:
33
- marker = Path(root) / ".code-flow/.active-task.json"
34
- if not marker.exists():
35
- return ()
36
- active = load_active_task(root)
37
- context = load_context(str(Path(root) / active.task_dir / "spec-context.yml"))
38
- bound = {item.spec_id for item in context.bindings}
39
- return tuple(
40
- item.spec_id for item in resolve_candidate_headers(root, "code", (relative,))
41
- if item.spec_id not in bound and item.enforcement == "required"
42
- and getattr(item, "scope", "path") != "unmatched"
43
- )
44
-
45
-
46
- def _inject(text: str) -> dict[str, object]:
47
- return {"hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": text}}
48
-
49
-
50
- def main() -> None:
51
- try:
52
- ensure_utf8_io()
53
- raw = sys.stdin.read()
54
- if not raw.strip():
55
- return
56
- data = json.loads(raw)
57
- tool_name = data.get("tool_name", "")
58
- file_path = (data.get("tool_input") or {}).get("file_path", "")
59
- if tool_name not in {"Edit", "Write", "MultiEdit"} or not isinstance(file_path, str) or not file_path:
60
- return
61
- session_root = os.getcwd()
62
- absolute = file_path if os.path.isabs(file_path) else os.path.join(session_root, file_path)
63
- # 并行子 agent 在 .code-flow/worktrees/... 内编辑时路由到该 worktree 的上下文
64
- root = effective_project_root(session_root, absolute)
65
- sid = resolve_session_id(data)
66
- config = load_config(root)
67
- enforcement = resolve_enforcement(config) if config else "required"
68
- relative = normalize_path(os.path.relpath(absolute, root))
69
- try:
70
- expanded = _active_expansion(root, relative)
71
- result = route_prompt(root, (relative,), sid)
72
- except RouterError as exc:
73
- if enforcement == "required":
74
- sys.stdout.write(json.dumps(_inject(f"SPEC_WORKFLOW_BLOCKED [{exc.code}]: {exc}. Run cf-spec doctor before continuing."), ensure_ascii=False))
75
- elif enforcement == "warn":
76
- sys.stdout.write(json.dumps(_inject(_WARN_TEXT.format(message=str(exc))), ensure_ascii=False))
77
- return
78
- if expanded:
79
- message = f"⚠ 该路径引入未绑定 required Spec:{', '.join(expanded)} — 建议先 refresh Context / Plan 再继续编辑。"
80
- sys.stdout.write(json.dumps(_inject(message), ensure_ascii=False))
81
- return
82
- text = result.text
83
- if result.mode == "task" and result.context_sha256:
84
- # The projection is already in context from the prompt route unless
85
- # the session/task/contract version changed; skip per-edit
86
- # re-injection. Same version key as the prompt hook so a TASK
87
- # switch re-injects here too.
88
- state = load_session_state(root)
89
- version = injection_version(root, sid, result.context_sha256)
90
- if state.get("injected_version") == version:
91
- text = ""
92
- else:
93
- state["injected_version"] = version
94
- save_session_state(root, state)
95
- if text:
96
- sys.stdout.write(json.dumps(_inject(text), ensure_ascii=False))
97
- cf_log.append_event(root, "edit_intent", {"file": relative, "tool": tool_name, "specs": list(result.specs)}, sid)
98
- except (json.JSONDecodeError, OSError, ValueError) as exc:
99
- _log(f"cf_pre_tool_hook error: {exc}")
100
-
5
+ from codeflow.hooks import cf_pre_tool_hook as _impl
101
6
 
102
7
  if __name__ == "__main__":
103
- main()
104
- timing_log("cf_pre_tool_hook")
8
+ _impl.main()
9
+ _impl.timing_log("cf_pre_tool_hook")
10
+ else:
11
+ sys.modules[__name__] = _impl
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env python3
2
+ """Path-frozen entry shim: implementation lives in codeflow.cli.cf_scenarios."""
3
+ import sys
4
+
5
+ from codeflow.cli import cf_scenarios as _impl
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(_impl.main())
9
+ else:
10
+ sys.modules[__name__] = _impl