@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
package/lib/render.js ADDED
@@ -0,0 +1,115 @@
1
+ 'use strict';
2
+ const path = require('node:path');
3
+ const f = require('./fs');
4
+ const config = require('./config');
5
+ const spec = require('./spec');
6
+ const glob = require('./glob');
7
+ function diagnostic(error, source) {
8
+ return { code: error.code || 'read_error', source, message: error.message };
9
+ }
10
+ function requests(settings, task, paths) {
11
+ const result = settings.always.map(ref => ({ ref, priority: 0, source: 'always' }));
12
+ for (const ref of task?.specRefs ?? []) result.push({ ref, priority: ref.includes('#') ? 1 : 2, source: 'task.spec_refs' });
13
+ for (const file of paths) {
14
+ for (const [pattern, refs] of Object.entries(settings.paths)) {
15
+ if (glob.matches(pattern, file)) {
16
+ for (const ref of refs) result.push({ ref, priority: 3, source: 'path:' + file });
17
+ }
18
+ }
19
+ }
20
+ return result.sort((a, b) => a.priority - b.priority);
21
+ }
22
+ function select(root, entries, diagnostics) {
23
+ const loaded = new Map(), selected = new Map();
24
+ for (const entry of entries) {
25
+ try {
26
+ const ref = config.reference(entry.ref);
27
+ if (!loaded.has(ref.id)) loaded.set(ref.id, spec.load(root, ref.id));
28
+ const document = loaded.get(ref.id);
29
+ const rules = ref.rule ? document.rules.filter(rule => rule.id === ref.rule) : document.rules;
30
+ f.demand(!ref.rule || rules.length, 'rule_missing', 'Missing Rule: ' + entry.ref);
31
+ for (const rule of rules) {
32
+ const key = ref.id + '#' + rule.id;
33
+ if (!selected.has(key)) selected.set(key, { ref: key, spec: ref.id, rule: rule.id,
34
+ text: rule.text, source: '.code-flow/specs/' + ref.id + '.md', selectedBy: [] });
35
+ const item = selected.get(key);
36
+ if (!item.selectedBy.includes(entry.source)) item.selectedBy.push(entry.source);
37
+ }
38
+ } catch (error) { diagnostics.push(diagnostic(error, entry.ref)); }
39
+ }
40
+ return [...selected.values()];
41
+ }
42
+ function estimate(text) {
43
+ const ascii = (text.match(/[\x00-\x7f]/g) || []).length;
44
+ return Math.ceil(ascii / 4) + [...text].length - ascii;
45
+ }
46
+ function ruleText(rule, previous) {
47
+ const header = !previous || previous.spec !== rule.spec || previous.source !== rule.source ||
48
+ previous.selectedBy.join(', ') !== rule.selectedBy.join(', ')
49
+ ? '### ' + rule.spec + '\nsource: ' + rule.source +
50
+ '\nselected-by: ' + rule.selectedBy.join(', ') + '\n'
51
+ : '';
52
+ return header + '\n- [' + rule.rule + '] ' + rule.text + '\n';
53
+ }
54
+ function fits(text, budget) {
55
+ return estimate(text) <= budget.maxTokens && Buffer.byteLength(text, 'utf8') <= budget.maxBytes;
56
+ }
57
+ function render(selected, settings, context) {
58
+ const heading = '## Spec Constraints (latest)\n\n' +
59
+ (context.session ? 'session: ' + context.session + '\n' : '') +
60
+ (context.task ? 'task: ' + context.task + '\n' : '') +
61
+ 'workflow: ' + settings.workflow + '\n';
62
+ const readMore = '\nread-more: code-flow spec inject' +
63
+ (context.task ? ' --task-file "' + context.taskFile + '" --task ' + context.taskId : '') + ' --full\n';
64
+ const reserve = '\nomitted: ' + selected.slice(0, 3).map(item => item.ref).join(', ') + readMore;
65
+ const emitted = [], omitted = [];
66
+ let text = heading, last = null;
67
+ for (const item of selected) {
68
+ const addition = '\n' + ruleText(item, last);
69
+ if (context.full || fits(text + addition + reserve, settings.budget)) {
70
+ text += addition; emitted.push(item.ref); last = item;
71
+ } else omitted.push(item);
72
+ }
73
+ if (omitted.length) text += '\nomitted: ' + omitted.slice(0, 3).map(item => item.ref).join(', ') +
74
+ (omitted.length > 3 ? ' (+' + (omitted.length - 3) + ')' : '') + readMore;
75
+ if (!context.full && !fits(text, settings.budget)) {
76
+ text = 'Spec budget too small. Run code-flow spec inject --full.\n';
77
+ }
78
+ return { text, emitted, omitted: omitted.map(item => ({ ref: item.ref, source: item.source })) };
79
+ }
80
+ function normalizePaths(root, paths) {
81
+ return [...new Set(paths.map(file => {
82
+ const relative = path.isAbsolute(file) ? path.relative(root, file) : file;
83
+ f.inside(root, relative);
84
+ return relative.replace(/\\/g, '/').replace(/^\.\//, '');
85
+ }))];
86
+ }
87
+ function inject(root, options = {}) {
88
+ const settings = config.load(root), diagnostics = [];
89
+ let task = null;
90
+ if (options.taskFile) {
91
+ try { task = require('./task').loadTask(root, options.taskFile, options.taskId); } catch (error) {
92
+ if (options.strictTask) throw error;
93
+ diagnostics.push(diagnostic(error, options.taskFile));
94
+ }
95
+ }
96
+ const paths = [];
97
+ for (const file of options.paths ?? []) {
98
+ try { paths.push(...normalizePaths(root, [file])); } catch (error) { diagnostics.push(diagnostic(error, file)); }
99
+ }
100
+ const entries = requests(settings, task, paths), selected = select(root, entries, diagnostics);
101
+ let catalog = [];
102
+ if (!task && entries.length === settings.always.length) {
103
+ const listing = spec.list(root, false); catalog = listing.items; diagnostics.push(...listing.diagnostics);
104
+ }
105
+ const identity = task ? task.relative + '#' + task.id : null;
106
+ const result = render(selected, settings, { ...options, task: identity,
107
+ taskFile: task?.relative, taskId: task?.id });
108
+ if (catalog.length) {
109
+ const navigation = '\n## Spec Catalog\n' + catalog.slice(0, 20).map(item => item.id + ': ' + item.description).join('\n') +
110
+ '\n' + catalog.length + ' Specs; code-flow spec list --json\n';
111
+ if (options.full || fits(result.text + navigation, settings.budget)) result.text += navigation;
112
+ }
113
+ return { ...result, selected, diagnostics, catalog, task: identity, taskFile: task?.relative ?? null, taskId: task?.id ?? null };
114
+ }
115
+ module.exports = { inject, select, requests, render, estimate, normalizePaths };
package/lib/repo.js ADDED
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { spawnSync } = require('node:child_process');
5
+ const f = require('./fs');
6
+
7
+ function run(root, args) {
8
+ const result = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 1024 * 1024 });
9
+ if (result.error) {
10
+ throw new f.FlowError('git_identity', 'Cannot read Git identity: ' + result.error.message, 2);
11
+ }
12
+ f.demand(result.status === 0, 'git_identity',
13
+ 'Cannot read Git identity: ' + (result.stderr || 'git exited ' + result.status).trim(), 2);
14
+ return result.stdout.trim();
15
+ }
16
+
17
+ function identity(root) {
18
+ const real = f.realpath(root);
19
+ if (!fs.existsSync(path.join(real, '.git'))) return { git: false, root: real, linked: false };
20
+ const gitDir = path.resolve(real, run(real, ['rev-parse', '--git-dir']));
21
+ const commonDir = path.resolve(real, run(real, ['rev-parse', '--git-common-dir']));
22
+ const top = f.realpath(run(real, ['rev-parse', '--show-toplevel']));
23
+ const linked = f.realpath(gitDir) !== f.realpath(commonDir);
24
+ const branchResult = spawnSync('git', ['symbolic-ref', '-q', 'HEAD'], { cwd: real, encoding: 'utf8' });
25
+ return { git: true, root: real, top, git_dir: f.realpath(gitDir),
26
+ git_common_dir: f.realpath(commonDir), branch: branchResult.status === 0 ? branchResult.stdout.trim() : null,
27
+ linked };
28
+ }
29
+
30
+ function state(root) {
31
+ const real = f.realpath(root);
32
+ if (!fs.existsSync(path.join(real, '.git'))) return { head: null, dirty: null };
33
+ const head = spawnSync('git', ['rev-parse', 'HEAD'], { cwd: real, encoding: 'utf8' });
34
+ const status = spawnSync('git', ['status', '--porcelain', '--untracked-files=all'],
35
+ { cwd: real, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
36
+ const dirty = status.status !== 0 ? null : status.stdout.split('\n').filter(Boolean)
37
+ .some(line => !line.slice(3).trim().replace(/^"|"$/g, '').startsWith('.code-flow/'));
38
+ return { head: head.status === 0 ? head.stdout.trim() : null, dirty };
39
+ }
40
+
41
+ function requireCoordinator(root, operation) {
42
+ const current = identity(root);
43
+ f.demand(!current.linked, 'coordinator_required',
44
+ operation + ' must run in the coordinator worktree: ' + current.top, 1);
45
+ return current;
46
+ }
47
+
48
+ module.exports = { identity, requireCoordinator, state };
package/lib/runs.js ADDED
@@ -0,0 +1,155 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const { spawnSync } = require('node:child_process');
5
+ const f = require('./fs');
6
+ const repo = require('./repo');
7
+
8
+ const UNFINISHED = new Set(['preparing', 'running', 'merging', 'verifying', 'cleaning', 'blocked']);
9
+ const STATUSES = new Set([...UNFINISHED, 'done', 'aborted']);
10
+ const WORKTREES = '.code-flow/worktrees';
11
+
12
+ function validateLedger(ledger, runId) {
13
+ const errors = [];
14
+ if (!ledger || typeof ledger !== 'object' || Array.isArray(ledger)) return ['Run ledger must be an object'];
15
+ if (typeof ledger.run_id !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(ledger.run_id)) {
16
+ errors.push('Invalid run identity');
17
+ } else if (runId !== undefined && ledger.run_id !== runId) {
18
+ errors.push('Run ledger identity does not match: ' + runId);
19
+ }
20
+ if (!STATUSES.has(ledger.status)) errors.push('Invalid run status: ' + String(ledger.status));
21
+ if (!ledger.coordinator || typeof ledger.coordinator !== 'object' || Array.isArray(ledger.coordinator) ||
22
+ typeof ledger.coordinator.root !== 'string' || typeof ledger.coordinator.branch !== 'string' ||
23
+ typeof ledger.coordinator.git_common_dir !== 'string') {
24
+ errors.push('Run ledger has no coordinator identity');
25
+ }
26
+ if (!Array.isArray(ledger.worktrees)) errors.push('Run ledger has no worktree list');
27
+ if (!ledger.merge || typeof ledger.merge !== 'object' || !Array.isArray(ledger.merge.order)) {
28
+ errors.push('Run ledger has no merge order');
29
+ }
30
+ if (errors.length) return errors;
31
+ for (const worker of ledger.worktrees) {
32
+ if (!worker || typeof worker !== 'object' || !/^TASK-\d+$/.test(worker.task) ||
33
+ worker.path !== WORKTREES + '/' + ledger.run_id + '/' + worker.task ||
34
+ worker.branch !== 'cf-task/' + ledger.run_id + '/' + worker.task) {
35
+ errors.push('Run ledger owns an invalid worker resource: ' + String(worker?.task));
36
+ }
37
+ }
38
+ if (errors.length) return errors;
39
+ const tasks = ledger.worktrees.map(worker => worker.task).sort();
40
+ if (JSON.stringify(tasks) !== JSON.stringify([...ledger.merge.order].sort())) {
41
+ errors.push('Run ledger merge order does not match worktrees');
42
+ }
43
+ return errors;
44
+ }
45
+
46
+ function list(root) {
47
+ const base = path.join(root, WORKTREES);
48
+ if (!fs.existsSync(base)) return [];
49
+ const runs = [];
50
+ for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
51
+ if (!entry.isDirectory()) continue;
52
+ const file = path.join(base, entry.name, 'run.json');
53
+ if (!fs.existsSync(file)) continue;
54
+ try {
55
+ const ledger = JSON.parse(fs.readFileSync(file, 'utf8'));
56
+ const taskFile = typeof ledger?.task_file === 'string'
57
+ ? ledger.task_file.replace(/\\/g, '/').replace(/^\.\//, '') : null;
58
+ const errors = validateLedger(ledger, entry.name);
59
+ runs.push(errors.length
60
+ ? { run_id: entry.name, status: 'invalid', task_file: null,
61
+ file: path.join(WORKTREES, entry.name, 'run.json'), error: errors[0] }
62
+ : { run_id: ledger.run_id, status: ledger.status,
63
+ task_file: taskFile, file: path.join(WORKTREES, entry.name, 'run.json'), ledger });
64
+ } catch (error) {
65
+ runs.push({ run_id: entry.name, status: 'invalid', task_file: null,
66
+ file: path.join(WORKTREES, entry.name, 'run.json'), error: error.message });
67
+ }
68
+ }
69
+ return runs;
70
+ }
71
+
72
+ function unfinished(root, filter = () => true) {
73
+ return list(root).filter(run => (UNFINISHED.has(run.status) || run.status === 'invalid') && filter(run));
74
+ }
75
+
76
+ function assertArchiveAllowed(root, requirementDir) {
77
+ const normalized = requirementDir.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '');
78
+ const blocked = unfinished(root, run => !run.task_file ||
79
+ run.task_file === normalized || run.task_file.startsWith(normalized + '/'));
80
+ f.demand(!blocked.length, 'run_incomplete',
81
+ 'Unfinished worktree run for requirement: ' + blocked.map(run => run.run_id).join(', '), 1);
82
+ }
83
+
84
+ function assertLearnAllowed(root) {
85
+ const blocked = unfinished(root);
86
+ f.demand(!blocked.length, 'run_incomplete',
87
+ 'Unfinished worktree run before Spec learning: ' + blocked.map(run => run.run_id).join(', '), 1);
88
+ }
89
+
90
+ function readWorkerLedger(file, name, owned) {
91
+ if (!fs.existsSync(file)) {
92
+ if (owned) throw new f.FlowError('run_invalid', 'Missing run ledger: ' + name, 2);
93
+ return null;
94
+ }
95
+ try {
96
+ const ledger = JSON.parse(fs.readFileSync(file, 'utf8'));
97
+ if (!ledger || !Array.isArray(ledger.worktrees)) throw new Error('missing worktrees');
98
+ return ledger;
99
+ } catch (error) {
100
+ if (owned) throw new f.FlowError('run_invalid', 'Invalid run ledger: ' + error.message, 2);
101
+ return null;
102
+ }
103
+ }
104
+
105
+ function ledgerWorkerEnv(ledger, name, current, coordinator) {
106
+ for (const worker of ledger.worktrees) {
107
+ if (!worker.env || typeof worker.path !== 'string') continue;
108
+ if (worker.path !== WORKTREES + '/' + name + '/' + worker.task) continue;
109
+ let workerRoot;
110
+ try { workerRoot = f.realpath(path.join(coordinator, worker.path)); } catch (_) { continue; }
111
+ if (workerRoot !== current) continue;
112
+ const env = worker.env;
113
+ f.demand(env && typeof env.CF_WORKTREE_ID === 'string' &&
114
+ typeof env.CF_TASK_ID === 'string' && typeof env.CF_RUN_ID === 'string' &&
115
+ env.CF_WORKTREE_ID === name + '/' + worker.task &&
116
+ env.CF_TASK_ID === worker.task && env.CF_RUN_ID === name,
117
+ 'run_invalid', 'Invalid worker environment in run ledger', 2);
118
+ return { ...env };
119
+ }
120
+ return null;
121
+ }
122
+
123
+ function workerEnv(root) {
124
+ const current = f.realpath(root);
125
+ const identity = repo.identity(current);
126
+ if (!identity.git || !identity.linked) return {};
127
+ const result = spawnSync('git', ['worktree', 'list', '--porcelain'],
128
+ { cwd: current, encoding: 'utf8', maxBuffer: 1024 * 1024 });
129
+ f.demand(result.status === 0, 'git_identity',
130
+ 'Cannot read Git worktree registration: ' + (result.stderr || '').trim(), 2);
131
+ const main = result.stdout.split('\n').find(line => line.startsWith('worktree '));
132
+ f.demand(main, 'git_identity', 'Cannot locate coordinator worktree', 2);
133
+ const coordinator = f.realpath(main.slice(9));
134
+ const base = path.join(coordinator, WORKTREES);
135
+ if (!fs.existsSync(base)) return {};
136
+ const relative = path.relative(coordinator, current).split(path.sep).join('/');
137
+ let found = null;
138
+ for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
139
+ if (!entry.isDirectory()) continue;
140
+ const owned = relative.startsWith(WORKTREES + '/' + entry.name + '/');
141
+ const ledger = readWorkerLedger(path.join(base, entry.name, 'run.json'), entry.name, owned);
142
+ if (!ledger) continue;
143
+ const env = ledgerWorkerEnv(ledger, entry.name, current, coordinator);
144
+ if (!env) {
145
+ if (owned) throw new f.FlowError('run_invalid',
146
+ 'Run ledger does not own this worker: ' + entry.name, 2);
147
+ continue;
148
+ }
149
+ f.demand(!found, 'env_conflict', 'Worktree is claimed by multiple run ledgers', 1);
150
+ found = env;
151
+ }
152
+ return found ?? {};
153
+ }
154
+
155
+ module.exports = { list, unfinished, assertArchiveAllowed, assertLearnAllowed, workerEnv, validateLedger, UNFINISHED };
package/lib/session.js ADDED
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+ const path = require('node:path');
3
+ const fs = require('node:fs');
4
+ const f = require('./fs');
5
+ function token(root, platform, sessionId) {
6
+ f.string(sessionId, 'session ID');
7
+ return f.hash(JSON.stringify([f.realpath(root), platform, sessionId]));
8
+ }
9
+ function location(root, value) {
10
+ f.demand(typeof value === 'string' && /^[a-f0-9]{64}$/.test(value),
11
+ 'session_invalid', 'Session token must be 64 lowercase hexadecimal characters');
12
+ return path.join(f.transientRoot(root), 'sessions', value + '.json');
13
+ }
14
+ function read(root, value) {
15
+ const raw = f.readText(location(root, value), 65536);
16
+ if (raw === null) return null;
17
+ let result;
18
+ try { result = JSON.parse(raw); } catch (error) {
19
+ throw new f.FlowError('session_invalid', 'Invalid session binding: ' + error.message);
20
+ }
21
+ f.object(result, 'session binding');
22
+ f.string(result.task_file, 'session task_file');
23
+ f.string(result.task_id, 'session task_id');
24
+ return result;
25
+ }
26
+ async function bind(root, value, taskFile, taskId) {
27
+ const file = location(root, value);
28
+ f.string(taskFile, 'task file');
29
+ f.string(taskId, 'task id');
30
+ await f.withLock(root, 'session:' + value, async () => {
31
+ f.atomicWrite(file, JSON.stringify({ task_file: taskFile, task_id: taskId,
32
+ bound_at: new Date().toISOString() }) + '\n', f.readText(file));
33
+ });
34
+ }
35
+ async function unbind(root, value) {
36
+ const file = location(root, value);
37
+ await f.withLock(root, 'session:' + value, async () => f.removeIfPresent(file));
38
+ return { ok: true, action: 'unbind' };
39
+ }
40
+ module.exports = { token, location, read, bind, unbind };
package/lib/spec.js ADDED
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const f = require('./fs');
5
+ const fm = require('./frontmatter');
6
+ const { reference } = require('./config');
7
+ const markdown = require('./markdown');
8
+
9
+ function parseRules(body) {
10
+ const lines = body.replace(/\r\n/g, '\n').split('\n');
11
+ const rules = [], used = new Set();
12
+ let section = false, fence = null, current = null;
13
+ const finish = end => {
14
+ if (!current) return;
15
+ current.end = end;
16
+ current.text = lines.slice(current.start, end).map((line, i) =>
17
+ i === 0 ? line.replace(/^- \[RULE-[^\]]+\] /, '') : line.replace(/^ /, '')).join('\n').trim();
18
+ f.demand(!used.has(current.id), 'duplicate_rule', 'Duplicate Rule ID: ' + current.id);
19
+ used.add(current.id);
20
+ rules.push(current); current = null;
21
+ };
22
+ for (let i = 0; i < lines.length; i++) {
23
+ const line = lines[i], marker = markdown.fenceMarker(line, fence);
24
+ if (marker) {
25
+ fence = marker.next;
26
+ continue;
27
+ }
28
+ if (fence) continue;
29
+ if (/^##\s/.test(line)) { finish(i); section = /^## Rules\s*$/.test(line); continue; }
30
+ if (!section) continue;
31
+ const match = line.match(/^- \[(RULE-[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)\] (.+)$/);
32
+ if (match) { finish(i); current = { id: match[1], start: i }; }
33
+ else if (/^- /.test(line)) {
34
+ finish(i);
35
+ throw new f.FlowError('rule_invalid', 'Rule must have a stable RULE ID: ' + line);
36
+ }
37
+ }
38
+ f.demand(!fence, 'rule_fence', 'Unclosed code fence in Spec');
39
+ finish(lines.length);
40
+ return { rules, lines };
41
+ }
42
+ function header(meta) {
43
+ f.string(meta.description, 'Spec description');
44
+ f.positive(meta.next_rule, 'Spec next_rule', true);
45
+ return meta;
46
+ }
47
+ function load(root, id) {
48
+ f.demand(!id.includes('#'), 'invalid_reference', 'Expected Spec path without Rule suffix');
49
+ reference(id);
50
+ const file = f.inside(root, '.code-flow/specs/' + id + '.md');
51
+ const raw = f.readText(file);
52
+ f.demand(raw !== null, 'spec_missing', 'Missing Spec: ' + id);
53
+ const document = fm.parse(raw);
54
+ header(document.meta);
55
+ return { ...document, ...parseRules(document.body), id, file };
56
+ }
57
+ function walk(directory, prefix = '') {
58
+ if (!fs.existsSync(directory)) return [];
59
+ return fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))
60
+ .flatMap(entry => {
61
+ const relative = prefix + entry.name;
62
+ if (entry.isSymbolicLink()) return [];
63
+ if (entry.isDirectory()) return walk(path.join(directory, entry.name), relative + '/');
64
+ return entry.isFile() && entry.name.endsWith('.md') ? [relative] : [];
65
+ });
66
+ }
67
+ function list(root, full = true) {
68
+ const items = [], diagnostics = [];
69
+ for (const relative of walk(f.inside(root, '.code-flow/specs'))) {
70
+ const id = relative.slice(0, -3);
71
+ try {
72
+ reference(id);
73
+ const info = full ? load(root, id) : { meta: header(fm.readHeader(f.inside(root, '.code-flow/specs/' + relative))) };
74
+ items.push({ id, path: '.code-flow/specs/' + relative, description: info.meta.description,
75
+ ...(full ? { rules: info.rules.length } : {}) });
76
+ } catch (error) { diagnostics.push({ code: error.code || 'spec_error', source: id, message: error.message }); }
77
+ }
78
+ return { items, diagnostics };
79
+ }
80
+ module.exports = { load, list, parseRules, walk, header };