@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/hook.js ADDED
@@ -0,0 +1,99 @@
1
+ 'use strict';
2
+ const path = require('node:path');
3
+ const f = require('./fs');
4
+ const config = require('./config');
5
+ const session = require('./session');
6
+ const task = require('./task');
7
+ const render = require('./render');
8
+ const markdown = require('./markdown');
9
+ function cleanPrompt(text) {
10
+ let fence = null;
11
+ return text.replace(/<(task-notification|output-file)\b[^>]*>[\s\S]*?(?:<\/\1>|$)/gi, '')
12
+ .split('\n').filter(line => {
13
+ const marker = markdown.fenceMarker(line, fence);
14
+ if (marker) {
15
+ fence = marker.next;
16
+ return false;
17
+ }
18
+ return !fence && !/^\s*>/.test(line);
19
+ }).join('\n').replace(/\b[a-z]+:\/\/[^\s<>]+/gi, '');
20
+ }
21
+ function paths(prompt, root) {
22
+ const text = prompt.replace(/^task:[^\n]*$/gm, '');
23
+ const found = [];
24
+ const pattern = /(?:@)?(?:"([^"\r\n]+)"|'([^'\r\n]+)'|`([^`\r\n]+)`|([A-Za-z0-9_.@/\\:-]+))/g;
25
+ for (const match of text.matchAll(pattern)) {
26
+ const value = (match[1] || match[2] || match[3] || match[4]).replace(/^@/, '').replace(/[.,;:]+$/, '');
27
+ if (!/[\\/]/.test(value) && !/\.[A-Za-z][A-Za-z0-9]{0,9}$/.test(value)) continue;
28
+ try { found.push(...render.normalizePaths(root, [value])); } catch (error) {
29
+ if (error.code !== 'invalid_path') throw error;
30
+ }
31
+ }
32
+ return [...new Set(found)];
33
+ }
34
+ function nativeEvent(data, format) {
35
+ f.object(data, 'Hook input');
36
+ if (format === 'opencode') {
37
+ f.object(data.event, 'OpenCode event');
38
+ return { cwd: data.directory, sessionId: data.event.sessionID, prompt: data.event.prompt?.text };
39
+ }
40
+ f.demand(['claude', 'codex', 'costrict'].includes(format), 'hook_format', 'Unknown Hook format');
41
+ return { cwd: data.cwd, sessionId: data.session_id, prompt: data.prompt };
42
+ }
43
+ function parseTaskRef(value) {
44
+ const match = /^(.+\.md)#(TASK-\d+)$/.exec(String(value).trim());
45
+ f.demand(match, 'task_reference', 'Use task: <task-document>.md#TASK-NNN');
46
+ return { taskFile: match[1], taskId: match[2] };
47
+ }
48
+
49
+ function choose(root, prompt, token, diagnostics) {
50
+ const explicit = [...prompt.matchAll(/^task:[ \t]*(.+?)[ \t]*$/gm)].map(match => match[1]);
51
+ if (explicit.length) {
52
+ if (explicit.length !== 1) diagnostics.push({ code: 'task_ambiguous', message: 'Use exactly one task: <task-document>.md#TASK-NNN' });
53
+ else {
54
+ try {
55
+ const ref = parseTaskRef(explicit[0]);
56
+ const selected = task.loadTask(root, ref.taskFile, ref.taskId);
57
+ return { taskFile: selected.relative, taskId: selected.id };
58
+ } catch (error) { diagnostics.push({ code: error.code, message: error.message }); }
59
+ }
60
+ return null;
61
+ }
62
+ if (!token) return null;
63
+ try {
64
+ const binding = session.read(root, token);
65
+ if (!binding) return null;
66
+ const selected = task.loadTask(root, binding.task_file, binding.task_id);
67
+ f.demand(selected.status === 'in-progress', 'binding_stale', 'Bound Task is no longer in-progress');
68
+ return { taskFile: selected.relative, taskId: selected.id };
69
+ } catch (error) { diagnostics.push({ code: error.code || 'binding_invalid', message: error.message }); return null; }
70
+ }
71
+ function payload(format, text) {
72
+ return format === 'opencode' ? { context: text } :
73
+ { hookSpecificOutput: { hookEventName: 'UserPromptSubmit', additionalContext: text } };
74
+ }
75
+ function build(data, format) {
76
+ const diagnostics = [];
77
+ try {
78
+ const event = nativeEvent(data, format);
79
+ f.demand(typeof event.prompt === 'string', 'hook_prompt', 'prompt must be a string');
80
+ if (!event.prompt.trim()) return { payload: null, diagnostics };
81
+ f.string(event.cwd, 'Hook cwd');
82
+ f.demand(path.isAbsolute(event.cwd), 'hook_cwd', 'Hook cwd must be absolute');
83
+ const root = config.findRoot(event.cwd, false);
84
+ if (!root) return { payload: null, diagnostics };
85
+ config.load(root);
86
+ const prompt = cleanPrompt(event.prompt);
87
+ const token = typeof event.sessionId === 'string' && event.sessionId ?
88
+ session.token(root, format, event.sessionId) : null;
89
+ if (!token) diagnostics.push({ code: 'session_missing', message: 'No session ID; explicit paths remain available' });
90
+ const selected = choose(root, prompt, token, diagnostics);
91
+ const result = render.inject(root, { taskFile: selected?.taskFile, taskId: selected?.taskId,
92
+ paths: paths(prompt, root), session: token });
93
+ return { ...result, payload: payload(format, result.text), diagnostics: [...diagnostics, ...result.diagnostics] };
94
+ } catch (error) {
95
+ diagnostics.push({ code: error.code || 'hook_error', message: error.message });
96
+ return { payload: payload(format, ''), diagnostics };
97
+ }
98
+ }
99
+ module.exports = { build, payload, paths, cleanPrompt, choose, parseTaskRef, nativeEvent };
package/lib/init.js ADDED
@@ -0,0 +1,132 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const f = require('./fs');
5
+ const config = require('./config');
6
+ const adapters = require('./adapters');
7
+ const templateRoot = path.join(__dirname, '../templates');
8
+ const SKILLS = [
9
+ { name: 'cf-init', path: 'cf-init' },
10
+ { name: 'cf-check', path: 'cf-check' },
11
+ { name: 'cf-spec', path: 'cf-spec' },
12
+ { name: 'cf-task-prd', path: 'cf-task/prd' },
13
+ { name: 'cf-task-align', path: 'cf-task/align' },
14
+ { name: 'cf-task-plan', path: 'cf-task/plan' },
15
+ { name: 'cf-task-start', path: 'cf-task/start' },
16
+ { name: 'cf-task-archive', path: 'cf-task/archive' }
17
+ ];
18
+ const WORKFLOW_TEMPLATES = ['prd.md', 'design-lite.md', 'design-full.md',
19
+ 'design-frontend.md', 'scenarios.md', 'task.md', 'project-guidelines.md'];
20
+ const LAUNCHER_TEMPLATES = ['bridge.cjs', 'hook.sh', 'hook.cmd', 'hook.ps1'];
21
+
22
+ function launcherContent(name, node, entry) {
23
+ const template = fs.readFileSync(path.join(templateRoot, 'launcher', name), 'utf8');
24
+ const sh = value => value.replace(/'/g, "'\\''");
25
+ const cmd = value => value.replace(/%/g, '%%');
26
+ const ps = value => value.replace(/'/g, "''");
27
+ if (name === 'hook.sh') return template.replaceAll('__FIXED_NODE__', sh(node))
28
+ .replaceAll('__FIXED_ENTRY__', sh(entry));
29
+ if (name === 'hook.cmd') return template.replaceAll('__FIXED_NODE__', cmd(node))
30
+ .replaceAll('__FIXED_ENTRY__', cmd(entry));
31
+ if (name === 'hook.ps1') return template.replaceAll('__FIXED_NODE__', ps(node))
32
+ .replaceAll('__FIXED_ENTRY__', ps(entry));
33
+ return template;
34
+ }
35
+ function installed(root) {
36
+ return Object.entries(adapters.PLATFORMS).filter(([name, value]) => {
37
+ const text = f.readText(f.inside(root, value.hooks));
38
+ if (text === null) return false;
39
+ if (name === 'opencode') return adapters.owned(text);
40
+ const data = f.object(JSON.parse(text), 'platform config');
41
+ return Object.values(data.hooks ?? {}).some(groups => Array.isArray(groups) &&
42
+ groups.some(group => Array.isArray(group.hooks) && group.hooks.some(adapters.managed)));
43
+ }).map(([name]) => name);
44
+ }
45
+ function add(plans, conflicts, root, relative, after, managed = false) {
46
+ const file = f.inside(root, relative), before = f.readText(file);
47
+ if (managed && before !== null && !adapters.owned(before)) {
48
+ conflicts.push({ path: relative, code: 'file_unmanaged',
49
+ message: 'Preserve/remove unmanaged file before init' });
50
+ return;
51
+ }
52
+ if (before !== after) plans.set(relative, { file, path: relative, before, after });
53
+ }
54
+ function attempt(conflicts, relative, operation) {
55
+ try { operation(); } catch (error) {
56
+ conflicts.push({ path: relative, code: error.code || 'init_file_failed', message: error.message });
57
+ }
58
+ }
59
+ function platformPlan(plans, conflicts, root, platform, node, entry) {
60
+ const target = adapters.PLATFORMS[platform];
61
+ const hookFile = f.inside(root, target.hooks);
62
+ const before = f.readText(hookFile);
63
+ if (platform === 'opencode') {
64
+ add(plans, conflicts, root, target.hooks, adapters.plugin(node, entry), true);
65
+ add(plans, conflicts, root, '.opencode/plugins/code-flow/package.json',
66
+ JSON.stringify({ private: true, type: 'module', managed: adapters.MARKER }, null, 2) + '\n', true);
67
+ }
68
+ else attempt(conflicts, target.hooks, () => add(plans, conflicts, root, target.hooks,
69
+ adapters.mergeHooks(before, platform, node, entry)));
70
+ const instruction = fs.readFileSync(path.join(templateRoot, 'instructions.md'), 'utf8');
71
+ attempt(conflicts, target.instructions, () => add(plans, conflicts, root, target.instructions,
72
+ adapters.instructions(f.readText(f.inside(root, target.instructions)), instruction)));
73
+ for (const skill of SKILLS) {
74
+ const name = platform === 'codex' ? skill.name : skill.path;
75
+ const relative = target.skills + '/' + name + (platform === 'codex' ? '/SKILL.md' : '.md');
76
+ add(plans, conflicts, root, relative,
77
+ fs.readFileSync(path.join(templateRoot, 'skills', skill.name + '.md'), 'utf8'), true);
78
+ }
79
+ if (platform === 'codex') {
80
+ const relative = '.codex/config.toml';
81
+ attempt(conflicts, relative, () => add(plans, conflicts, root, relative,
82
+ adapters.codexConfig(f.readText(f.inside(root, relative)))));
83
+ }
84
+ }
85
+ function plan(root, options = {}) {
86
+ const dataDir = f.inside(root, '.code-flow'), configuration = f.inside(root, '.code-flow/config.json');
87
+ const before = f.readText(configuration, 65536);
88
+ f.demand(before !== null || !fs.existsSync(dataDir) || fs.readdirSync(dataDir).length === 0,
89
+ 'directory_format', 'Nonempty .code-flow without v1 config; preserve it outside the project before init');
90
+ if (before !== null) config.load(root);
91
+ const platforms = installed(root);
92
+ if (options.platform === 'all') {
93
+ for (const name of Object.keys(adapters.PLATFORMS)) if (!platforms.includes(name)) platforms.push(name);
94
+ } else if (options.platform) {
95
+ f.demand(Object.hasOwn(adapters.PLATFORMS, options.platform), 'platform_invalid', 'Unknown platform: ' + options.platform);
96
+ if (!platforms.includes(options.platform)) platforms.push(options.platform);
97
+ }
98
+ f.demand(platforms.length > 0, 'platform_required', 'First init requires --platform');
99
+ const plans = new Map(), conflicts = [];
100
+ if (before === null) add(plans, conflicts, root, '.code-flow/config.json',
101
+ fs.readFileSync(path.join(templateRoot, 'config.json'), 'utf8'));
102
+ for (const name of WORKFLOW_TEMPLATES) {
103
+ add(plans, conflicts, root, '.code-flow/templates/' + name,
104
+ fs.readFileSync(path.join(templateRoot, 'workflow', name), 'utf8'));
105
+ }
106
+ const node = options.node ?? process.execPath, entry = options.entry ?? path.resolve(__dirname, '../bin/code-flow.js');
107
+ for (const name of LAUNCHER_TEMPLATES) {
108
+ add(plans, conflicts, root, '.code-flow/launcher/' + name, launcherContent(name, node, entry));
109
+ }
110
+ for (const platform of platforms.sort()) platformPlan(plans, conflicts, root, platform, node, entry);
111
+ return { plans: [...plans.values()], platforms, conflicts };
112
+ }
113
+ async function init(root, options = {}) {
114
+ const build = () => plan(root, options);
115
+ const execute = prepared => {
116
+ if (!options.dryRun) {
117
+ for (const item of prepared.plans) f.atomicWrite(item.file, item.after, item.before);
118
+ for (const directory of ['.code-flow/specs', '.code-flow/tasks',
119
+ '.code-flow/templates', '.code-flow/launcher'])
120
+ fs.mkdirSync(f.inside(root, directory), { recursive: true });
121
+ }
122
+ return { ok: prepared.conflicts.length === 0, action: 'init', dry_run: Boolean(options.dryRun),
123
+ platforms: prepared.platforms, conflicts: prepared.conflicts,
124
+ changes: prepared.plans.map(item => ({ path: item.path,
125
+ action: item.before === null ? 'create' : 'update' })),
126
+ next: prepared.conflicts.length ? 'Resolve reported file conflicts, then re-run init.' :
127
+ prepared.platforms.includes('codex') ? 'Review/trust changed hooks in Codex /hooks.' : 'Reload platform configuration.' };
128
+ };
129
+ if (options.dryRun) return execute(build());
130
+ return f.withLock(root, 'write', async () => execute(build()));
131
+ }
132
+ module.exports = { init, plan, installed, WORKFLOW_TEMPLATES, LAUNCHER_TEMPLATES };
package/lib/learn.js ADDED
@@ -0,0 +1,170 @@
1
+ 'use strict';
2
+ const path = require('node:path');
3
+ const f = require('./fs');
4
+ const fm = require('./frontmatter');
5
+ const spec = require('./spec');
6
+ const { reference } = require('./config');
7
+ const markdown = require('./markdown');
8
+ function normalize(text) { return text.replace(/\r\n/g, '\n').trim(); }
9
+ function validate(delta) {
10
+ f.object(delta, 'Delta');
11
+ f.demand(typeof delta.id === 'string' && /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(delta.id), 'delta_id', 'Invalid Delta ID');
12
+ f.demand(['add', 'update', 'remove'].includes(delta.op), 'delta_op', 'Invalid Delta operation');
13
+ const target = reference(delta.target);
14
+ f.demand(!target.rule, 'delta_target', 'Delta target must be a Spec path');
15
+ const result = { id: delta.id, op: delta.op, target: target.id,
16
+ evidence: normalize(f.string(delta.evidence, 'Delta evidence')) };
17
+ if (delta.op !== 'remove') result.rule = normalize(f.string(delta.rule, 'Delta rule'));
18
+ if (delta.op !== 'add') {
19
+ f.string(delta.ref, 'Delta ref');
20
+ f.demand(/^RULE-[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/.test(delta.ref), 'delta_ref', 'Invalid Delta Rule reference');
21
+ result.ref = delta.ref; result.before = normalize(f.string(delta.before, 'Delta before'));
22
+ }
23
+ return result;
24
+ }
25
+ function deltas(task) {
26
+ const source = task.specDelta ?? [];
27
+ f.demand(Array.isArray(source), 'delta_invalid', 'Spec Delta must be an array');
28
+ const ids = new Set();
29
+ return source.map(item => {
30
+ const delta = validate(item);
31
+ f.demand(!ids.has(delta.id), 'delta_duplicate', 'Duplicate Delta ID: ' + delta.id); ids.add(delta.id);
32
+ return delta;
33
+ });
34
+ }
35
+ function section(body, name) {
36
+ const lines = body.replace(/\r\n/g, '\n').split('\n');
37
+ let start = -1, fence = null;
38
+ for (let i = 0; i < lines.length; i++) {
39
+ const marker = markdown.fenceMarker(lines[i], fence);
40
+ if (marker) {
41
+ fence = marker.next;
42
+ continue;
43
+ }
44
+ if (fence) continue;
45
+ if (lines[i] === '## ' + name && start === -1) { start = i; continue; }
46
+ if (start !== -1 && /^## /.test(lines[i])) return { lines, start, end: i };
47
+ }
48
+ return { lines, start, end: lines.length };
49
+ }
50
+ function records(body) {
51
+ const area = section(body, 'Applied Deltas');
52
+ if (area.start < 0) return [];
53
+ return area.lines.slice(area.start + 1, area.end).filter(line => line.startsWith('- ')).map(line => {
54
+ let record;
55
+ try { record = JSON.parse(line.slice(2)); } catch (error) {
56
+ throw new f.FlowError('delta_history', 'Invalid Applied Deltas JSON: ' + error.message);
57
+ }
58
+ f.object(record, 'Applied Delta');
59
+ f.string(record.source, 'Applied Delta source');
60
+ f.demand(/^RULE-[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*$/.test(record.rule) &&
61
+ /^[a-f0-9]{64}$/.test(record.payload_sha256), 'delta_history', 'Invalid Applied Delta record');
62
+ return record;
63
+ });
64
+ }
65
+ function appendSection(body, name, addition) {
66
+ const area = section(body, name), lines = area.lines;
67
+ if (area.start < 0) return body.replace(/\s*$/, '') + '\n\n## ' + name + '\n\n' + addition + '\n';
68
+ lines.splice(area.end, 0, addition, '');
69
+ return lines.join('\n');
70
+ }
71
+ function ruleBlock(id, text) {
72
+ return normalize(text).split('\n').map((line, index) => index ? (line ? ' ' + line : '') : '- [' + id + '] ' + line).join('\n');
73
+ }
74
+ function mutate(document, delta, history) {
75
+ const parsed = spec.parseRules(document.body), meta = { ...document.meta };
76
+ const existing = parsed.rules.find(rule => rule.id === delta.ref);
77
+ if (delta.op !== 'add') {
78
+ f.demand(existing && normalize(existing.text) === delta.before, 'delta_precondition',
79
+ 'Rule changed or missing: ' + delta.target + '#' + delta.ref, 1);
80
+ const lines = parsed.lines;
81
+ lines.splice(existing.start, existing.end - existing.start,
82
+ ...(delta.op === 'remove' ? [] : [ruleBlock(existing.id, delta.rule), '']));
83
+ return { body: lines.join('\n'), meta, rule: existing.id, status: 'applied' };
84
+ }
85
+ const duplicate = parsed.rules.find(rule => normalize(rule.text) === delta.rule);
86
+ if (duplicate) return { body: document.body, meta, rule: duplicate.id, status: 'duplicate' };
87
+ const used = [...parsed.rules.map(rule => rule.id), ...history.map(item => item.rule)];
88
+ f.demand(!used.some(id => /^RULE-\d+$/.test(id) && Number(id.slice(5)) >= meta.next_rule),
89
+ 'rule_counter', 'next_rule would reuse an allocated Rule ID', 1);
90
+ f.demand(Number.isSafeInteger(meta.next_rule + 1), 'rule_counter', 'next_rule exhausted', 1);
91
+ const id = 'RULE-' + String(meta.next_rule).padStart(3, '0'); meta.next_rule++;
92
+ return { body: appendSection(document.body, 'Rules', ruleBlock(id, delta.rule)), meta, rule: id, status: 'applied' };
93
+ }
94
+ function applyDelta(document, delta, source, globalHistory) {
95
+ const targetHistory = records(document.body);
96
+ const payload = f.hash(JSON.stringify(delta));
97
+ const previous = globalHistory.get(source) ?? [];
98
+ f.demand(previous.length <= 1, 'delta_history', 'Duplicate Applied Delta identity: ' + source, 1);
99
+ if (previous.length) {
100
+ f.demand(previous[0].record.payload_sha256 === payload, 'delta_conflict',
101
+ 'Applied Delta payload changed: ' + source, 1);
102
+ return { document, result: { source, target: delta.target, rule: previous[0].record.rule,
103
+ status: 'already-applied' } };
104
+ }
105
+ const mutation = mutate({ ...document, body: document.body.replace(/\r\n/g, '\n') }, delta, targetHistory);
106
+ const record = { source, op: delta.op, rule: mutation.rule, payload_sha256: payload, at: new Date().toISOString() };
107
+ const body = appendSection(mutation.body, 'Applied Deltas', '- ' + JSON.stringify(record));
108
+ spec.parseRules(body);
109
+ globalHistory.set(source, [{ record, target: document.id }]);
110
+ const after = fm.stringify(document, mutation.meta, body.replace(/\n/g, document.eol));
111
+ const updated = { ...fm.parse(after), id: document.id, file: document.file };
112
+ return { document: updated, result: { source, target: delta.target, rule: mutation.rule, status: mutation.status } };
113
+ }
114
+
115
+ function loadHistory(root) {
116
+ const base = path.join(root, '.code-flow/specs'), history = new Map();
117
+ for (const relative of spec.walk(base)) {
118
+ const id = relative.slice(0, -3), document = spec.load(root, id);
119
+ for (const record of records(document.body)) {
120
+ const list = history.get(record.source) ?? [];
121
+ list.push({ record, target: id });
122
+ history.set(record.source, list);
123
+ }
124
+ }
125
+ return history;
126
+ }
127
+ function plan(root, tasks) {
128
+ const changes = new Map(), results = [], sources = new Map(), history = loadHistory(root);
129
+ const ordered = [...tasks].sort((a, b) =>
130
+ a.relative.localeCompare(b.relative) || a.id.localeCompare(b.id));
131
+ for (const task of ordered) {
132
+ f.demand(task.status === 'done' && !task.relative.startsWith('.code-flow/tasks/archived/'),
133
+ 'learn_state', 'Learning requires an unarchived done Task', 1);
134
+ sources.set(task.relative, { file: task.document.file, raw: task.document.raw });
135
+ for (const delta of deltas(task)) {
136
+ if (!changes.has(delta.target)) {
137
+ const loaded = spec.load(root, delta.target);
138
+ changes.set(delta.target, { before: loaded.raw, document: loaded });
139
+ }
140
+ const item = changes.get(delta.target);
141
+ const source = task.relative.slice('.code-flow/tasks/'.length) + '#' + task.id + '#' + delta.id;
142
+ const applied = applyDelta(item.document, delta, source, history);
143
+ item.document = applied.document; results.push(applied.result);
144
+ }
145
+ }
146
+ return { changes: [...changes.values()].sort((a, b) => a.document.id.localeCompare(b.document.id)), results,
147
+ sources: [...sources.values()] };
148
+ }
149
+ function checkSources(prepared) {
150
+ for (const item of prepared.sources) f.demand(f.readText(item.file) === item.raw,
151
+ 'concurrent_change', 'Task changed during learning: ' + item.file, 1);
152
+ }
153
+ function commit(prepared, writer = f.atomicWrite) {
154
+ checkSources(prepared);
155
+ for (const item of prepared.changes) {
156
+ checkSources(prepared);
157
+ if (item.document.raw !== item.before) writer(item.document.file, item.document.raw, item.before);
158
+ }
159
+ return { ok: true, action: 'learn', results: prepared.results };
160
+ }
161
+ function preview(prepared) {
162
+ return { ok: true, action: 'learn', dry_run: true, results: prepared.results,
163
+ changes: prepared.changes.filter(item => item.before !== item.document.raw)
164
+ .map(item => ({ file: item.document.file, before: item.before, after: item.document.raw })) };
165
+ }
166
+ async function apply(root, documents, dryRun = false) {
167
+ if (dryRun) return preview(plan(root, documents));
168
+ return f.withLock(root, 'write', async () => commit(plan(root, documents)));
169
+ }
170
+ module.exports = { validate, deltas, plan, commit, preview, apply, records, normalize };
package/lib/locks.js ADDED
@@ -0,0 +1,52 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const f = require('./fs');
5
+ const repo = require('./repo');
6
+
7
+ const bases = new Map();
8
+
9
+ function base(root) {
10
+ const real = f.realpath(root);
11
+ if (bases.has(real)) return bases.get(real);
12
+ const identity = repo.identity(real);
13
+ const value = identity.git ? identity.git_common_dir : real;
14
+ bases.set(real, value);
15
+ return value;
16
+ }
17
+
18
+ function directory(root) {
19
+ return path.join(f.transientRoot(base(root)), 'locks');
20
+ }
21
+
22
+ function withRepository(root, operation) {
23
+ return f.withLockIn(directory(root), 'repository', operation);
24
+ }
25
+
26
+ function withRepositorySync(root, operation) {
27
+ const lock = f.acquireIn(directory(root), 'repository');
28
+ try { return operation(); } finally { f.releaseLock(lock); }
29
+ }
30
+
31
+ function acquireGroup(root, group, timeoutSeconds, signal) {
32
+ const deadline = Date.now() + timeoutSeconds * 1000;
33
+ return new Promise((resolve, reject) => {
34
+ const attempt = () => {
35
+ if (signal.aborted) { reject(Object.assign(new Error('cancelled'), { code: 'cancelled' })); return; }
36
+ try { resolve(f.acquireIn(directory(root), 'group:' + group)); return; } catch (error) {
37
+ if (error.code !== 'locked') { reject(error); return; }
38
+ }
39
+ if (Date.now() >= deadline) {
40
+ reject(Object.assign(new Error('group lock timeout: ' + group), { code: 'lock_timeout' })); return;
41
+ }
42
+ setTimeout(attempt, 25);
43
+ };
44
+ attempt();
45
+ });
46
+ }
47
+
48
+ function release(lock) {
49
+ f.releaseLock(lock);
50
+ }
51
+
52
+ module.exports = { base, directory, withRepository, withRepositorySync, acquireGroup, release };
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+
3
+ function fenceMarker(line, current) {
4
+ const match = /^\s*(`{3,}|~{3,})/.exec(line);
5
+ if (!match) return null;
6
+ const token = match[1];
7
+ if (!current) return { next: token };
8
+ const closes = token[0] === current[0] && token.length >= current.length &&
9
+ line.slice(match[0].length).trim() === '';
10
+ return { next: closes ? null : current };
11
+ }
12
+
13
+ module.exports = { fenceMarker };
package/lib/process.js ADDED
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+ const fs = require('node:fs');
3
+ const path = require('node:path');
4
+ const crypto = require('node:crypto');
5
+ const { spawnSync } = require('node:child_process');
6
+ const config = require('./config');
7
+ const task = require('./task');
8
+ const spec = require('./spec');
9
+ const doctor = require('./doctor');
10
+
11
+ function stamp() {
12
+ return new Date().toISOString().replace(/[:.]/g, '-');
13
+ }
14
+
15
+ function write(file, text) {
16
+ fs.mkdirSync(path.dirname(file), { recursive: true });
17
+ fs.writeFileSync(file, text);
18
+ }
19
+
20
+ function json(file, value) {
21
+ write(file, JSON.stringify(value, null, 2) + '\n');
22
+ }
23
+
24
+ function walk(directory, prefix = '') {
25
+ if (!fs.existsSync(directory)) return [];
26
+ return fs.readdirSync(directory, { withFileTypes: true })
27
+ .sort((left, right) => left.name.localeCompare(right.name))
28
+ .flatMap(entry => {
29
+ const relative = prefix + entry.name;
30
+ return entry.isDirectory() ? walk(path.join(directory, entry.name), relative + '/') : [relative];
31
+ });
32
+ }
33
+
34
+ function copyTree(source, target) {
35
+ if (fs.existsSync(source)) fs.cpSync(source, target, { recursive: true });
36
+ }
37
+
38
+ function git(root, args) {
39
+ const result = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 8 * 1024 * 1024 });
40
+ return result.status === 0 ? result.stdout : 'git unavailable\n';
41
+ }
42
+
43
+ function safe(operation) {
44
+ try { return operation(); } catch (error) {
45
+ return { error: { code: error.code || 'operation_failed', message: error.message } };
46
+ }
47
+ }
48
+
49
+ const README = `code-flow process bundle
50
+
51
+ events.jsonl optional CLI/Hook event log (command, args, exit, ms, byte counts, compact summary)
52
+ task-status.json code-flow task status --json
53
+ doctor.json code-flow doctor --json
54
+ spec-list.json code-flow spec list --json
55
+ code-flow/specs/ project Spec snapshot
56
+ code-flow/tasks/ project task/requirement document snapshot
57
+ code-flow/config.json project config
58
+ files.sha256 SHA-256 of Specs and task documents
59
+ git-status.txt working tree status
60
+ git-log.txt recent commits
61
+ git-diff-stat.txt uncommitted diff summary
62
+ transcript.jsonl optional host conversation transcript
63
+
64
+ Privacy: this bundle can contain project paths, requirement text, and conversation
65
+ content. Review it before sharing. The event log does not store rule text or source code.
66
+
67
+ Analysis targets: command sequence and timing, finish retries, blocked states,
68
+ injected/omitted rules and byte counts, spec learn/archive results, task status,
69
+ and the requirement documents produced by the workflow.
70
+ `;
71
+
72
+ function collect(root, options = {}) {
73
+ const settings = config.load(root);
74
+ const out = options.out ? path.resolve(options.out) :
75
+ path.join(root, settings.capture.path, 'bundle-' + stamp());
76
+ fs.mkdirSync(out, { recursive: true });
77
+ json(path.join(out, 'task-status.json'), safe(() => task.list(root)));
78
+ json(path.join(out, 'doctor.json'), safe(() => doctor.run(root)));
79
+ json(path.join(out, 'spec-list.json'), safe(() => spec.list(root)));
80
+ const events = process.env.CF_EVENT_LOG || path.join(root, settings.capture.path, 'events.jsonl');
81
+ if (fs.existsSync(events)) fs.copyFileSync(events, path.join(out, 'events.jsonl'));
82
+ copyTree(path.join(root, '.code-flow/specs'), path.join(out, 'code-flow/specs'));
83
+ copyTree(path.join(root, '.code-flow/tasks'), path.join(out, 'code-flow/tasks'));
84
+ if (fs.existsSync(path.join(root, '.code-flow/config.json'))) {
85
+ fs.copyFileSync(path.join(root, '.code-flow/config.json'), path.join(out, 'code-flow/config.json'));
86
+ }
87
+ const documents = [];
88
+ for (const name of ['specs', 'tasks']) {
89
+ const base = path.join(root, '.code-flow', name);
90
+ for (const relative of walk(base)) if (relative.endsWith('.md')) documents.push([base, relative]);
91
+ }
92
+ const lines = documents.sort(([leftBase, left], [rightBase, right]) =>
93
+ (leftBase + '/' + left).localeCompare(rightBase + '/' + right)).map(([base, relative]) => {
94
+ const file = path.join(base, relative);
95
+ const digest = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
96
+ return digest + ' ' + path.relative(root, file).split(path.sep).join('/');
97
+ });
98
+ write(path.join(out, 'files.sha256'), lines.join('\n') + (lines.length ? '\n' : ''));
99
+ for (const [name, args] of [['git-status.txt', ['status', '--short']],
100
+ ['git-log.txt', ['log', '--oneline', '-20']], ['git-diff-stat.txt', ['diff', '--stat']]]) {
101
+ write(path.join(out, name), git(root, args));
102
+ }
103
+ let transcript = false;
104
+ if (options.transcript) {
105
+ const file = path.resolve(options.transcript);
106
+ if (fs.existsSync(file)) {
107
+ fs.copyFileSync(file, path.join(out, 'transcript.jsonl'));
108
+ transcript = true;
109
+ }
110
+ }
111
+ write(path.join(out, 'README.txt'), README);
112
+ return { ok: true, action: 'process collect', out, transcript,
113
+ text: 'Process bundle: ' + out + '\n' };
114
+ }
115
+
116
+ module.exports = { collect };