@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,385 +1,10 @@
1
1
  #!/usr/bin/env python3
2
- """Task-level worktree orchestration for parallel batch execution.
3
-
4
- `cf-task:start` 整文件模式对拓扑批次内的独立 TASK 并行派发子 agent 时,本脚本
5
- 负责可测试的确定性步骤,子 agent 派发与合并冲突解决仍由命令提示词编排:
6
-
7
- prepare 预检(git 仓库 / worktree 忽略规则 / 无 active marker / tracked 干净)
8
- → 自动提交仅位于需求目录内的流程产物 → 校验任务可并行 → 建 worktree+分支
9
- collect 回并前校验:改动已提交、Status 已 done/verified、marker 已清理
10
- cleanup 合并完成后移除 worktree;已合入的分支一并删除,未合入的保留可追溯
11
-
12
- 协议(详见 cf-task:start 命令正文):
13
- - worktree: .code-flow/worktrees/<run-id>/<TASK-ID>/
14
- - branch: cf-task/<slug>/<run-id>/<TASK-ID>
15
- - 元数据: .code-flow/worktrees/<run-id>/run.json(base head / 任务清单)
16
-
17
- stdout 始终为 JSON:{"ok": true, ...} 或 {"ok": false, "code": ..., "message": ...}。
18
- 预检失败时命令方回退串行,不得手工绕过本脚本建 worktree/分支。
19
- """
20
-
21
- from __future__ import annotations
22
-
23
- import argparse
24
- import json
25
- import re
26
- import subprocess
2
+ """Path-frozen entry shim: implementation lives in codeflow.cli.cf_task_parallel."""
27
3
  import sys
