@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,253 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Worktree isolation contract for acceptance checks.
3
-
4
- Isolated checks receive a deterministic namespace so parallel worktrees never
5
- share ports or databases. Shared checks stay serial by default: they may pass
6
- `parallel_safe: true`, but the effective answer is always false because a
7
- shared service cannot be partitioned across worktrees.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import hashlib
13
- import json
14
- import os
15
- import re
16
- import shlex
17
- import socket
18
- import subprocess
19
- import time
20
- from contextlib import contextmanager
21
- from pathlib import Path
22
- from typing import Iterator, Mapping, Optional
23
- from uuid import uuid4
24
-
25
- ENV_SCOPES = ("shared", "isolated")
26
- _SAFE_COMPONENT = re.compile(r"[^a-z0-9]+")
27
- _LOCK_RELATIVE = Path("code-flow/locks/shared-env.lock")
28
- _LOCK_POLL_SECONDS = 0.05
29
- DEFAULT_SHARED_LOCK_WAIT = 300.0
30
- DEFAULT_SHARED_LOCK_STALE = 1800.0
31
-
32
-
33
- class SharedEnvLockTimeout(TimeoutError):
34
- """A shared environment stayed locked past the configured wait budget."""
35
-
36
- def __init__(self, path: Path, waited: float) -> None:
37
- super().__init__(f"shared environment lock timeout after {waited:.1f}s: {path}")
38
- self.path = path
39
- self.waited = waited
40
-
41
-
42
- def env_scope(check: Mapping[str, object]) -> str:
43
- value = check.get("env_scope", "shared")
44
- return value if value in ENV_SCOPES else "shared"
45
-
46
-
47
- def is_isolated(check: Mapping[str, object]) -> bool:
48
- return env_scope(check) == "isolated"
49
-
50
-
51
- def effective_parallel_safe(check: Mapping[str, object]) -> bool:
52
- """Shared environments are never parallel-safe, regardless of the flag."""
53
- if not is_isolated(check):
54
- return False
55
- declared = check.get("parallel_safe", False)
56
- return declared is True
57
-
58
-
59
- def worktree_identity(root: str, explicit: str = "") -> str:
60
- for candidate in (explicit, os.environ.get("CF_WORKTREE_ID", "")):
61
- if isinstance(candidate, str) and candidate.strip():
62
- return candidate.strip()
63
- path = Path(root).resolve()
64
- digest = hashlib.sha256(str(path).encode("utf-8")).hexdigest()[:8]
65
- return f"{path.name or 'worktree'}-{digest}"
66
-
67
-
68
- def resolve_run_id(explicit: str = "") -> str:
69
- for candidate in (explicit, os.environ.get("CF_RUN_ID", "")):
70
- if isinstance(candidate, str) and candidate.strip():
71
- return candidate.strip()
72
- return uuid4().hex
73
-
74
-
75
- def port_offset(worktree_id: str) -> int:
76
- digest = hashlib.sha256(worktree_id.encode("utf-8")).hexdigest()
77
- return 10000 + int(digest[:4], 16) % 40000
78
-
79
-
80
- def db_suffix(worktree_id: str) -> str:
81
- slug = _SAFE_COMPONENT.sub("_", worktree_id.strip().lower()).strip("_") or "worktree"
82
- digest = hashlib.sha256(worktree_id.encode("utf-8")).hexdigest()[:6]
83
- return f"_{slug}_{digest}"
84
-
85
-
86
- def worktree_namespace(root: str, worktree_id: str = "", run_id: str = "", task_id: str = "") -> dict[str, str]:
87
- """Stable per-worktree namespace injected into isolated child processes."""
88
- identity = worktree_identity(root, worktree_id)
89
- return {
90
- "CF_WORKTREE_ID": identity,
91
- "CF_RUN_ID": resolve_run_id(run_id),
92
- "CF_TASK_ID": task_id or os.environ.get("CF_TASK_ID", ""),
93
- "CF_PORT_OFFSET": str(port_offset(identity)),
94
- "CF_DB_SUFFIX": db_suffix(identity),
95
- }
96
-
97
-
98
- def check_environment(
99
- check: Mapping[str, object], root: str, namespace: Optional[Mapping[str, str]] = None
100
- ) -> dict[str, str]:
101
- """Environment overlay for a check: isolated only, shared is untouched."""
102
- if not is_isolated(check):
103
- return {}
104
- resolved = dict(namespace) if namespace is not None else worktree_namespace(root)
105
- return {key: str(value) for key, value in resolved.items()}
106
-
107
-
108
- def _git_common_dir(root: str) -> Path:
109
- """Directory shared by every worktree of the same repository."""
110
- result = subprocess.run(
111
- ("git", "rev-parse", "--git-common-dir"),
112
- cwd=str(root), text=True, encoding="utf-8", capture_output=True, check=False,
113
- )
114
- if result.returncode == 0 and result.stdout.strip():
115
- common = Path(result.stdout.strip())
116
- return common if common.is_absolute() else (Path(root) / common).resolve()
117
- return (Path(root) / ".code-flow").resolve()
118
-
119
-
120
- def shared_env_lock_path(root: str) -> Path:
121
- """Lock file lives in the git common dir so all worktrees contend for it."""
122
- return _git_common_dir(root) / _LOCK_RELATIVE
123
-
124
-
125
- def _lock_payload(token: str, run_id: str, worktree_id: str) -> dict[str, object]:
126
- return {
127
- "token": token,
128
- "pid": os.getpid(),
129
- "host": socket.gethostname(),
130
- "created_at": time.time(),
131
- "run_id": run_id,
132
- "worktree_id": worktree_id,
133
- }
134
-
135
-
136
- def _read_lock(path: Path) -> Optional[dict[str, object]]:
137
- try:
138
- data = json.loads(path.read_text(encoding="utf-8"))
139
- except (OSError, json.JSONDecodeError):
140
- return None
141
- return data if isinstance(data, dict) else None
142
-
143
-
144
- def _lock_is_stale(path: Path, stale_after: float) -> bool:
145
- """Age is authoritative; a dead local PID is stale before the timeout."""
146
- try:
147
- stat = path.stat()
148
- except OSError:
149
- return False
150
- data = _read_lock(path)
151
- if data is None:
152
- # A writer may be between O_CREAT and its first write; only break files
153
- # that stayed unreadable beyond the grace window.
154
- grace = min(max(stale_after, 0.1), 1.0)
155
- return time.time() - stat.st_mtime >= grace
156
- if data.get("host") == socket.gethostname():
157
- pid = data.get("pid")
158
- if isinstance(pid, int) and pid > 0:
159
- try:
160
- os.kill(pid, 0)
161
- except ProcessLookupError:
162
- return True
163
- except (PermissionError, OSError):
164
- return False
165
- # A live local owner is never stale purely because the run is long.
166
- return False
167
- created = data.get("created_at")
168
- if isinstance(created, (int, float)) and time.time() - float(created) >= stale_after:
169
- return True
170
- return False
171
-
172
-
173
- def _acquire_shared_lock(path: Path, wait: float, stale_after: float, token: str,
174
- run_id: str, worktree_id: str) -> None:
175
- path.parent.mkdir(parents=True, exist_ok=True)
176
- deadline = time.monotonic() + wait
177
- while True:
178
- try:
179
- descriptor = os.open(str(path), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
180
- except FileExistsError:
181
- if _lock_is_stale(path, stale_after):
182
- try:
183
- path.unlink()
184
- except FileNotFoundError:
185
- pass
186
- continue
187
- remaining = deadline - time.monotonic()
188
- if remaining <= 0:
189
- raise SharedEnvLockTimeout(path, wait) from None
190
- time.sleep(min(_LOCK_POLL_SECONDS, remaining))
191
- continue
192
- with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
193
- handle.write(json.dumps(_lock_payload(token, run_id, worktree_id), ensure_ascii=False))
194
- handle.flush()
195
- os.fsync(handle.fileno())
196
- return
197
-
198
-
199
- def _release_shared_lock(path: Path, token: str) -> None:
200
- data = _read_lock(path)
201
- if not isinstance(data, dict) or data.get("token") != token:
202
- return
203
- try:
204
- path.unlink()
205
- except FileNotFoundError:
206
- return
207
-
208
-
209
- @contextmanager
210
- def shared_env_lock(
211
- root: str,
212
- *,
213
- wait: Optional[float] = None,
214
- stale_after: Optional[float] = None,
215
- run_id: str = "",
216
- worktree_id: str = "",
217
- ) -> Iterator[dict[str, object]]:
218
- """Cross-worktree lock for shared (non-partitionable) environment checks.
219
-
220
- The lock is a JSON sidecar in the git common directory with an owner token.
221
- Wait and stale budgets are explicit so a crashed worktree cannot block the
222
- batch forever, while a live owner keeps the lock until released.
223
- """
224
- path = shared_env_lock_path(root)
225
- resolved_wait = DEFAULT_SHARED_LOCK_WAIT if wait is None else max(0.0, float(wait))
226
- resolved_stale = DEFAULT_SHARED_LOCK_STALE if stale_after is None else max(0.1, float(stale_after))
227
- token = uuid4().hex
228
- identity = worktree_id or worktree_identity(root)
229
- key = run_id or resolve_run_id()
230
- _acquire_shared_lock(path, resolved_wait, resolved_stale, token, key, identity)
231
- try:
232
- yield {"path": str(path), "token": token, "run_id": key, "worktree_id": identity}
233
- finally:
234
- _release_shared_lock(path, token)
235
-
236
-
237
- def preflight_commands(check: Mapping[str, object]) -> list[list[str]]:
238
- """Normalize preflight entries (argv array or shell words) to argv arrays."""
239
- raw = check.get("preflight", [])
240
- if not isinstance(raw, list):
241
- raise ValueError("invalid check preflight: expected list")
242
- commands: list[list[str]] = []
243
- for entry in raw:
244
- if isinstance(entry, str):
245
- argv = shlex.split(entry)
246
- elif isinstance(entry, list) and all(isinstance(token, str) for token in entry):
247
- argv = list(entry)
248
- else:
249
- raise ValueError("invalid check preflight entry: expected argv array or command string")
250
- if not argv or any(not token for token in argv):
251
- raise ValueError("invalid check preflight entry: empty argv")
252
- commands.append(argv)
253
- return commands