@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,347 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Lock and validate plan Acceptance Coverage entries."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import os
8
- import json
9
- from datetime import datetime, timezone
10
- from pathlib import Path
11
- import re
12
- import sys
13
- from typing import IO, Mapping, Optional, Sequence
14
-
15
- from codeflow.acceptance.cf_acceptance_manifest_core import (
16
- IMMUTABLE_SCENARIO_FIELDS,
17
- contract_commands as _contract_commands,
18
- coverage_execution,
19
- extract_manifest,
20
- kind as _kind,
21
- legacy_kind as _legacy_kind,
22
- validate_manifest,
23
- )
24
- from codeflow.acceptance.cf_acceptance_schema import load_manifest, scenario_check_ids, validate_check_levels
25
-
26
-
27
- _RUNTIME_SCENARIO_FIELDS = ("status", "revision", "evidence", "runs", "stale_checks", "stale_reason")
28
-
29
- _CHECK_CONFIG_FIELDS = ("env", "env_scope", "parallel_safe", "profile", "inputs", "preflight", "accepted_risk",
30
- "suite", "test_ids", "markers", "collect", "level", "environment_revision", "evidence_ttl_seconds")
31
-
32
-
33
- def _manifest_source(task_file: str, output: str, scenarios_file: str) -> str:
34
- if scenarios_file:
35
- return scenarios_file
36
- if Path(output).is_file():
37
- try:
38
- stored = json.loads(Path(output).read_text(encoding="utf-8"))
39
- recorded = stored.get("scenarios_file") if isinstance(stored, Mapping) else None
40
- except (OSError, ValueError, TypeError) as exc:
41
- sys.stderr.write(f"acceptance relock source unavailable: {exc}\n")
42
- recorded = None
43
- if isinstance(recorded, str) and recorded.strip():
44
- return str((Path(output).parent / recorded).resolve())
45
- candidate = Path(task_file).with_suffix(".scenarios.md")
46
- return str(candidate) if candidate.is_file() else ""
47
-
48
-
49
- def _project_source_checks(manifest: dict[str, object], scenarios_file: str) -> None:
50
- if scenarios_file:
51
- from codeflow.cli.cf_scenarios import parse_scenarios
52
- source = parse_scenarios(scenarios_file)
53
- rows = {str(item["id"]): item for item in manifest["scenarios"]}
54
- check_ids = {str(item["id"]) for item in manifest.get("checks", [])}
55
- for scenario in source["scenarios"]:
56
- scenario_id = str(scenario["id"])
57
- row = rows.get(scenario_id)
58
- if row is None:
59
- continue
60
- checks = [str(value) for value in scenario.get("checks", [])]
61
- unknown = [value for value in checks if value not in check_ids]
62
- if unknown:
63
- raise ValueError(f"unknown_check:{scenario_id}:{','.join(unknown)}")
64
- if checks:
65
- row["checks"] = checks
66
-
67
-
68
- def write_manifest(task_file: str, output: str, scenarios_file: str = "", *,
69
- register_checks: bool = False, registration_file: str = "") -> dict[str, object]:
70
- scenarios_file = _manifest_source(task_file, output, scenarios_file)
71
- manifest = extract_manifest(task_file, scenarios_file=scenarios_file)
72
- _project_source_checks(manifest, scenarios_file)
73
- target = Path(output)
74
- if scenarios_file:
75
- source_path = Path(scenarios_file).resolve()
76
- try:
77
- manifest["scenarios_file"] = str(source_path.relative_to(target.parent.resolve()))
78
- except ValueError:
79
- manifest["scenarios_file"] = str(source_path)
80
- # Re-lock explicitly opts into current quality; reading old plans does not.
81
- manifest["e2e_gate"] = 1
82
- manifest["quality_schema"] = 2
83
- _preserve_check_config(target, manifest)
84
- if register_checks or registration_file:
85
- from codeflow.acceptance.cf_acceptance_registration import register_checks as configure
86
- from codeflow.evidence.cf_evidence_store import discover_project_root
87
- root = discover_project_root(task_file)
88
- manifest["registration"] = configure(manifest, str(root), registration_file)
89
- validate_check_levels(manifest["scenarios"], manifest["checks"])
90
- _preserve_runtime_state(task_file, target, manifest)
91
- for scenario in manifest.get("scenarios", []):
92
- if isinstance(scenario, dict) and scenario.get("status") in ("verified", "failed") and not scenario.get("evidence"):
93
- scenario["status"] = "planned"
94
- try:
95
- manifest["task_file"] = str(Path(task_file).resolve().relative_to(target.parent.resolve()))
96
- except ValueError:
97
- manifest["task_file"] = str(Path(task_file).resolve())
98
- target.parent.mkdir(parents=True, exist_ok=True)
99
- target.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
100
- return manifest
101
-
102
-
103
- def record_accepted_risk(
104
- manifest_file: str, check_id: str, owner: str, reason: str, expires_at: str,
105
- confirmed_by: str, source: str,
106
- ) -> dict[str, object]:
107
- """Attach a user-confirmed, time-boxed risk acceptance to one check."""
108
- if not all(str(value).strip() for value in (check_id, owner, reason, expires_at, confirmed_by, source)):
109
- raise ValueError("accepted_risk requires check, owner, reason, expires_at, confirmed_by and source")
110
- agent_names = {
111
- "agent", "assistant", "claude", "claude code", "codex", "opencode",
112
- "costrict", "ai", "bot",
113
- }
114
- identity = confirmed_by.strip().lower().split(":", 1)[0]
115
- if identity in agent_names:
116
- raise ValueError("accepted_risk must be confirmed by the user, not the agent")
117
- try:
118
- deadline = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
119
- except ValueError as exc:
120
- raise ValueError(f"invalid expires_at: {expires_at}") from exc
121
- if re.fullmatch(r"\d{4}-\d{2}-\d{2}", expires_at.strip()):
122
- deadline = deadline.replace(hour=23, minute=59, second=59, microsecond=999999)
123
- if deadline.tzinfo is None:
124
- deadline = deadline.replace(tzinfo=timezone.utc)
125
- now = datetime.now(timezone.utc)
126
- if deadline <= now:
127
- raise ValueError("expires_at must be in the future")
128
- path = Path(manifest_file)
129
- data = load_manifest(path)
130
- checks = data.get("checks")
131
- if not isinstance(checks, list):
132
- raise ValueError("accepted_risk requires a schema3 manifest with checks")
133
- for check in checks:
134
- if isinstance(check, dict) and check.get("id") == check_id:
135
- from codeflow.acceptance.cf_acceptance_risk import risk_scenario_ids
136
- scenario_ids = risk_scenario_ids(data, check_id, owner.strip())
137
- if not scenario_ids:
138
- raise ValueError(f"accepted_risk owner {owner!r} owns no scenario for {check_id}")
139
- check["accepted_risk"] = {
140
- "owner": owner.strip(),
141
- "scenario_ids": scenario_ids,
142
- "reason": reason.strip(),
143
- "expires_at": deadline.isoformat(),
144
- "confirmed_by": confirmed_by.strip(),
145
- "confirmed_at": now.isoformat(),
146
- "source": source.strip(),
147
- }
148
- path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
149
- return {"ok": True, "check_id": check_id, "accepted_risk": check["accepted_risk"]}
150
- raise ValueError(f"unknown check: {check_id}")
151
-
152
-
153
- def _preserve_check_config(target: Path, manifest: dict[str, object]) -> None:
154
- """Re-locking the plan must not erase user-authored check environment.
155
-
156
- Check IDs are command identities, so a stored check with the same ID still
157
- describes the same execution; its env/preflight configuration remains valid.
158
- """
159
- if not target.is_file():
160
- return
161
- try:
162
- stored = {
163
- item.get("id"): item
164
- for item in load_manifest(target).get("checks", [])
165
- if isinstance(item, dict) and isinstance(item.get("id"), str)
166
- }
167
- except (OSError, ValueError, json.JSONDecodeError):
168
- return
169
- checks = manifest.get("checks")
170
- if not isinstance(checks, list):
171
- return
172
- scenarios = manifest.get("scenarios")
173
- for item in checks:
174
- if not isinstance(item, dict):
175
- continue
176
- old = stored.get(item.get("id"))
177
- if not isinstance(old, dict):
178
- continue
179
- derived_profile = any(
180
- isinstance(row, dict)
181
- and "smoke" in row
182
- and item.get("id") in scenario_check_ids(row)
183
- for row in (scenarios if isinstance(scenarios, list) else [])
184
- )
185
- for key in _CHECK_CONFIG_FIELDS:
186
- if key == "profile" and derived_profile:
187
- continue
188
- if key in old:
189
- item[key] = old[key]
190
- if item.get("env_scope", "shared") != "isolated":
191
- item["parallel_safe"] = False
192
-
193
-
194
- def _preserve_runtime_state(task_file: str, target: Path, manifest: dict[str, object]) -> None:
195
- """Keep history per scenario; changed contracts become visibly stale."""
196
- if not target.is_file():
197
- return
198
- try:
199
- previous = load_manifest(target)
200
- except (OSError, ValueError) as exc:
201
- manifest["relock"] = {"warnings": [f"previous_manifest_invalid: {exc}"]}
202
- return
203
- previous_task = previous.get("task_file")
204
- if previous_task and (target.parent / str(previous_task)).resolve() != Path(task_file).resolve():
205
- manifest["relock"] = {"warnings": ["previous_manifest_task_mismatch"]}
206
- return
207
- stored = {item["id"]: item for item in previous["scenarios"]}
208
- current_ids = {item["id"] for item in manifest["scenarios"]}
209
- report: dict[str, object] = {"preserved": [], "stale": [], "removed": sorted(set(stored) - current_ids)}
210
- for item in manifest.get("scenarios", []):
211
- old = stored.get(item.get("id"))
212
- if not isinstance(old, dict):
213
- continue
214
- for key in _RUNTIME_SCENARIO_FIELDS:
215
- if key in old:
216
- item[key] = old[key]
217
- changed = _scenario_contract(old, previous) != _scenario_contract(item, manifest)
218
- if changed and any(key in old for key in ("evidence", "runs")):
219
- item["status"] = "stale"
220
- item["stale_checks"] = scenario_check_ids(item)
221
- item["stale_reason"] = "scenario_or_check_contract_changed"
222
- report["stale"].append(item["id"])
223
- else:
224
- report["preserved"].append(item["id"])
225
- manifest["relock"] = report
226
-
227
-
228
- def _scenario_contract(row: Mapping[str, object], manifest: Mapping[str, object]) -> dict[str, object]:
229
- ids = scenario_check_ids(row)
230
- checks = {item["id"]: item for item in manifest.get("checks", [])}
231
- return {"scenario": {key: row.get(key) for key in IMMUTABLE_SCENARIO_FIELDS},
232
- "checks": {key: checks.get(key) for key in ids}}
233
-
234
-
235
- def record_manual_evidence(manifest_file: str, scenario_id: str, confirmed_by: str, evidence: str) -> None:
236
- if not confirmed_by or confirmed_by.lower().split(":", 1)[0] in {"agent", "assistant", "codex", "claude", "opencode", "costrict"}:
237
- raise ValueError("manual evidence must be confirmed by a user")
238
- path = Path(manifest_file)
239
- data = load_manifest(path)
240
- scenarios = data.get("scenarios")
241
- if not isinstance(scenarios, list):
242
- raise ValueError("invalid manifest scenarios")
243
- found = False
244
- owner = ""
245
- task_file = data.get("task_file")
246
- for item in scenarios:
247
- if isinstance(item, dict) and item.get("id") == scenario_id:
248
- if item.get("kind") != "manual" and item.get("exec") != "manual":
249
- raise ValueError("scenario is not manual")
250
- item["status"] = "verified"
251
- item["evidence"] = {"confirmed_by": confirmed_by, "evidence": evidence}
252
- found = True
253
- owner = item.get("owner") if isinstance(item.get("owner"), str) else ""
254
- if not found:
255
- raise ValueError("scenario not found")
256
- path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
257
- if isinstance(task_file, str) and task_file:
258
- task_path = Path(task_file)
259
- if not task_path.is_absolute():
260
- task_path = path.parent / task_path
261
- _sync_task_evidence(task_path, scenario_id, confirmed_by, evidence, owner)
262
-
263
-
264
- def _sync_task_evidence(task_file: Path, scenario_id: str, confirmed_by: str, evidence: str, owner: str = "") -> None:
265
- from codeflow.acceptance.cf_acceptance_evidence import sync_task_evidence
266
- sync_task_evidence(task_file, scenario_id, confirmed_by, evidence, owner)
267
-
268
-
269
- def _parser() -> argparse.ArgumentParser:
270
- parser = argparse.ArgumentParser(prog=os.environ.get("CF_RUNTIME_COMMAND") or None)
271
- parser.add_argument("--task-file", default="")
272
- parser.add_argument("--output", default="")
273
- parser.add_argument("--scenarios-file", default="")
274
- parser.add_argument("--register-checks", action="store_true",
275
- help="collect pytest/Playwright tests and register checks before writing")
276
- parser.add_argument("--registration-file", default="",
277
- help="JSON check registration overrides for custom collectors")
278
- parser.add_argument("--verify-plan", action="store_true")
279
- parser.add_argument("--require-scenarios", action="store_true")
280
- parser.add_argument("--task-dir", default="")
281
- parser.add_argument("--record-manual", action="store_true")
282
- parser.add_argument("--accept-risk", action="store_true")
283
- parser.add_argument("--project", action="store_true",
284
- help="write manifest and project sibling scenarios checks in one step")
285
- parser.add_argument("--check-id", default="")
286
- parser.add_argument("--risk-owner", default="")
287
- parser.add_argument("--risk-reason", default="")
288
- parser.add_argument("--expires-at", default="")
289
- parser.add_argument("--scenario-id", default="")
290
- parser.add_argument("--confirmed-by", default="")
291
- parser.add_argument("--source", default="")
292
- parser.add_argument("--evidence", default="")
293
- return parser
294
-
295
-
296
- def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
297
- args = _parser().parse_args(argv)
298
- try:
299
- if (args.project or args.verify_plan) and not args.task_file:
300
- raise ValueError(f"{'--project' if args.project else '--verify-plan'} requires --task-file")
301
- if args.verify_plan and (args.register_checks or args.registration_file):
302
- raise ValueError("--verify-plan is read-only; register checks while writing the manifest")
303
- if (args.accept_risk or args.record_manual) and (args.register_checks or args.registration_file):
304
- raise ValueError("check registration requires writing the manifest")
305
- if args.project:
306
- stdout.write(json.dumps(_write_manifest_action(args), ensure_ascii=False))
307
- return 0
308
- if args.accept_risk:
309
- result = record_accepted_risk(
310
- args.output, args.check_id, args.risk_owner, args.risk_reason,
311
- args.expires_at, args.confirmed_by, args.source,
312
- )
313
- stdout.write(json.dumps(result, ensure_ascii=False))
314
- return 0
315
- if args.record_manual:
316
- if not args.output or not args.scenario_id or not args.evidence:
317
- raise ValueError("--record-manual requires --output, --scenario-id and --evidence")
318
- record_manual_evidence(args.output, args.scenario_id, args.confirmed_by, args.evidence)
319
- stdout.write(json.dumps({"ok": True, "scenario": args.scenario_id}, ensure_ascii=False))
320
- return 0
321
- if args.verify_plan:
322
- if not args.task_dir:
323
- raise ValueError("--verify-plan requires --task-dir")
324
- from codeflow.acceptance.cf_plan_quality import verify_plan
325
- result = verify_plan(args.task_dir, args.task_file, args.require_scenarios)
326
- stdout.write(json.dumps(result, ensure_ascii=False))
327
- return 0 if result["decision"] == "pass" else 3
328
- stdout.write(json.dumps(_write_manifest_action(args), ensure_ascii=False))
329
- return 0
330
- except (OSError, ValueError) as exc:
331
- stdout.write(json.dumps({"ok": False, "error": str(exc),
332
- **({"registration": exc.report} if hasattr(exc, "report") else {})}, ensure_ascii=False))
333
- return 2
334
-
335
-
336
- def _write_manifest_action(args: argparse.Namespace) -> dict[str, object]:
337
- output = args.output or (str(Path(args.task_file).with_name(".acceptance-manifest.json")) if args.project else "")
338
- if not output:
339
- raise ValueError("--output is required unless --verify-plan is used")
340
- if not args.task_file:
341
- raise ValueError("--task-file is required")
342
- manifest = write_manifest(args.task_file, output, args.scenarios_file,
343
- register_checks=args.register_checks, registration_file=args.registration_file)
344
- return {"ok": True, "manifest": output, "scenarios": len(manifest["scenarios"]),
345
- "checks": len(manifest["checks"]),
346
- "projected_scenarios": sum(1 for row in manifest["scenarios"] if row.get("checks")),
347
- "relock": manifest.get("relock", {}), "registration": manifest.get("registration", {})}