28
- from datetime import datetime
29
- from pathlib import Path
30
- from typing import IO, Optional, Sequence
31
-
32
- from cf_task_index import parse_task_file
33
- from cf_task_index import task_batches
34
- from cf_task_state import FINISHED_STATUSES
35
-
36
-
37
- WORKTREES_DIR = ".code-flow/worktrees"
38
- MARKER = ".code-flow/.active-task.json"
39
- GITIGNORE = ".code-flow/.gitignore"
40
- _RUN_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
41
- _TASK_ID = re.compile(r"^TASK-\d+$")
42
-
43
-
44
- class ParallelError(ValueError):
45
- """Precondition or protocol violation with a stable machine code."""
46
-
47
- def __init__(self, code: str, message: str) -> None:
48
- super().__init__(message)
49
- self.code = code
50
- self.message = message
51
-
52
-
53
- def _run_git(root: Path, arguments: Sequence[str]) -> str:
54
- result = subprocess.run(
55
- ("git", *arguments), cwd=str(root), text=True, encoding="utf-8", capture_output=True, check=False
56
- )
57
- if result.returncode != 0:
58
- message = result.stderr.strip() or "Git command failed"
59
- raise ParallelError("git_error", message)
60
- return result.stdout
61
-
62
-
63
- def _assert_repo(root: Path) -> None:
64
- try:
65
- top = _run_git(root, ("rev-parse", "--show-toplevel")).strip()
66
- except ParallelError as exc:
67
- raise ParallelError("git_unavailable", "并行执行需要 git 仓库") from exc
68
- if Path(top).resolve() != root:
69
- raise ParallelError("not_project_root", f"root 必须是仓库根目录: {top}")
70
-
71
-
72
- def _status_paths(root: Path) -> list[str]:
73
- output = _run_git(root, ("status", "--porcelain=v1", "-z", "--untracked-files=all"))
74
- tokens = output.split("\0")
75
- paths: list[str] = []
76
- index = 0
77
- while index < len(tokens) and tokens[index]:
78
- record = tokens[index]
79
- if len(record) < 4:
80
- raise ParallelError("invalid_git_status", record)
81
- code, path = record[:2], record[3:]
82
- paths.append(path)
83
- if code[:1] in ("R", "C"):
84
- if index + 1 < len(tokens) and tokens[index + 1]:
85
- paths.append(tokens[index + 1])
86
- index += 2
87
- else:
88
- index += 1
89
- return paths
90
-
91
-
92
- def _require_ignored(root: Path) -> None:
93
- ignore = root / GITIGNORE
94
- try:
95
- text = ignore.read_text(encoding="utf-8")
96
- except OSError as exc:
97
- raise ParallelError("worktrees_not_ignored", f"缺少 {GITIGNORE}: {exc}") from exc
98
- if "worktrees/" not in text.split():
99
- raise ParallelError(
100
- "worktrees_not_ignored", f"{GITIGNORE} 缺少 worktrees/ 规则,请先运行 code-flow init 升级"
101
- )
102
-
103
-
104
- def _require_no_active(root: Path) -> None:
105
- if (root / MARKER).exists():
106
- raise ParallelError("active_exists", "主 worktree 已有 active TASK,先 finish/doctor 后再并行")
107
-
108
-
109
- def _commit_task_dir(root: Path, task_file: str) -> Optional[str]:
110
- task_dir = Path(task_file).parent
111
- entries = _status_paths(root)
112
- outside = [
113
- path
114
- for path in entries
115
- if not (path == str(task_dir) or path.startswith(f"{task_dir}/"))
116
- ]
117
- if outside:
118
- sample = ", ".join(outside[:8]) + (" ..." if len(outside) > 8 else "")
119
- raise ParallelError(
120
- "dirty_tree",
121
- "并行要求 tracked 工作区干净(需求目录以外仍有改动: "
122
- f"{sample})。请先提交(git add -A && git commit -m 'wip')"
123
- "或暂存(git stash push -u)后重试;需求目录内的流程产物会自动提交。",
124
- )
125
- if not entries:
126
- return None
127
- _run_git(root, ("add", "-A", "--", str(task_dir)))
128
- _run_git(root, ("commit", "-q", "-m", f"chore(cf-task): plan {Path(task_file).stem}"))
129
- return _run_git(root, ("rev-parse", "HEAD")).strip()
130
-
131
-
132
- def _validate_batch(root: Path, task_file: str, tasks: Sequence[str]) -> str:
133
- path = root / task_file
134
- if not path.is_file():
135
- raise ParallelError("task_file_missing", f"任务文件不存在: {task_file}")
136
- try:
137
- nodes = parse_task_file(str(path))
138
- except ValueError as exc:
139
- raise ParallelError("task_parse_error", str(exc)) from exc
140
- by_id = {node.task_id: node for node in nodes}
141
- for task in tasks:
142
- if task not in by_id:
143
- raise ParallelError("unknown_task", f"{task} 不在任务文件中")
144
- if by_id[task].status != "draft":
145
- raise ParallelError("task_not_draft", f"{task} 当前 Status 为 {by_id[task].status},不可并行激活")
146
- try:
147
- ready = task_batches(nodes)[0]
148
- except ValueError as exc:
149
- raise ParallelError("task_graph_error", str(exc)) from exc
150
- not_ready = [task for task in tasks if task not in ready]
151
- if not_ready:
152
- raise ParallelError("task_not_ready", f"依赖未完成,本批不可激活: {', '.join(not_ready)}(可激活: {', '.join(ready)})")
153
- return str(path)
154
-
155
-
156
- def _branch_slug(task_file: str) -> str:
157
- stem = Path(task_file).stem
158
- slug = re.sub(r"[^A-Za-z0-9._-]+", "-", stem).strip("-._")
159
- return slug[:40] or "batch"
160
-
161
-
162
- def _worktree_paths(root: Path, run_id: str, task: str, task_file: str) -> tuple[Path, str]:
163
- branch = f"cf-task/{_branch_slug(task_file)}/{run_id}/{task}"
164
- return root / WORKTREES_DIR / run_id / task, branch
165
-
166
-
167
- def _run_id() -> str:
168
- return datetime.now().strftime("%Y%m%d-%H%M%S")
169
-
170
-
171
- def _write_run_meta(run_dir: Path, payload: dict[str, object]) -> None:
172
- run_dir.mkdir(parents=True, exist_ok=True)
173
- (run_dir / "run.json").write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
174
-
175
-
176
- def _read_run_meta(root: Path, run_id: str) -> dict[str, object]:
177
- meta = root / WORKTREES_DIR / run_id / "run.json"
178
- try:
179
- return json.loads(meta.read_text(encoding="utf-8"))
180
- except (OSError, json.JSONDecodeError) as exc:
181
- raise ParallelError("run_not_found", f"找不到并行记录 {meta}: {exc}") from exc
182
-
183
-
184
- def _create_worktrees(root: Path, task_file: str, tasks: Sequence[str], run_id: str) -> dict[str, object]:
185
- created: list[tuple[Path, str]] = []
186
- base_head = _run_git(root, ("rev-parse", "HEAD")).strip()
187
- try:
188
- for task in tasks:
189
- path, branch = _worktree_paths(root, run_id, task, task_file)
190
- if path.exists() or _branch_exists(root, branch):
191
- raise ParallelError("worktree_exists", f"{task} 的 worktree 或分支已存在: {branch}")
192
- path.parent.mkdir(parents=True, exist_ok=True)
193
- _run_git(root, ("worktree", "add", "-b", branch, str(path), "HEAD"))
194
- if not (path / task_file).is_file():
195
- raise ParallelError("task_file_missing", f"{task}: worktree 内缺少 {task_file}(需先提交任务文件)")
196
- created.append((path, branch))
197
- except ParallelError:
198
- for path, branch in created:
199
- _remove_worktree(root, path, force=True)
200
- _run_git(root, ("branch", "-D", branch))
201
- raise
202
- return {"base_head": base_head, "created": created}
203
-
204
-
205
- def _branch_exists(root: Path, branch: str) -> bool:
206
- result = subprocess.run(
207
- ("git", "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}"),
208
- cwd=str(root), capture_output=True, check=False,
209
- )
210
- return result.returncode == 0
211
-
212
-
213
- def _delete_branch(root: Path, branch: str) -> bool:
214
- result = subprocess.run(
215
- ("git", "branch", "-d", branch), cwd=str(root), capture_output=True, check=False,
216
- )
217
- return result.returncode == 0
218
-
219
-
220
- def _remove_worktree(root: Path, path: Path, force: bool) -> None:
221
- arguments = ["worktree", "remove"]
222
- if force:
223
- arguments.append("--force")
224
- arguments.append(str(path))
225
- _run_git(root, tuple(arguments))
226
-
227
-
228
- def _relative_task_file(root: Path, task_file: str) -> str:
229
- candidate = Path(task_file)
230
- absolute = candidate if candidate.is_absolute() else root / candidate
231
- try:
232
- return str(absolute.resolve().relative_to(root))
233
- except ValueError as exc:
234
- raise ParallelError("task_file_outside_root", f"任务文件不在仓库内: {task_file}") from exc
235
-
236
-
237
- def prepare(root: Path, task_file: str, tasks: Sequence[str], run_id: Optional[str]) -> dict[str, object]:
238
- root = root.resolve()
239
- _assert_repo(root)
240
- task_file = _relative_task_file(root, task_file)
241
- _require_ignored(root)
242
- _require_no_active(root)
243
- identifier = run_id or _run_id()
244
- if not _RUN_ID.match(identifier):
245
- raise ParallelError("invalid_run_id", f"run-id 非法: {identifier}")
246
- if not tasks:
247
- raise ParallelError("no_tasks", "未指定并行 TASK")
248
- if len(set(tasks)) != len(tasks):
249
- raise ParallelError("duplicate_task", "并行 TASK 列表存在重复")
250
- for task in tasks:
251
- if not _TASK_ID.match(task):
252
- raise ParallelError("invalid_task_id", f"TASK-ID 非法: {task}")
253
- _validate_batch(root, task_file, tasks)
254
- _commit_task_dir(root, task_file)
255
- outcome = _create_worktrees(root, task_file, tasks, identifier)
256
- worktrees = [
257
- {"task": task, "branch": branch, "path": str(path)}
258
- for task, (path, branch) in zip(tasks, outcome["created"])
259
- ]
260
- _write_run_meta(
261
- root / WORKTREES_DIR / identifier,
262
- {"version": 1, "run_id": identifier, "task_file": task_file,
263
- "base_head": outcome["base_head"], "worktrees": worktrees},
264
- )
265
- return {"ok": True, "action": "prepare", "run_id": identifier, "task_file": task_file,
266
- "base_head": outcome["base_head"], "worktrees": worktrees}
267
-
268
-
269
- def _collect_one(root: Path, entry: dict[str, object], base_head: str, task_file: str) -> dict[str, object]:
270
- task = str(entry["task"])
271
- path = Path(str(entry["path"]))
272
- branch = str(entry["branch"])
273
- if not path.is_dir():
274
- raise ParallelError("worktree_missing", f"{task}: worktree 不存在: {path}")
275
- dirty = _status_paths(path)
276
- if dirty:
277
- raise ParallelError("worktree_dirty", f"{task}: 尚有未提交改动: {', '.join(dirty[:8])}")
278
- if (path / MARKER).exists():
279
- raise ParallelError("active_marker_present", f"{task}: marker 未清理,finish 未完成")
280
- head = _run_git(path, ("rev-parse", "HEAD")).strip()
281
- if head == base_head:
282
- raise ParallelError("no_commits", f"{task}: worktree 无新提交")
283
- try:
284
- nodes = parse_task_file(str(path / task_file))
285
- except ValueError as exc:
286
- raise ParallelError("task_parse_error", f"{task}: {exc}") from exc
287
- status = next((node.status for node in nodes if node.task_id == task), None)
288
- if status not in FINISHED_STATUSES:
289
- raise ParallelError("task_not_finished", f"{task}: Status 为 {status},未通过 Done Gate")
290
- files = _run_git(root, ("diff", "--name-only", f"{base_head}..{branch}")).splitlines()
291
- return {"task": task, "branch": branch, "path": str(path), "head": head,
292
- "status": status, "files": sorted(item for item in files if item)}
293
-
294
-
295
- def collect(root: Path, run_id: str, tasks: Optional[Sequence[str]]) -> dict[str, object]:
296
- root = root.resolve()
297
- meta = _read_run_meta(root, run_id)
298
- entries = [item for item in meta.get("worktrees", []) if isinstance(item, dict)]
299
- if tasks:
300
- wanted = set(tasks)
301
- entries = [item for item in entries if item.get("task") in wanted]
302
- results: list[dict[str, object]] = []
303
- for entry in entries:
304
- try:
305
- results.append(_collect_one(root, entry, str(meta["base_head"]), str(meta["task_file"])))
306
- except ParallelError as exc:
307
- results.append({"task": str(entry.get("task", "")), "ok": False, "code": exc.code, "message": exc.message})
308
- ok = bool(results) and all(item.get("code") is None for item in results)
309
- return {"ok": ok, "action": "collect", "run_id": run_id, "results": results}
310
-
311
-
312
- def cleanup(root: Path, run_id: str, force: bool) -> dict[str, object]:
313
- root = root.resolve()
314
- meta = _read_run_meta(root, run_id)
315
- results: list[dict[str, object]] = []
316
- for item in meta.get("worktrees", []):
317
- if not isinstance(item, dict):
318
- continue
319
- path = Path(str(item.get("path", "")))
320
- if not path.exists():
321
- results.append({"task": item.get("task"), "removed": False, "reason": "already_absent"})
322
- continue
323
- try:
324
- _require_clean_for_removal(path, force)
325
- _remove_worktree(root, path, force)
326
- branch = str(item.get("branch", ""))
327
- merged = _delete_branch(root, branch) if branch else False
328
- results.append({"task": item.get("task"), "removed": True, "branch_deleted": merged})
329
- except ParallelError as exc:
330
- results.append({"task": item.get("task"), "removed": False, "code": exc.code, "message": exc.message})
331
- _run_git(root, ("worktree", "prune"))
332
- run_dir = root / WORKTREES_DIR / run_id
333
- if run_dir.is_dir() and not any(child.is_dir() for child in run_dir.iterdir()):
334
- (run_dir / "run.json").unlink(missing_ok=True)
335
- run_dir.rmdir()
336
- ok = all(item.get("removed") or item.get("reason") == "already_absent" for item in results)
337
- return {"ok": ok, "action": "cleanup", "run_id": run_id, "results": results}
338
-
339
-
340
- def _require_clean_for_removal(path: Path, force: bool) -> None:
341
- if force:
342
- return
343
- dirty = _status_paths(path)
344
- if dirty:
345
- raise ParallelError("worktree_dirty", f"worktree 有未提交改动: {', '.join(dirty[:8])}")
346
-
347
-
348
- def _emit(payload: dict[str, object], stdout: IO[str]) -> None:
349
- stdout.write(json.dumps(payload, ensure_ascii=False) + "\n")
350
-
351
-
352
- def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
353
- parser = argparse.ArgumentParser(prog="cf_task_parallel.py")
354
- sub = parser.add_subparsers(dest="action", required=True)
355
- for name in ("prepare", "collect", "cleanup"):
356
- command = sub.add_parser(name)
357
- command.add_argument("--root", default=".")
358
- command.add_argument("--json", action="store_true")
359
- prepare_parser = sub.choices["prepare"]
360
- prepare_parser.add_argument("--task-file", required=True)
361
- prepare_parser.add_argument("--tasks", required=True)
362
- prepare_parser.add_argument("--run-id", default="")
363
- for name in ("collect", "cleanup"):
364
- sub.choices[name].add_argument("--run-id", required=True)
365
- sub.choices["collect"].add_argument("--tasks", default="")
366
- sub.choices["cleanup"].add_argument("--force", action="store_true")
367
- args = parser.parse_args(argv)
368
- root = Path(args.root).resolve()
369
- try:
370
- if args.action == "prepare":
371
- tasks = [item.strip() for item in args.tasks.split(",") if item.strip()]
372
- payload = prepare(root, args.task_file, tasks, args.run_id or None)
373
- elif args.action == "collect":
374
- tasks = [item.strip() for item in args.tasks.split(",") if item.strip()] or None
375
- payload = collect(root, args.run_id, tasks)
376
- else:
377
- payload = cleanup(root, args.run_id, args.force)
378
- except ParallelError as exc:
379
- payload = {"ok": False, "action": args.action, "code": exc.code, "message": exc.message}
380
- _emit(payload, stdout)
381
- return 0 if payload.get("ok") else 2
382
4
 
