@michaelxwb/code-flow 0.8.1 → 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 (268) hide show
  1. package/README.md +73 -152
  2. package/bin/code-flow.js +12 -0
  3. package/docs/README.md +19 -0
  4. package/docs/USAGE.md +156 -1417
  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 +18 -21
  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 +12 -12
  47. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-full.md +17 -19
  48. package/{src/core/code-flow/specs/shared/design → templates/workflow}/design-lite.md +12 -12
  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/acceptance-contract.md +0 -217
  54. package/docs/ci/acceptance-evidence.md +0 -77
  55. package/docs/migrations/runtime-0.7.0.md +0 -41
  56. package/docs/migrations/runtime-0.8.0.md +0 -154
  57. package/docs/migrations/runtime-0.9.0.md +0 -40
  58. package/docs/migrations/spec-workflow-0.6.0.md +0 -42
  59. package/docs/spec-injection-hardening.md +0 -54
  60. package/docs/spec-injection-performance.json +0 -73
  61. package/docs/spec-optimization.md +0 -12
  62. package/src/adapters/claude/CLAUDE.md +0 -45
  63. package/src/adapters/claude/commands/cf-init.md +0 -262
  64. package/src/adapters/claude/commands/cf-learn.md +0 -291
  65. package/src/adapters/claude/commands/cf-spec.md +0 -87
  66. package/src/adapters/claude/commands/cf-stats.md +0 -96
  67. package/src/adapters/claude/commands/cf-sync.md +0 -39
  68. package/src/adapters/claude/commands/cf-task/align.md +0 -331
  69. package/src/adapters/claude/commands/cf-task/archive.md +0 -155
  70. package/src/adapters/claude/commands/cf-task/block.md +0 -49
  71. package/src/adapters/claude/commands/cf-task/graph.md +0 -92
  72. package/src/adapters/claude/commands/cf-task/note.md +0 -248
  73. package/src/adapters/claude/commands/cf-task/plan.md +0 -477
  74. package/src/adapters/claude/commands/cf-task/prd.md +0 -173
  75. package/src/adapters/claude/commands/cf-task/start.md +0 -317
  76. package/src/adapters/claude/commands/cf-task/status.md +0 -71
  77. package/src/adapters/claude/commands/cf-task/verify-e2e.md +0 -148
  78. package/src/adapters/claude/commands/cf-validate.md +0 -100
  79. package/src/adapters/claude/settings.local.json +0 -50
  80. package/src/adapters/codex/AGENTS.md +0 -45
  81. package/src/adapters/codex/config.toml +0 -3
  82. package/src/adapters/codex/hooks.json +0 -54
  83. package/src/adapters/codex/skills/cf-init/SKILL.md +0 -273
  84. package/src/adapters/codex/skills/cf-learn/SKILL.md +0 -296
  85. package/src/adapters/codex/skills/cf-spec/SKILL.md +0 -88
  86. package/src/adapters/codex/skills/cf-stats/SKILL.md +0 -97
  87. package/src/adapters/codex/skills/cf-sync/SKILL.md +0 -45
  88. package/src/adapters/codex/skills/cf-task-align/SKILL.md +0 -332
  89. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +0 -156
  90. package/src/adapters/codex/skills/cf-task-block/SKILL.md +0 -50
  91. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +0 -93
  92. package/src/adapters/codex/skills/cf-task-note/SKILL.md +0 -245
  93. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +0 -478
  94. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +0 -174
  95. package/src/adapters/codex/skills/cf-task-start/SKILL.md +0 -318
  96. package/src/adapters/codex/skills/cf-task-status/SKILL.md +0 -72
  97. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +0 -145
  98. package/src/adapters/codex/skills/cf-validate/SKILL.md +0 -102
  99. package/src/adapters/commands.json +0 -115
  100. package/src/adapters/costrict/CLAUDE.md +0 -45
  101. package/src/adapters/costrict/commands/cf-init.md +0 -262
  102. package/src/adapters/costrict/commands/cf-learn.md +0 -291
  103. package/src/adapters/costrict/commands/cf-spec.md +0 -87
  104. package/src/adapters/costrict/commands/cf-stats.md +0 -96
  105. package/src/adapters/costrict/commands/cf-sync.md +0 -39
  106. package/src/adapters/costrict/commands/cf-task/align.md +0 -331
  107. package/src/adapters/costrict/commands/cf-task/archive.md +0 -155
  108. package/src/adapters/costrict/commands/cf-task/block.md +0 -49
  109. package/src/adapters/costrict/commands/cf-task/graph.md +0 -92
  110. package/src/adapters/costrict/commands/cf-task/note.md +0 -248
  111. package/src/adapters/costrict/commands/cf-task/plan.md +0 -477
  112. package/src/adapters/costrict/commands/cf-task/prd.md +0 -173
  113. package/src/adapters/costrict/commands/cf-task/start.md +0 -317
  114. package/src/adapters/costrict/commands/cf-task/status.md +0 -71
  115. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +0 -148
  116. package/src/adapters/costrict/commands/cf-validate.md +0 -100
  117. package/src/adapters/costrict/settings.local.json +0 -50
  118. package/src/adapters/install-plan.json +0 -100
  119. package/src/adapters/opencode/AGENTS.md +0 -45
  120. package/src/adapters/opencode/commands/cf-init.md +0 -281
  121. package/src/adapters/opencode/commands/cf-learn.md +0 -295
  122. package/src/adapters/opencode/commands/cf-spec.md +0 -91
  123. package/src/adapters/opencode/commands/cf-stats.md +0 -100
  124. package/src/adapters/opencode/commands/cf-sync.md +0 -44
  125. package/src/adapters/opencode/commands/cf-task/align.md +0 -331
  126. package/src/adapters/opencode/commands/cf-task/archive.md +0 -160
  127. package/src/adapters/opencode/commands/cf-task/block.md +0 -54
  128. package/src/adapters/opencode/commands/cf-task/graph.md +0 -97
  129. package/src/adapters/opencode/commands/cf-task/note.md +0 -253
  130. package/src/adapters/opencode/commands/cf-task/plan.md +0 -477
  131. package/src/adapters/opencode/commands/cf-task/prd.md +0 -178
  132. package/src/adapters/opencode/commands/cf-task/start.md +0 -317
  133. package/src/adapters/opencode/commands/cf-task/status.md +0 -76
  134. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +0 -148
  135. package/src/adapters/opencode/commands/cf-validate.md +0 -105
  136. package/src/adapters/opencode/opencode.json +0 -3
  137. package/src/adapters/opencode/plugins/code-flow/index.js +0 -162
  138. package/src/adapters/opencode/plugins/code-flow/package.json +0 -6
  139. package/src/adapters/platform-bindings.json +0 -370
  140. package/src/cli.js +0 -1076
  141. package/src/core/code-flow/config.yml +0 -161
  142. package/src/core/code-flow/runtime-commands.json +0 -259
  143. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +0 -10
  144. package/src/core/code-flow/scripts/cf_acceptance_runner.py +0 -10
  145. package/src/core/code-flow/scripts/cf_audit.py +0 -10
  146. package/src/core/code-flow/scripts/cf_codex_hook.py +0 -10
  147. package/src/core/code-flow/scripts/cf_design_lint.py +0 -10
  148. package/src/core/code-flow/scripts/cf_evidence_import.py +0 -10
  149. package/src/core/code-flow/scripts/cf_feedback.py +0 -10
  150. package/src/core/code-flow/scripts/cf_opencode_event.py +0 -10
  151. package/src/core/code-flow/scripts/cf_post_hook.py +0 -11
  152. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +0 -11
  153. package/src/core/code-flow/scripts/cf_scenarios.py +0 -10
  154. package/src/core/code-flow/scripts/cf_spec_context.py +0 -10
  155. package/src/core/code-flow/scripts/cf_spec_gate.py +0 -10
  156. package/src/core/code-flow/scripts/cf_spec_session.py +0 -10
  157. package/src/core/code-flow/scripts/cf_stats.py +0 -10
  158. package/src/core/code-flow/scripts/cf_stop_hook.py +0 -11
  159. package/src/core/code-flow/scripts/cf_sync.py +0 -10
  160. package/src/core/code-flow/scripts/cf_task_index.py +0 -10
  161. package/src/core/code-flow/scripts/cf_task_parallel.py +0 -10
  162. package/src/core/code-flow/scripts/cf_task_workflow.py +0 -10
  163. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +0 -11
  164. package/src/core/code-flow/scripts/cf_validation.py +0 -10
  165. package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
  166. package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
  167. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +0 -347
  168. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +0 -660
  169. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +0 -202
  170. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +0 -60
  171. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +0 -73
  172. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +0 -282
  173. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +0 -266
  174. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +0 -172
  175. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +0 -326
  176. package/src/core/code-flow/scripts/codeflow/acceptance/cf_checks.py +0 -333
  177. package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +0 -197
  178. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +0 -174
  179. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +0 -300
  180. package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +0 -502
  181. package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
  182. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +0 -347
  183. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +0 -1490
  184. package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +0 -533
  185. package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +0 -172
  186. package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +0 -607
  187. package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +0 -53
  188. package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +0 -58
  189. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +0 -1175
  190. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +0 -60
  191. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +0 -46
  192. package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +0 -540
  193. package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +0 -144
  194. package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +0 -177
  195. package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +0 -1142
  196. package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +0 -742
  197. package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +0 -31
  198. package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
  199. package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +0 -67
  200. package/src/core/code-flow/scripts/codeflow/core/cf_core.py +0 -609
  201. package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +0 -325
  202. package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +0 -57
  203. package/src/core/code-flow/scripts/codeflow/core/cf_git.py +0 -92
  204. package/src/core/code-flow/scripts/codeflow/core/cf_log.py +0 -160
  205. package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +0 -77
  206. package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +0 -26
  207. package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +0 -33
  208. package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +0 -32
  209. package/src/core/code-flow/scripts/codeflow/core/cf_session_state.py +0 -37
  210. package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +0 -53
  211. package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +0 -9
  212. package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +0 -408
  213. package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +0 -253
  214. package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
  215. package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +0 -527
  216. package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
  217. package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +0 -95
  218. package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +0 -98
  219. package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +0 -30
  220. package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +0 -41
  221. package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +0 -292
  222. package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +0 -161
  223. package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
  224. package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +0 -412
  225. package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +0 -301
  226. package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +0 -149
  227. package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +0 -111
  228. package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +0 -114
  229. package/src/core/code-flow/scripts/codeflow/spec/cf_scan.py +0 -367
  230. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +0 -55
  231. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +0 -477
  232. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +0 -119
  233. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_metadata.py +0 -387
  234. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_migrate.py +0 -529
  235. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +0 -193
  236. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +0 -187
  237. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_resolver.py +0 -369
  238. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_router.py +0 -304
  239. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_verify.py +0 -687
  240. package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +0 -330
  241. package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +0 -83
  242. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +0 -755
  243. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_transaction.py +0 -110
  244. package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
  245. package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +0 -867
  246. package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +0 -178
  247. package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +0 -635
  248. package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +0 -251
  249. package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +0 -570
  250. package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +0 -73
  251. package/src/core/code-flow/scripts/runtime-resources.json +0 -29
  252. package/src/core/code-flow/specs/backend/_map.md +0 -56
  253. package/src/core/code-flow/specs/backend/code-quality-performance.md +0 -59
  254. package/src/core/code-flow/specs/backend/database.md +0 -51
  255. package/src/core/code-flow/specs/backend/directory-structure.md +0 -54
  256. package/src/core/code-flow/specs/backend/logging.md +0 -50
  257. package/src/core/code-flow/specs/backend/platform-rules.md +0 -55
  258. package/src/core/code-flow/specs/frontend/_map.md +0 -52
  259. package/src/core/code-flow/specs/frontend/component-specs.md +0 -68
  260. package/src/core/code-flow/specs/frontend/directory-structure.md +0 -53
  261. package/src/core/code-flow/specs/frontend/quality-standards.md +0 -58
  262. package/src/core/code-flow/specs/shared/_map.md +0 -57
  263. package/src/core/code-flow/specs/shared/scenarios-template.md +0 -55
  264. package/src/core/code-flow/tasks/.gitkeep +0 -0
  265. package/src/core/code-flow/validation.yml +0 -59
  266. package/src/migrate/runtime.js +0 -213
  267. package/src/migrate/spec-workflow.js +0 -253
  268. package/src/runtime.js +0 -219
