@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,325 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Shared execution substrate: deadline-aware argv subprocess runner.
3
-
4
- All check executors (acceptance scenarios, Spec verifiers, session
5
- validators) funnel through here so one entry deadline constrains the whole
6
- chain, commands never pass through a shell, and unrun work is reported as
7
- `incomplete` instead of silently skipped or falsely passed.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import hashlib
13
- import os
14
- import re
15
- import signal
16
- import shlex
17
- import json
18
- import subprocess
19
- import sys
20
- import time
21
- from contextlib import contextmanager
22
- from contextvars import ContextVar
23
- from pathlib import Path
24
- from typing import Callable, Iterator, Mapping, Optional, Sequence
25
-
26
- _EXECUTIONS: ContextVar[Optional[dict[str, dict[str, object]]]] = ContextVar("cf_executions", default=None)
27
- _REPORTS: ContextVar[Optional[list[dict[str, object]]]] = ContextVar("cf_reports", default=None)
28
- _SECRET_OPTION = re.compile(r"(?i)^--?(?:api[-_]?key|access[-_]?token|token|password|passwd|secret|credential)$")
29
- _SECRET_ASSIGNMENT = re.compile(r"(?i)(--?(?:api[-_]?key|access[-_]?token|token|password|passwd|secret|credential)=)([^\s]+)")
30
- _RUNTIME_ENV = frozenset(("CF_RUNTIME_COMMAND", "CF_RUNTIME_ACTION", "CF_CLI_VERSION"))
31
-
32
-
33
- def redact_sensitive_text(value: str) -> str:
34
- """Hide inherited sensitive environment values from diagnostics."""
35
- names = ("SECRET", "TOKEN", "PASSWORD", "PASSWD", "CREDENTIAL", "API_KEY", "PRIVATE_KEY")
36
- secrets = {item for key, item in os.environ.items()
37
- if item and any(name in key.upper() for name in names)}
38
- for secret in sorted(secrets, key=len, reverse=True):
39
- value = value.replace(secret, "[REDACTED]")
40
- return _SECRET_ASSIGNMENT.sub(r"\1[REDACTED]", value)
41
-
42
-
43
- def _redact_sequence(values: Sequence[object]) -> list[object]:
44
- result: list[object] = []
45
- hide_next = False
46
- for item in values:
47
- if hide_next:
48
- result.append("[REDACTED]")
49
- hide_next = False
50
- continue
51
- result.append(redact_sensitive_payload(item))
52
- hide_next = isinstance(item, str) and bool(_SECRET_OPTION.fullmatch(item))
53
- return result
54
-
55
-
56
- def redact_sensitive_payload(value: object) -> object:
57
- """Return a JSON-compatible copy with sensitive strings redacted."""
58
- if isinstance(value, str):
59
- return redact_sensitive_text(value)
60
- if isinstance(value, Mapping):
61
- return {key: redact_sensitive_payload(item) for key, item in value.items()}
62
- if isinstance(value, (list, tuple)):
63
- return _redact_sequence(value)
64
- return value
65
-
66
-
67
- @contextmanager
68
- def report_session() -> Iterator[list[dict[str, object]]]:
69
- reports: list[dict[str, object]] = []
70
- token = _REPORTS.set(reports)
71
- try:
72
- yield reports
73
- finally:
74
- _REPORTS.reset(token)
75
-
76
-
77
- def record_report(report: Mapping[str, object]) -> None:
78
- reports = _REPORTS.get()
79
- if reports is not None:
80
- reports.append(dict(report))
81
-
82
-
83
- def report_requested() -> bool:
84
- return _REPORTS.get() is not None
85
-
86
-
87
- @contextmanager
88
- def execution_session() -> Iterator[None]:
89
- """Share results only inside one validation operation, never across turns."""
90
- if _EXECUTIONS.get() is not None:
91
- yield
92
- return
93
- token = _EXECUTIONS.set({})
94
- try:
95
- yield
96
- finally:
97
- _EXECUTIONS.reset(token)
98
-
99
-
100
- def invalidate_executions() -> None:
101
- cache = _EXECUTIONS.get()
102
- if cache is not None:
103
- cache.clear()
104
-
105
-
106
- def command_environment(env: Optional[Mapping[str, str]] = None) -> dict[str, str]:
107
- """Do not pass CLI dispatch metadata into user checks."""
108
- environment = {key: value for key, value in os.environ.items() if key not in _RUNTIME_ENV}
109
- environment.update({str(key): str(value) for key, value in (env or {}).items()})
110
- return environment
111
-
112
-
113
- def execution_key(argv: Sequence[str], cwd: str, timeout: float,
114
- env: Optional[Mapping[str, str]] = None) -> str:
115
- """Identity of a command under the current process environment."""
116
- environment = command_environment(env)
117
- payload: list[object] = [list(argv), str(Path(cwd).resolve()), float(timeout), sorted(environment.items())]
118
- return hashlib.sha256(json.dumps(payload, ensure_ascii=False).encode()).hexdigest()
119
-
120
-
121
- def execution_environment_hash(env: Optional[Mapping[str, str]] = None) -> str:
122
- """Bind evidence to inherited and explicit environment without exposing values."""
123
- environment = command_environment(env)
124
- payload = json.dumps(sorted(environment.items()), ensure_ascii=False)
125
- return hashlib.sha256(payload.encode()).hexdigest()
126
-
127
-
128
- def remaining_seconds(deadline: Optional[float]) -> Optional[float]:
129
- """Seconds left until the absolute monotonic deadline (None = unbounded)."""
130
- if deadline is None:
131
- return None
132
- return deadline - time.monotonic()
133
-
134
-
135
- def build_argv(template: str, files: Sequence[str]) -> list[str]:
136
- """Expand a validator command template into argv without a shell.
137
-
138
- `{files}` must be a standalone token and expands to one argv entry per
139
- file (spaces/metachars safe). Embedded usage is a config error — refusing
140
- is safer than reintroducing shell splitting.
141
- """
142
- tokens = shlex.split(template or "")
143
- if not tokens:
144
- return []
145
- argv: list[str] = []
146
- for token in tokens:
147
- if token == "{files}":
148
- argv.extend(files)
149
- elif "{files}" in token:
150
- raise ValueError(f"validator command must keep {{files}} as a standalone token: {template!r}")
151
- else:
152
- argv.append(token)
153
- return argv
154
-
155
-
156
- def run_command(
157
- argv: Sequence[str],
158
- cwd: str,
159
- timeout: float,
160
- deadline: Optional[float] = None,
161
- env: Optional[Mapping[str, str]] = None,
162
- identity_context: Optional[Mapping[str, object]] = None,
163
- ) -> dict[str, object]:
164
- """Run argv synchronously under timeout+deadline. Never uses a shell."""
165
- key = execution_key(argv, cwd, timeout, env)
166
- if identity_context:
167
- payload = json.dumps(identity_context, sort_keys=True, ensure_ascii=False, default=str)
168
- key = hashlib.sha256(f"{key}:{payload}".encode()).hexdigest()
169
- cache = _EXECUTIONS.get()
170
- fingerprint = ""
171
- if cache is not None:
172
- root = _fingerprint_git(cwd, "rev-parse", "--show-toplevel")
173
- if root is not None and root.returncode == 0:
174
- fingerprint = worktree_fingerprint(root.stdout.strip())
175
- key = hashlib.sha256(f"{key}:{fingerprint}".encode()).hexdigest() if fingerprint else ""
176
- if cache is not None and key and key in cache:
177
- return {**cache[key], "execution_ms": 0.0, "execution_reused": True}
178
- started = time.monotonic()
179
- result = _execute(argv, cwd, timeout, deadline, env)
180
- result["execution_ms"] = ((time.monotonic() - started) * 1000
181
- if result.get("execution_started", True) else 0.0)
182
- result["execution_reused"] = False
183
- if cache is not None and key and result["status"] == "ok" and result["returncode"] == 0:
184
- cache[key] = result
185
- return result
186
-
187
-
188
- def hash_worktree_paths(project_root: str, paths: Sequence[str]) -> Optional[list]:
189
- """Batch-hash worktree file contents (single `git hash-object` process)."""
190
- if not paths:
191
- return []
192
- try:
193
- completed = subprocess.run(
194
- ("git", "-C", project_root, "hash-object", "--stdin-paths"),
195
- input="\n".join(paths) + "\n", text=True, capture_output=True, timeout=5,
196
- )
197
- except (OSError, subprocess.TimeoutExpired) as exc:
198
- sys.stderr.write(f"worktree hashing failed: {exc}\n")
199
- return None
200
- if completed.returncode != 0:
201
- return None
202
- hashes = completed.stdout.splitlines()
203
- if len(hashes) != len(paths):
204
- return None
205
- return [(path, sha.strip()) for path, sha in zip(paths, hashes)]
206
-
207
-
208
- def _fingerprint_git(root: str, *arguments: str) -> Optional[subprocess.CompletedProcess]:
209
- try:
210
- return subprocess.run(
211
- ("git", "-C", root, *arguments), text=True, encoding="utf-8",
212
- capture_output=True, check=False, timeout=5,
213
- )
214
- except (OSError, subprocess.TimeoutExpired) as exc:
215
- sys.stderr.write(f"worktree fingerprint failed: {exc}\n")
216
- return None
217
-
218
-
219
- def _runtime_path(path: str) -> bool:
220
- return path.startswith(".code-flow/") or Path(path).name == ".acceptance-manifest.json"
221
-
222
-
223
- def _fingerprint_changes(status: str, contents: dict[str, str], include_untracked: bool,
224
- untracked_filter: Optional[Callable[[str], bool]]) -> Optional[list[str]]:
225
- changed: list[str] = []
226
- for line in status.splitlines():
227
- code, path = line[:2], line[3:].strip()
228
- if " -> " in path:
229
- path = path.split(" -> ", 1)[1].strip()
230
- if not path or _runtime_path(path):
231
- continue
232
- if path.startswith('"'):
233
- return None # Quoted/escaped paths cannot be identified reliably.
234
- if code.strip() == "??":
235
- if include_untracked and (untracked_filter is None or untracked_filter(path)):
236
- changed.append(path)
237
- elif "D" in code:
238
- contents.pop(path, None)
239
- else:
240
- changed.append(path)
241
- return changed
242
-
243
-
244
- def worktree_fingerprint(project_root: str, untracked_filter: Optional[Callable[[str], bool]] = None,
245
- include_untracked: bool = True) -> str:
246
- """Hash real content including new files, excluding workflow runtime state.
247
-
248
- Staging/committing unchanged bytes does not change identity. Ignored files
249
- never participate; test outputs must be Git-ignored. Explicit check inputs
250
- are hashed separately. Return an empty identity when it cannot be proven.
251
- """
252
- tree = _fingerprint_git(project_root, "ls-tree", "-r", "HEAD")
253
- status = _fingerprint_git(project_root, "status", "--porcelain", "--untracked-files=all")
254
- if tree is None or status is None or tree.returncode != 0 or status.returncode != 0:
255
- return ""
256
- contents: dict[str, str] = {}
257
- for line in tree.stdout.splitlines():
258
- meta, _, path = line.partition("\t")
259
- fields = meta.split()
260
- if path and len(fields) >= 3 and not _runtime_path(path):
261
- contents[path] = fields[2]
262
- changed = _fingerprint_changes(status.stdout, contents, include_untracked, untracked_filter)
263
- if changed is None:
264
- return ""
265
- hashed = hash_worktree_paths(project_root, changed)
266
- if hashed is None:
267
- return ""
268
- for path, sha in hashed:
269
- contents[path] = sha
270
- digest = hashlib.sha256()
271
- for path in sorted(contents):
272
- digest.update(path.encode())
273
- digest.update(b"\0")
274
- digest.update(contents[path].encode())
275
- digest.update(b"\n")
276
- return digest.hexdigest()
277
-
278
-
279
- def _execute(argv: Sequence[str], cwd: str, timeout: float, deadline: Optional[float],
280
- env: Optional[Mapping[str, str]] = None) -> dict[str, object]:
281
- remaining = remaining_seconds(deadline)
282
- if remaining is not None and remaining <= 0:
283
- return {"status": "deadline_exceeded", "returncode": None, "stdout": "", "stderr": "",
284
- "execution_started": False}
285
- # When the entry deadline is tighter than the command timeout, expiry is
286
- # unexecuted work (`incomplete`), never a product failure.
287
- deadline_bounded = remaining is not None and remaining <= timeout
288
- effective = min(timeout, remaining) if remaining is not None else timeout
289
- child_env = command_environment(env)
290
- try:
291
- proc = subprocess.Popen(
292
- list(argv), cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True,
293
- env=child_env, start_new_session=os.name != "nt",
294
- )
295
- out, err = proc.communicate(timeout=max(effective, 0.01))
296
- except subprocess.TimeoutExpired as exc:
297
- return _timeout_result(proc, exc, deadline_bounded)
298
- except OSError as exc:
299
- return {"status": "spawn_error", "returncode": None, "stdout": "", "stderr": str(exc),
300
- "execution_started": False}
301
- return {"status": "ok", "returncode": proc.returncode, "stdout": out or "", "stderr": err or ""}
302
-
303
-
304
- def _timeout_result(proc: subprocess.Popen, expired: subprocess.TimeoutExpired,
305
- deadline_bounded: bool) -> dict[str, object]:
306
- if os.name != "nt":
307
- try:
308
- os.killpg(proc.pid, signal.SIGKILL)
309
- except ProcessLookupError:
310
- pass # The process group already exited between timeout and kill.
311
- else:
312
- subprocess.run(("taskkill", "/F", "/T", "/PID", str(proc.pid)),
313
- capture_output=True, timeout=2, check=False)
314
- proc.kill()
315
- try:
316
- out, err = proc.communicate(timeout=1)
317
- except subprocess.TimeoutExpired:
318
- out, err = expired.stdout, expired.stderr
319
- for pipe in (proc.stdout, proc.stderr):
320
- if pipe is not None:
321
- pipe.close()
322
- def tail(value: object) -> str:
323
- return (value.decode(errors="replace") if isinstance(value, bytes) else str(value or ""))[-4000:]
324
- return {"status": "deadline_exceeded" if deadline_bounded else "timeout", "returncode": None,
325
- "stdout": tail(out), "stderr": tail(err)}
@@ -1,57 +0,0 @@
1
- #!/usr/bin/env python3
2
- """L0 filesystem primitives: text reads and atomic replacement writes."""
3
-
4
- from __future__ import annotations
5
-
6
- import os
7
- import tempfile
8
- from pathlib import Path
9
- from typing import Optional, Union
10
-
11
- PathLike = Union[str, os.PathLike]
12
-
13
-
14
- def read_text(path: PathLike) -> Optional[str]:
15
- """Read UTF-8 text; a missing file is None, other IO errors propagate."""
16
- try:
17
- return Path(path).read_text(encoding="utf-8")
18
- except FileNotFoundError:
19
- return None
20
-
21
-
22
- def write_text(path: PathLike, text: str) -> None:
23
- """Write UTF-8 text in place (non-atomic convenience writer)."""
24
- Path(path).write_text(text, encoding="utf-8")
25
-
26
-
27
- def atomic_write_text(path: PathLike, text: str) -> None:
28
- """Replace path with exact UTF-8 text via a temp file and os.replace.
29
-
30
- On any failure the target keeps its previous content and the temp file is
31
- removed before the original exception propagates. Disable native newline
32
- translation so transaction snapshots also round-trip unchanged on Windows.
33
- """
34
- target = Path(path)
35
- temporary = ""
36
- try:
37
- with tempfile.NamedTemporaryFile(
38
- "w",
39
- encoding="utf-8",
40
- newline="",
41
- dir=str(target.parent),
42
- prefix=f".{target.name}.",
43
- suffix=".tmp",
44
- delete=False,
45
- ) as handle:
46
- temporary = handle.name
47
- handle.write(text)
48
- handle.flush()
49
- os.fsync(handle.fileno())
50
- os.replace(temporary, target)
51
- except Exception:
52
- if temporary:
53
- try:
54
- os.unlink(temporary)
55
- except OSError:
56
- pass # best effort: the original failure is what callers need
57
- raise
@@ -1,92 +0,0 @@
1
- #!/usr/bin/env python3
2
- """L0 Git primitives: one implementation of the git CLI for every consumer."""
3
-
4
- from __future__ import annotations
5
-
6
- import subprocess
7
- from typing import Mapping, Optional, Sequence
8
-
9
- _STATUS_ARGV = ("status", "--porcelain=v1", "-z", "--untracked-files=all")
10
- _STATUS_NO_UNTRACKED_ARGV = ("status", "--porcelain=v1", "-z", "--untracked-files=no")
11
-
12
-
13
- class GitError(RuntimeError):
14
- """Stable git failure carrying a machine code and the failing argv."""
15
-
16
- def __init__(self, code: str, message: str, *, argv: Sequence[str] = (),
17
- returncode: Optional[int] = None) -> None:
18
- super().__init__(message)
19
- self.code = code
20
- self.message = message
21
- self.argv = tuple(argv)
22
- self.returncode = returncode
23
-
24
-
25
- def run_git(root: str, argv: Sequence[str], *, timeout: float = 30) -> str:
26
- """Run git in root and return stdout; any failure raises GitError."""
27
- try:
28
- result = subprocess.run(
29
- ("git", *argv), cwd=root, text=True, encoding="utf-8",
30
- capture_output=True, check=False, timeout=timeout,
31
- )
32
- except OSError as exc:
33
- raise GitError("git_unavailable", str(exc), argv=argv) from exc
34
- except subprocess.TimeoutExpired as exc:
35
- raise GitError("git_timeout", f"git timed out after {timeout}s", argv=argv) from exc
36
- if result.returncode != 0:
37
- message = result.stderr.strip() or "Git command failed"
38
- raise GitError("git_error", message, argv=argv, returncode=result.returncode)
39
- return result.stdout
40
-
41
-
42
- def _status_name(code: str) -> str:
43
- if code == "??":
44
- return "untracked"
45
- if "D" in code:
46
- return "deleted"
47
- if "A" in code:
48
- return "added"
49
- if "M" in code:
50
- return "modified"
51
- return "changed"
52
-
53
-
54
- def changed_entries(root: str, *, include_untracked: bool = True) -> Mapping[str, str]:
55
- """Working-tree changes keyed by relative path; renames also name the old path."""
56
- argv = _STATUS_ARGV if include_untracked else _STATUS_NO_UNTRACKED_ARGV
57
- records = run_git(root, argv).split("\0")
58
- changes: dict[str, str] = {}
59
- index = 0
60
- while index < len(records) and records[index]:
61
- record = records[index]
62
- if len(record) < 4:
63
- raise GitError("invalid_git_status", record, argv=argv)
64
- code, path = record[:2], record[3:]
65
- index += 1
66
- if "R" in code or "C" in code:
67
- # porcelain -z rename/copy emits two records: "XY <new>" then "<old>"
68
- if index >= len(records) or not records[index]:
69
- raise GitError("invalid_git_status", f"{code} {path}", argv=argv)
70
- old_path = records[index]
71
- index += 1
72
- changes[path] = _status_name(code)
73
- changes[old_path] = "deleted"
74
- continue
75
- changes[path] = _status_name(code)
76
- return changes
77
-
78
-
79
- def changed_files(root: str, *, include_untracked: bool = True) -> tuple[str, ...]:
80
- """Relative paths with uncommitted changes (working tree and index)."""
81
- return tuple(changed_entries(root, include_untracked=include_untracked))
82
-
83
-
84
- def tracked_files(root: str) -> tuple[str, ...]:
85
- """Tracked relative paths; non-git projects read as an empty set."""
86
- try:
87
- output = run_git(root, ("ls-files",))
88
- except GitError:
89
- return ()
90
- return tuple(
91
- line.replace("\\", "/").strip() for line in output.splitlines() if line.strip()
92
- )
@@ -1,160 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Session activity log: JSONL event stream for the quality-loop feature.
3
-
4
- Design contract (spec-quality-loop.design.md §3.3):
5
- - One event per line: {"v", "ts", "sid", "event", "data"}.
6
- - append is O(1) single-write; failures are swallowed (stderr only) so the
7
- hook protocol is never broken by logging (RULE-01 / E-03).
8
- - Reads are line-tolerant: corrupt lines are skipped, never raised (RULE-05).
9
- - Rotation: when the live file reaches MAX_BYTES it is archived to
10
- sessions/YYYY-MM/session-log-<ts>.jsonl and a fresh file starts (B-02).
11
- """
12
- import json
13
- import os
14
- import sys
15
- from datetime import datetime, timedelta
16
-
17
- LOG_NAME = ".session-log.jsonl"
18
- SESSIONS_DIR = "sessions"
19
- MAX_BYTES = 5 * 1024 * 1024
20
- SCHEMA_VERSION = 1
21
-
22
- KNOWN_EVENTS = (
23
- "inject",
24
- "edit",
25
- "violation",
26
- "correction",
27
- "false_positive",
28
- "degrade",
29
- "stop_check",
30
- "spec_candidate",
31
- "spec_bound",
32
- "spec_applied",
33
- "spec_read",
34
- "spec_drift",
35
- "spec_gate",
36
- "late_violation",
37
- "metrics_degraded",
38
- )
39
-
40
-
41
- def warn(msg: str) -> None:
42
- """Write one diagnostic line to stderr; stdout stays reserved for protocol JSON."""
43
- sys.stderr.write(f"[cf-log] {msg}\n")
44
-
45
-
46
- def log_path(project_root: str) -> str:
47
- return os.path.join(project_root, ".code-flow", LOG_NAME)
48
-
49
-
50
- def _archive_dir(project_root: str, month: str) -> str:
51
- return os.path.join(project_root, ".code-flow", SESSIONS_DIR, month)
52
-
53
-
54
- def _rotate_if_needed(project_root: str, path: str) -> None:
55
- try:
56
- if os.path.getsize(path) < MAX_BYTES:
57
- return
58
- except OSError:
59
- return
60
- now = datetime.now()
61
- month_dir = _archive_dir(project_root, now.strftime("%Y-%m"))
62
- os.makedirs(month_dir, exist_ok=True)
63
- stamp = now.strftime("%Y%m%dT%H%M%S")
64
- target = os.path.join(month_dir, f"session-log-{stamp}.jsonl")
65
- # Second-resolution names collide when a log rotates twice quickly; never
66
- # overwrite an existing archive.
67
- counter = 1
68
- while os.path.exists(target):
69
- counter += 1
70
- target = os.path.join(month_dir, f"session-log-{stamp}-{counter}.jsonl")
71
- os.replace(path, target)
72
-
73
-
74
- def append_event(project_root: str, event: str, data: dict, sid: str) -> bool:
75
- """Append one event line. Never raises; returns False on failure (E-03)."""
76
- try:
77
- path = log_path(project_root)
78
- os.makedirs(os.path.dirname(path), exist_ok=True)
79
- _rotate_if_needed(project_root, path)
80
- line = json.dumps(
81
- {
82
- "v": SCHEMA_VERSION,
83
- "ts": datetime.now().isoformat(timespec="seconds"),
84
- "sid": sid,
85
- "event": event,
86
- "data": data or {},
87
- },
88
- ensure_ascii=False,
89
- )
90
- with open(path, "a", encoding="utf-8") as f:
91
- f.write(line + "\n")
92
- return True
93
- except Exception as exc:
94
- warn(f"append_event failed: {exc}")
95
- return False
96
-
97
-
98
- def degrade(project_root: str, component: str, error: object, sid: str = "") -> None:
99
- """Record a degrade event (FEAT-02). Never raises — degrading the
100
- degrade-logger itself must stay invisible (RULE-01)."""
101
- append_event(
102
- project_root,
103
- "degrade",
104
- {"component": component, "error": str(error)[:200]},
105
- sid,
106
- )
107
-
108
-
109
- def _candidate_files(project_root: str, days: int) -> list:
110
- """Live log + archived months overlapping the [now-days, now] window."""
111
- paths = []
112
- base = os.path.join(project_root, ".code-flow", SESSIONS_DIR)
113
- if os.path.isdir(base):
114
- cutoff = datetime.now() - timedelta(days=days)
115
- for month in sorted(os.listdir(base)):
116
- try:
117
- month_start = datetime.strptime(month, "%Y-%m")
118
- except ValueError:
119
- continue
120
- # keep a month if any part of it can fall inside the window
121
- if month_start >= cutoff.replace(day=1, hour=0, minute=0, second=0, microsecond=0):
122
- month_dir = os.path.join(base, month)
123
- paths.extend(
124
- os.path.join(month_dir, name)
125
- for name in sorted(os.listdir(month_dir))
126
- if name.endswith(".jsonl")
127
- )
128
- live = log_path(project_root)
129
- if os.path.exists(live):
130
- paths.append(live)
131
- return paths
132
-
133
-
134
- def read_events(project_root: str, days: int = 30, events: tuple = ()) -> list:
135
- """Read events within the window, oldest first. Corrupt lines skipped."""
136
- cutoff = (datetime.now() - timedelta(days=days)).isoformat(timespec="seconds")
137
- wanted = set(events) if events else None
138
- out = []
139
- for path in _candidate_files(project_root, days):
140
- try:
141
- with open(path, "r", encoding="utf-8") as f:
142
- for raw in f:
143
- raw = raw.strip()
144
- if not raw:
145
- continue
146
- try:
147
- item = json.loads(raw)
148
- except Exception:
149
- continue
150
- if not isinstance(item, dict) or "event" not in item:
151
- continue
152
- if wanted is not None and item["event"] not in wanted:
153
- continue
154
- if str(item.get("ts", "")) < cutoff:
155
- continue
156
- out.append(item)
157
- except Exception as exc:
158
- warn(f"read_events skipped {path}: {exc}")
159
- continue
160
- return out