5
+ from codeflow.cli import cf_task_parallel as _impl
383
6
 
384
7
  if __name__ == "__main__":
385
- raise SystemExit(main())
8
+ raise SystemExit(_impl.main())
9
+ else:
10
+ sys.modules[__name__] = _impl
@@ -1,107 +1,10 @@
1
- """Executable task handoffs used by every platform command."""
2
- from __future__ import annotations
3
-
4
- import argparse
5
- import json
6
- import os
7
- import re
8
- from pathlib import Path
9
- from typing import IO, Optional, Sequence
1
+ #!/usr/bin/env python3
2
+ """Path-frozen entry shim: implementation lives in codeflow.cli.cf_task_workflow."""
10
3
  import sys
11
4
 
12
- from cf_acceptance_runner import run_manifest
13
- from cf_acceptance_schema import load_manifest, validate_execution_baseline, verified_evidence
14
- from cf_task_index import parse_task_file
15
- from cf_task_runtime import run_done_gate
16
- from cf_workflow_service import (FINISHED_STATUSES, _require_marker_task,
17
- _sync_markdown, block_task, complete_task, locate_task_file, resume_task)
18
- from cf_workflow_transaction import recover_transition
19
-
20
-
21
- def finish_task(root: str, directory: str, task_file: str, task_id: str) -> dict[str, object]:
22
- _require_marker_task(root, task_id, directory, task_file)
23
- gate = run_done_gate(root, directory, task_id=task_id)
24
- if gate.decision != "pass":
25
- return {"decision": "block", "reason": gate.message, "evidence": gate.evidence}
26
- return {"decision": "pass", **complete_task(root, directory, task_file, task_id, True)}
27
-
28
-
29
- def _demand_tasks(directory: Path) -> list[tuple[Path, str, str]]:
30
- tasks = []
31
- seen = set()
32
- for task_file in sorted(directory.glob("*.md")):
33
- if task_file.name.endswith((".design.md", ".prd.md")):
34
- continue
35
- if not re.search(r"(?m)^## TASK-\d+:", task_file.read_text(encoding="utf-8")):
36
- continue
37
- for node in parse_task_file(str(task_file)):
38
- if node.task_id in seen:
39
- raise ValueError(f"duplicate task id: {node.task_id}")
40
- seen.add(node.task_id)
41
- tasks.append((task_file, node.task_id, node.status))
42
- if not tasks:
43
- raise ValueError("no TASK sections found")
44
- return tasks
45
-
46
-
47
- def verify_e2e(root: str, directory: str) -> dict[str, object]:
48
- recover_transition(root)
49
- tasks = _demand_tasks(Path(directory))
50
- unfinished = [task_id for _, task_id, status in tasks if status not in FINISHED_STATUSES]
51
- if unfinished:
52
- return {"decision": "block", "reason": "unfinished_tasks", "tasks": unfinished}
53
- if (Path(root) / ".code-flow/.active-task.json").exists():
54
- return {"decision": "block", "reason": "finish_active_task_first"}
55
- manifest = Path(directory) / ".acceptance-manifest.json"
56
- data = load_manifest(manifest)
57
- validate_execution_baseline(manifest, data)
58
- bound_task = (manifest.parent / str(data.get("task_file", ""))).resolve()
59
- if {task.resolve() for task, _, _ in tasks} != {bound_task}:
60
- return {"decision": "block", "reason": "acceptance_manifest_task_mismatch"}
61
- required = [row["id"] for row in data["scenarios"] if row.get("kind", "functional") != "e2e"
62
- and not verified_evidence(row)]
63
- if required:
64
- return {"decision": "block", "reason": "functional_or_manual_evidence_missing", "scenarios": required}
65
- result = run_manifest(str(manifest), root, write_evidence=True, only_e2e=True)
66
- if result["decision"] == "pass":
67
- for task_file, task_id, _ in tasks:
68
- _sync_markdown(str(task_file), task_id, "verified")
69
- return result
70
-
71
-
72
- def main(argv: Optional[Sequence[str]] = None, stdout: IO[str] = sys.stdout) -> int:
73
- parser = argparse.ArgumentParser()
74
- parser.add_argument("action", choices=("finish", "block", "resume", "verify-e2e"))
75
- parser.add_argument("--root", default=os.getcwd())
76
- parser.add_argument("--task-dir", required=True)
77
- parser.add_argument("--task", default="")
78
- parser.add_argument("--reason", default="")
79
- parser.add_argument("--json", action="store_true")
80
- args = parser.parse_args(argv)
81
- try:
82
- root = str(Path(args.root).resolve())
83
- directory = str((Path(root) / args.task_dir).resolve())
84
- if Path(root) not in Path(directory).parents:
85
- raise ValueError("task directory outside project")
86
- recover_transition(root)
87
- if args.action == "verify-e2e":
88
- result = verify_e2e(root, directory)
89
- else:
90
- task = locate_task_file(directory, args.task)
91
- if task is None:
92
- raise ValueError("task file missing or ambiguous")
93
- if args.action == "finish":
94
- result = finish_task(root, directory, str(task), args.task)
95
- elif args.action == "block":
96
- result = block_task(root, directory, str(task), args.task, args.reason)
97
- else:
98
- result = resume_task(root, directory, str(task), args.task)
99
- stdout.write(json.dumps(result, ensure_ascii=False))
100
- return 3 if result.get("decision") == "block" else 0
101
- except (OSError, ValueError) as exc:
102
- stdout.write(json.dumps({"decision": "block", "error": str(exc)}, ensure_ascii=False))
103
- return 2
104
-
5
+ from codeflow.cli import cf_task_workflow as _impl
105
6
 
106
7
  if __name__ == "__main__":
107
- raise SystemExit(main())
8
+ raise SystemExit(_impl.main())
9
+ else:
10
+ sys.modules[__name__] = _impl