@@ -1,529 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Read-only migration preflight and staging transformer."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import hashlib
8
- import json
9
- from pathlib import Path
10
- import re
11
- import shutil
12
- import sys
13
- from typing import IO, Mapping, Optional, Sequence
14
-
15
- import yaml
16
-
17
- if __package__ in (None, ""):
18
- sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
19
-
20
- from codeflow.cli.cf_spec_context import BindingInput, bind_specs, new_context, save_context
21
- from codeflow.spec.cf_spec_metadata import SpecMetadata, SpecMetadataError, load_spec_metadata
22
- from codeflow.spec.cf_spec_resolver import SCOPE_PRIORITY, SpecCandidate
23
-
24
-
25
- TARGET_VERSION = "0.6.0"
26
- TARGET_SCHEMA = 1
27
- PACKAGE_ROOT = Path(__file__).resolve().parents[6]
28
- _DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
29
- _SUFFIXES = (".frontend.design.md", ".backend.design.md", ".design.md", ".prd.md", ".md")
30
-
31
-
32
- def _sha256(path: Path) -> str:
33
- return hashlib.sha256(path.read_bytes()).hexdigest()
34
-
35
-
36
- def _relative(root: Path, path: Path) -> str:
37
- return path.relative_to(root).as_posix()
38
-
39
-
40
- def _issue(code: str, path: str, message: str) -> dict[str, str]:
41
- return {"code": code, "path": path, "message": message}
42
-
43
-
44
- def _version_tuple(value: str) -> Optional[tuple[int, int, int]]:
45
- match = re.fullmatch(r"(\d+)\.(\d+)\.(\d+)", value.strip())
46
- return tuple(int(item) for item in match.groups()) if match else None
47
-
48
-
49
- def _source_version(flow: Path) -> str:
50
- path = flow / ".version"
51
- try:
52
- return path.read_text(encoding="utf-8").strip() if path.is_file() else "missing"
53
- except OSError:
54
- return "unreadable"
55
-
56
-
57
- def _schema(flow: Path) -> Optional[int]:
58
- path = flow / "config.yml"
59
- try:
60
- data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
61
- except (OSError, yaml.YAMLError):
62
- return None
63
- workflow = data.get("spec_workflow") if isinstance(data, dict) else None
64
- value = workflow.get("schema_version") if isinstance(workflow, dict) else 0
65
- return value if isinstance(value, int) else None
66
-
67
-
68
- def _version_issues(version: str, schema: Optional[int]) -> list[dict[str, str]]:
69
- parsed = _version_tuple(version)
70
- if version in ("missing", "unreadable"):
71
- return []
72
- if parsed is None:
73
- return [_issue("invalid_version", ".code-flow/.version", version)]
74
- if parsed < (0, 4, 2):
75
- return [_issue("unsupported_version", ".code-flow/.version", version)]
76
- if parsed > (0, 6, 0) or (schema is not None and schema > TARGET_SCHEMA):
77
- return [_issue("future_version", ".code-flow/.version", version)]
78
- if parsed == (0, 6, 0) and schema != TARGET_SCHEMA:
79
- return [_issue("incomplete_target_schema", ".code-flow/config.yml", str(schema))]
80
- return []
81
-
82
-
83
- def _scan_specs(root: Path, flow: Path) -> tuple[list[dict[str, object]], list[dict[str, str]]]:
84
- manifest: list[dict[str, object]] = []
85
- unresolved: list[dict[str, str]] = []
86
- seen: dict[str, str] = {}
87
- for path in sorted((flow / "specs").rglob("*.md")):
88
- relative = _relative(root, path)
89
- spec_relative = path.relative_to(flow / "specs").as_posix()
90
- if "/_session/" in f"/{relative}/" or path.name == "_map.md" or spec_relative.startswith("shared/"):
91
- continue
92
- try:
93
- metadata = load_spec_metadata(str(path))
94
- except SpecMetadataError as exc:
95
- unresolved.append(_issue("invalid_spec", relative, exc.message))
96
- continue
97
- if metadata.id in seen:
98
- unresolved.append(_issue("duplicate_spec_id", relative, metadata.id))
99
- seen[metadata.id] = relative
100
- manifest.append({"id": metadata.id, "path": relative, "sha256": metadata.hashes.file_sha256})
101
- return manifest, unresolved
102
-
103
-
104
- def _demand_key(name: str) -> str:
105
- for suffix in _SUFFIXES:
106
- if name.endswith(suffix):
107
- return name[:-len(suffix)]
108
- return name
109
-
110
-
111
- def _target_conflict(files: Sequence[Path], target: Path) -> bool:
112
- if not target.exists():
113
- return False
114
- for source in files:
115
- destination = target / source.name
116
- if destination.exists() and destination.read_bytes() != source.read_bytes():
117
- return True
118
- return False
119
-
120
-
121
- def _flat_transforms(root: Path, date_dir: Path) -> tuple[list[dict[str, object]], list[dict[str, str]]]:
122
- groups: dict[str, list[Path]] = {}
123
- for path in sorted(date_dir.glob("*.md")):
124
- groups.setdefault(_demand_key(path.name), []).append(path)
125
- transforms: list[dict[str, object]] = []
126
- unresolved: list[dict[str, str]] = []
127
- for demand, files in sorted(groups.items()):
128
- target = date_dir / demand
129
- if _target_conflict(files, target):
130
- unresolved.append(_issue("target_conflict", _relative(root, target), demand))
131
- transforms.append({
132
- "demand": demand,
133
- "files": [_relative(root, path) for path in files],
134
- "target": _relative(root, target),
135
- })
136
- return transforms, unresolved
137
-
138
-
139
- def _scan_tasks(root: Path, flow: Path) -> tuple[list[dict[str, object]], list[dict[str, str]], list[dict[str, object]]]:
140
- tasks = flow / "tasks"
141
- transforms: list[dict[str, object]] = []
142
- unresolved: list[dict[str, str]] = []
143
- active: list[dict[str, object]] = []
144
- if not tasks.is_dir():
145
- return transforms, unresolved, active
146
- for date_dir in sorted(path for path in tasks.iterdir() if path.is_dir() and _DATE_RE.fullmatch(path.name)):
147
- found, issues = _flat_transforms(root, date_dir)
148
- transforms.extend(found)
149
- unresolved.extend(issues)
150
- for demand_dir in sorted(path for path in date_dir.iterdir() if path.is_dir()):
151
- mains = [path for path in demand_dir.glob("*.md") if not path.name.endswith((".prd.md", ".design.md"))]
152
- if len(mains) > 1:
153
- unresolved.append(_issue("multiple_task_files", _relative(root, demand_dir), str(len(mains))))
154
- active.append({"path": _relative(root, demand_dir), "active": True})
155
- return transforms, unresolved, active
156
-
157
-
158
- def _archived(root: Path, flow: Path) -> list[dict[str, object]]:
159
- path = flow / "tasks" / "archived"
160
- if not path.is_dir():
161
- return []
162
- return [
163
- {"path": _relative(root, item), "sha256": _sha256(item), "active": False}
164
- for item in sorted(path.rglob("*")) if item.is_file()
165
- ]
166
-
167
-
168
- def preflight(root: str) -> dict[str, object]:
169
- project = Path(root).resolve()
170
- flow = project / ".code-flow"
171
- version, schema = _source_version(flow), _schema(flow)
172
- specs, spec_issues = _scan_specs(project, flow)
173
- transforms, task_issues, active = _scan_tasks(project, flow)
174
- unresolved = [*_version_issues(version, schema), *spec_issues, *task_issues]
175
- warnings = [] if version != "missing" else [_issue("version_mismatch", ".code-flow/.version", "missing")]
176
- return {
177
- "project_root": str(project),
178
- "source_package_version": version,
179
- "source_schema": schema,
180
- "target_package_version": TARGET_VERSION,
181
- "target_schema": TARGET_SCHEMA,
182
- "specs": specs,
183
- "layout_transforms": transforms,
184
- "active_demands": active,
185
- "archived": _archived(project, flow),
186
- "unresolved": unresolved,
187
- "warnings": warnings,
188
- "ready": not unresolved,
189
- }
190
-
191
-
192
- def _copy_source(project: Path, staging: Path) -> None:
193
- source = project / ".code-flow"
194
- target = staging / ".code-flow"
195
- shutil.copytree(
196
- source,
197
- target,
198
- dirs_exist_ok=True,
199
- ignore=shutil.ignore_patterns("migrations", "_session", ".active-task.json", ".active-task.lock"),
200
- )
201
-
202
-
203
- def _copy_runtime(staging: Path) -> None:
204
- source = PACKAGE_ROOT / "src/core/code-flow"
205
- target = staging / ".code-flow"
206
- shutil.copytree(source / "scripts", target / "scripts", dirs_exist_ok=True)
207
- shutil.copytree(source / "specs/shared", target / "specs/shared", dirs_exist_ok=True)
208
-
209
-
210
- def _copy_adapter_tree(source: Path, target: Path) -> None:
211
- if source.is_dir():
212
- shutil.copytree(source, target, dirs_exist_ok=True)
213
- elif source.is_file():
214
- target.parent.mkdir(parents=True, exist_ok=True)
215
- shutil.copy2(source, target)
216
-
217
-
218
- def _clean_hook_config(data: dict[str, object]) -> None:
219
- hooks = data.get("hooks")
220
- if not isinstance(hooks, dict):
221
- return
222
- for event, groups in list(hooks.items()):
223
- if not isinstance(groups, list):
224
- continue
225
- for group in groups:
226
- commands = group.get("hooks") if isinstance(group, dict) else None
227
- if isinstance(commands, list):
228
- group["hooks"] = [
229
- item for item in commands
230
- if isinstance(item, dict) and not any(token in str(item.get("command", "")) for token in ("cf_inject_hook.py", "cf_session_hook.py"))
231
- ]
232
- hooks[event] = [group for group in groups if isinstance(group, dict) and group.get("hooks")]
233
-
234
-
235
- def _merge_hook_config(current: dict[str, object], canonical: Mapping[str, object]) -> dict[str, object]:
236
- _clean_hook_config(current)
237
- target_hooks = current.setdefault("hooks", {})
238
- source_hooks = canonical.get("hooks")
239
- if not isinstance(target_hooks, dict) or not isinstance(source_hooks, dict):
240
- raise ValueError("invalid hook config")
241
- for event, source_groups in source_hooks.items():
242
- target_groups = target_hooks.setdefault(event, [])
243
- if not isinstance(source_groups, list) or not isinstance(target_groups, list):
244
- raise ValueError(f"invalid hook event: {event}")
245
- for source_group in source_groups:
246
- matcher = source_group.get("matcher", "") if isinstance(source_group, dict) else ""
247
- target = next((item for item in target_groups if isinstance(item, dict) and item.get("matcher", "") == matcher), None)
248
- if target is None:
249
- target_groups.append(source_group)
250
- continue
251
- known = {item.get("command") for item in target.get("hooks", []) if isinstance(item, dict)}
252
- target.setdefault("hooks", []).extend(item for item in source_group.get("hooks", []) if item.get("command") not in known)
253
- return current
254
-
255
-
256
- def _write_hook_config(project: Path, canonical: Path, relative: str, staging: Path) -> None:
257
- source = project / relative
258
- current = json.loads(source.read_text(encoding="utf-8")) if source.is_file() else {}
259
- template = json.loads(canonical.read_text(encoding="utf-8"))
260
- merged = _merge_hook_config(current, template)
261
- target = staging / relative
262
- target.parent.mkdir(parents=True, exist_ok=True)
263
- target.write_text(json.dumps(merged, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
264
-
265
-
266
- def _write_adapters(project: Path, staging: Path) -> None:
267
- adapters = PACKAGE_ROOT / "src/adapters"
268
- mappings = (
269
- (adapters / "claude/commands", staging / ".claude/commands"),
270
- (adapters / "costrict/commands", staging / ".costrict/commands"),
271
- (adapters / "codex/skills", staging / ".agents/skills"),
272
- (adapters / "opencode/commands", staging / ".opencode/commands"),
273
- (adapters / "opencode/plugins", staging / ".opencode/plugins"),
274
- )
275
- for source, target in mappings:
276
- _copy_adapter_tree(source, target)
277
- for canonical, relative in (
278
- (adapters / "claude/settings.local.json", ".claude/settings.local.json"),
279
- (adapters / "costrict/settings.local.json", ".costrict/settings.local.json"),
280
- (adapters / "codex/hooks.json", ".codex/hooks.json"),
281
- ):
282
- _write_hook_config(project, canonical, relative, staging)
283
-
284
-
285
- def _managed_block(template: str) -> str:
286
- match = re.search(
287
- r"(?s)<!-- code-flow:spec-loading schema=1 start -->.*?<!-- code-flow:spec-loading schema=1 end -->",
288
- template,
289
- )
290
- if match is None:
291
- raise ValueError("canonical managed block missing")
292
- return match.group(0)
293
-
294
-
295
- def _replace_managed(text: str, block: str) -> str:
296
- current = re.search(
297
- r"(?s)<!-- code-flow:spec-loading schema=\d+ start -->.*?<!-- code-flow:spec-loading schema=\d+ end -->",
298
- text,
299
- )
300
- if current is not None:
301
- return text[:current.start()] + block + text[current.end():]
302
- legacy = re.search(r"(?ms)^## Spec Loading\s*$.*?(?=^## |\Z)", text)
303
- if legacy is not None:
304
- return text[:legacy.start()] + block + "\n\n" + text[legacy.end():]
305
- return text.rstrip() + "\n\n" + block + "\n"
306
-
307
-
308
- def _write_agent_files(project: Path, staging: Path) -> None:
309
- templates = {
310
- "AGENTS.md": PACKAGE_ROOT / "src/adapters/codex/AGENTS.md",
311
- "CLAUDE.md": PACKAGE_ROOT / "src/adapters/claude/CLAUDE.md",
312
- }
313
- for name, template_path in templates.items():
314
- source = project / name
315
- if not source.is_file():
316
- continue
317
- block = _managed_block(template_path.read_text(encoding="utf-8"))
318
- (staging / name).write_text(
319
- _replace_managed(source.read_text(encoding="utf-8"), block), encoding="utf-8"
320
- )
321
-
322
-
323
- def _apply_layout(staging: Path, transforms: Sequence[Mapping[str, object]]) -> None:
324
- for transform in transforms:
325
- target_value = transform.get("target")
326
- files_value = transform.get("files")
327
- if not isinstance(target_value, str) or not isinstance(files_value, list):
328
- raise ValueError("invalid layout transform")
329
- target = staging / target_value
330
- target.mkdir(parents=True, exist_ok=True)
331
- for relative in files_value:
332
- if not isinstance(relative, str):
333
- raise ValueError("invalid layout file")
334
- source = staging / relative
335
- shutil.move(str(source), str(target / source.name))
336
-
337
-
338
- def _demand_paths(staging: Path, plan: Mapping[str, object]) -> tuple[Path, ...]:
339
- paths: set[Path] = set()
340
- for item in plan.get("layout_transforms", []):
341
- if isinstance(item, dict) and isinstance(item.get("target"), str):
342
- paths.add(staging / str(item["target"]))
343
- for item in plan.get("active_demands", []):
344
- if isinstance(item, dict) and isinstance(item.get("path"), str):
345
- paths.add(staging / str(item["path"]))
346
- return tuple(sorted(paths))
347
-
348
-
349
- def _staged_metadata(staging: Path) -> tuple[dict[str, SpecMetadata], dict[str, SpecMetadata], dict[str, str]]:
350
- by_id: dict[str, SpecMetadata] = {}
351
- by_path: dict[str, SpecMetadata] = {}
352
- paths_by_id: dict[str, str] = {}
353
- root = staging / ".code-flow/specs"
354
- for path in root.rglob("*.md"):
355
- relative = path.relative_to(root).as_posix()
356
- if path.name == "_map.md" or relative.startswith(("shared/", "_session/")):
357
- continue
358
- metadata = load_spec_metadata(str(path))
359
- by_id[metadata.id] = metadata
360
- by_path[relative] = metadata
361
- paths_by_id[metadata.id] = relative
362
- return by_id, by_path, paths_by_id
363
-
364
-
365
- def _normalized_refs(value: str, by_id: Mapping[str, SpecMetadata], by_path: Mapping[str, SpecMetadata]) -> tuple[str, ...]:
366
- refs: list[str] = []
367
- for raw in value.split(","):
368
- token = raw.strip().strip("`")
369
- if "#" in token and token.split("#", 1)[0] in by_id:
370
- refs.append(token)
371
- continue
372
- metadata = by_path.get(token)
373
- if metadata is not None:
374
- refs.extend(f"{metadata.id}#{rule.ref}" for rule in metadata.rules if rule.enforcement == "required")
375
- return tuple(refs)
376
-
377
-
378
- def _rewrite_task_refs(demand: Path, by_id: Mapping[str, SpecMetadata], by_path: Mapping[str, SpecMetadata]) -> tuple[str, ...]:
379
- found: set[str] = set()
380
- pattern = re.compile(r"(?m)^- \*\*Spec-Refs\*\*:\s*(.+)$")
381
- for path in demand.glob("*.md"):
382
- text = path.read_text(encoding="utf-8")
383
- def replace(match: re.Match[str]) -> str:
384
- refs = _normalized_refs(match.group(1), by_id, by_path)
385
- found.update(refs)
386
- return "- **Spec-Refs**: " + ", ".join(refs) if refs else match.group(0)
387
- updated = pattern.sub(replace, text)
388
- if updated != text:
389
- path.write_text(updated, encoding="utf-8")
390
- return tuple(sorted(found))
391
-
392
-
393
- def _write_contexts(staging: Path, plan: Mapping[str, object]) -> None:
394
- by_id, by_path, paths_by_id = _staged_metadata(staging)
395
- for demand in _demand_paths(staging, plan):
396
- demand.mkdir(parents=True, exist_ok=True)
397
- refs = _rewrite_task_refs(demand, by_id, by_path)
398
- spec_ids = tuple(sorted({item.split("#", 1)[0] for item in refs}))
399
- candidates = tuple(
400
- SpecCandidate(spec_id, paths_by_id[spec_id], "task", SCOPE_PRIORITY["task"], (), by_id[spec_id])
401
- for spec_id in spec_ids
402
- )
403
- context = new_context(demand.name, (("migration", "prepared-plan"),))
404
- selections = tuple(BindingInput(item, "migration", "legacy Spec-Refs") for item in candidates)
405
- save_context(str(demand / "spec-context.yml"), bind_specs(context, selections))
406
-
407
-
408
- def _write_target_config(staging: Path) -> None:
409
- path = staging / ".code-flow/config.yml"
410
- data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
411
- if not isinstance(data, dict):
412
- raise ValueError("invalid config")
413
- inject = data.get("inject")
414
- quality = data.get("quality_loop")
415
- if not isinstance(quality, dict):
416
- quality = {}
417
- if isinstance(inject, dict):
418
- for key in ("compress", "code_extensions", "skip_extensions", "skip_paths"):
419
- if key in inject and key not in quality:
420
- quality[key] = inject[key]
421
- quality.pop("dedup_window", None)
422
- data.pop("inject", None)
423
- data["quality_loop"] = quality
424
- data["spec_workflow"] = {
425
- "schema_version": 1, "enforcement": "required", "drift_check": True,
426
- "scope_expansion_check": True, "metrics": True, "catalog": {"dedup_window": 5},
427
- }
428
- path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8")
429
-
430
-
431
- def _runtime_gitignore() -> tuple[str, list[str], str]:
432
- path = Path(__file__).resolve().parents[2] / "runtime-resources.json"
433
- data = json.loads(path.read_text(encoding="utf-8"))
434
- block = data["gitignore"]
435
- return block["start"], list(block["entries"]), block["end"]
436
-
437
-
438
- def _write_target_ignore(staging: Path) -> None:
439
- path = staging / ".code-flow/.gitignore"
440
- existing = path.read_text(encoding="utf-8") if path.exists() else ""
441
- # The packaged runtime-resources.json declaration is the single source shared
442
- # with the CLI: migrate must not leave sessions/worktrees/backups/logs
443
- # committable after an upgrade.
444
- start, entries, end = _runtime_gitignore()
445
- block = "\n".join([start, *entries, end]) + "\n"
446
- lines = [line for line in existing.splitlines() if line != ".inject-state"]
447
- if start not in existing:
448
- path.write_text("\n".join(lines).rstrip() + "\n" + block, encoding="utf-8")
449
- return
450
- if end in lines:
451
- missing = [entry for entry in entries if entry not in lines]
452
- if missing:
453
- at = lines.index(end)
454
- lines[at:at] = missing
455
- path.write_text("\n".join(lines) + "\n", encoding="utf-8")
456
-
457
-
458
- def _target_manifest(staging: Path) -> list[dict[str, str]]:
459
- return [
460
- {"path": _relative(staging, path), "sha256": _sha256(path)}
461
- for path in sorted(staging.rglob("*")) if path.is_file()
462
- ]
463
-
464
-
465
- def stage_plan(plan: Mapping[str, object], staging_root: str) -> dict[str, object]:
466
- unresolved = plan.get("unresolved")
467
- if not isinstance(unresolved, list) or unresolved:
468
- raise ValueError("prepared plan has unresolved items")
469
- project_value = plan.get("project_root")
470
- if not isinstance(project_value, str):
471
- raise ValueError("prepared plan missing project_root")
472
- staging = Path(staging_root).resolve()
473
- if not staging.is_dir() or any(staging.iterdir()):
474
- raise ValueError("staging root must be a pre-created empty directory")
475
- _copy_source(Path(project_value), staging)
476
- _copy_runtime(staging)
477
- _apply_layout(staging, plan.get("layout_transforms", []))
478
- _write_contexts(staging, plan)
479
- _write_target_config(staging)
480
- _write_target_ignore(staging)
481
- _write_agent_files(Path(project_value), staging)
482
- _write_adapters(Path(project_value), staging)
483
- inject_state = staging / ".code-flow/.inject-state"
484
- if inject_state.exists():
485
- inject_state.unlink()
486
- return {
487
- "targets": _target_manifest(staging),
488
- "deletions": [
489
- ".code-flow/.inject-state", ".code-flow/specs/_session",
490
- ".claude/commands/cf-inject.md", ".costrict/commands/cf-inject.md",
491
- ".opencode/commands/cf-inject.md", ".agents/skills/cf-inject",
492
- ],
493
- "residue": ["inject.mode", "injected_specs", "cf-inject"],
494
- }
495
-
496
-
497
- def _parser() -> argparse.ArgumentParser:
498
- parser = argparse.ArgumentParser(prog="cf_spec_migrate.py")
499
- commands = parser.add_subparsers(dest="command", required=True)
500
- command = commands.add_parser("preflight")
501
- command.add_argument("--root", required=True)
502
- command.add_argument("--json", action="store_true")
503
- stage = commands.add_parser("stage")
504
- stage.add_argument("--plan", required=True)
505
- stage.add_argument("--staging", required=True)
506
- stage.add_argument("--json", action="store_true")
507
- return parser
508
-
509
-
510
- def _load_plan(path: str) -> Mapping[str, object]:
511
- value = yaml.safe_load(Path(path).read_text(encoding="utf-8"))
512
- if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
513
- raise ValueError("migration plan must be a mapping")
514
- return value
515
-
516
-
517
- def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
518
- try:
519
- args = _parser().parse_args(argv)
520
- result = preflight(args.root) if args.command == "preflight" else stage_plan(_load_plan(args.plan), args.staging)
521
- stdout.write(json.dumps(result, ensure_ascii=False))
522
- return 0
523
- except Exception as exc:
524
- stdout.write(json.dumps({"ready": False, "error": str(exc)}, ensure_ascii=False))
525
- return 2
526
-
527
-
528
- if __name__ == "__main__":
529
- raise SystemExit(main())