@michaelxwb/code-flow 0.6.4 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.md +47 -3
  2. package/docs/USAGE.md +1529 -0
  3. package/docs/acceptance-contract.md +217 -0
  4. package/docs/ci/acceptance-evidence.md +77 -0
  5. package/docs/migrations/runtime-0.7.0.md +41 -0
  6. package/docs/migrations/runtime-0.8.0.md +154 -0
  7. package/docs/migrations/runtime-0.9.0.md +40 -0
  8. package/docs/spec-injection-hardening.md +54 -0
  9. package/docs/spec-injection-performance.json +73 -0
  10. package/docs/spec-optimization.md +12 -0
  11. package/package.json +18 -3
  12. package/src/adapters/claude/CLAUDE.md +1 -1
  13. package/src/adapters/claude/commands/cf-init.md +1 -3
  14. package/src/adapters/claude/commands/cf-spec.md +23 -3
  15. package/src/adapters/claude/commands/cf-stats.md +11 -1
  16. package/src/adapters/claude/commands/cf-sync.md +20 -6
  17. package/src/adapters/claude/commands/cf-task/align.md +25 -3
  18. package/src/adapters/claude/commands/cf-task/archive.md +36 -40
  19. package/src/adapters/claude/commands/cf-task/block.md +15 -2
  20. package/src/adapters/claude/commands/cf-task/graph.md +13 -1
  21. package/src/adapters/claude/commands/cf-task/note.md +13 -1
  22. package/src/adapters/claude/commands/cf-task/plan.md +137 -23
  23. package/src/adapters/claude/commands/cf-task/prd.md +13 -2
  24. package/src/adapters/claude/commands/cf-task/start.md +67 -35
  25. package/src/adapters/claude/commands/cf-task/status.md +12 -0
  26. package/src/adapters/claude/commands/cf-task/verify-e2e.md +72 -34
  27. package/src/adapters/claude/commands/cf-validate.md +30 -5
  28. package/src/adapters/codex/AGENTS.md +1 -1
  29. package/src/adapters/codex/hooks.json +14 -10
  30. package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
  31. package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
  32. package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
  33. package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
  34. package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
  35. package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -40
  36. package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
  37. package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
  38. package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
  39. package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
  40. package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
  41. package/src/adapters/codex/skills/cf-task-start/SKILL.md +67 -35
  42. package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
  43. package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +73 -34
  44. package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
  45. package/src/adapters/commands.json +115 -0
  46. package/src/adapters/costrict/CLAUDE.md +1 -1
  47. package/src/adapters/costrict/commands/cf-init.md +1 -3
  48. package/src/adapters/costrict/commands/cf-spec.md +23 -3
  49. package/src/adapters/costrict/commands/cf-stats.md +11 -1
  50. package/src/adapters/costrict/commands/cf-sync.md +20 -6
  51. package/src/adapters/costrict/commands/cf-task/align.md +25 -3
  52. package/src/adapters/costrict/commands/cf-task/archive.md +36 -40
  53. package/src/adapters/costrict/commands/cf-task/block.md +15 -2
  54. package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
  55. package/src/adapters/costrict/commands/cf-task/note.md +13 -1
  56. package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
  57. package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
  58. package/src/adapters/costrict/commands/cf-task/start.md +67 -35
  59. package/src/adapters/costrict/commands/cf-task/status.md +12 -0
  60. package/src/adapters/costrict/commands/cf-task/verify-e2e.md +72 -34
  61. package/src/adapters/costrict/commands/cf-validate.md +30 -5
  62. package/src/adapters/install-plan.json +100 -0
  63. package/src/adapters/opencode/AGENTS.md +1 -1
  64. package/src/adapters/opencode/commands/cf-init.md +7 -7
  65. package/src/adapters/opencode/commands/cf-learn.md +6 -6
  66. package/src/adapters/opencode/commands/cf-spec.md +34 -2
  67. package/src/adapters/opencode/commands/cf-stats.md +17 -7
  68. package/src/adapters/opencode/commands/cf-sync.md +21 -6
  69. package/src/adapters/opencode/commands/cf-task/align.md +25 -8
  70. package/src/adapters/opencode/commands/cf-task/archive.md +36 -40
  71. package/src/adapters/opencode/commands/cf-task/block.md +16 -2
  72. package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
  73. package/src/adapters/opencode/commands/cf-task/note.md +14 -1
  74. package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
  75. package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
  76. package/src/adapters/opencode/commands/cf-task/start.md +68 -41
  77. package/src/adapters/opencode/commands/cf-task/status.md +13 -0
  78. package/src/adapters/opencode/commands/cf-task/verify-e2e.md +72 -34
  79. package/src/adapters/opencode/commands/cf-validate.md +35 -9
  80. package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
  81. package/src/adapters/platform-bindings.json +370 -0
  82. package/src/cli.js +369 -316
  83. package/src/core/code-flow/config.yml +4 -1
  84. package/src/core/code-flow/runtime-commands.json +259 -0
  85. package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
  86. package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
  87. package/src/core/code-flow/scripts/cf_audit.py +10 -0
  88. package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
  89. package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
  90. package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
  91. package/src/core/code-flow/scripts/cf_feedback.py +5 -51
  92. package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
  93. package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
  94. package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
  95. package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
  96. package/src/core/code-flow/scripts/cf_spec_context.py +5 -1714
  97. package/src/core/code-flow/scripts/cf_spec_gate.py +5 -257
  98. package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
  99. package/src/core/code-flow/scripts/cf_stats.py +5 -501
  100. package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
  101. package/src/core/code-flow/scripts/cf_sync.py +5 -135
  102. package/src/core/code-flow/scripts/cf_task_index.py +5 -143
  103. package/src/core/code-flow/scripts/cf_task_parallel.py +5 -380
  104. package/src/core/code-flow/scripts/cf_task_workflow.py +6 -103
  105. package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
  106. package/src/core/code-flow/scripts/cf_validation.py +6 -77
  107. package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
  108. package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
  109. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +347 -0
  110. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
  111. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
  112. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
  113. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
  114. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
  115. package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
  116. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
  117. package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
  118. package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
  119. package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
  120. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
  121. package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
  122. package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
  123. package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
  124. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
  125. package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
  126. package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
  127. package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
  128. package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
  129. package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
  130. package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
  131. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
  132. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
  133. package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
  134. package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
  135. package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
  136. package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
  137. package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
  138. package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
  139. package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
  140. package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
  141. package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
  142. package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
  143. package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
  144. package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
  145. package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
  146. package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
  147. package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
  148. package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
  149. package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
  150. package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
  151. package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
  152. package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
  153. package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
  154. package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
  155. package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
  156. package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
  157. package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
  158. package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
  159. package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
  160. package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
  161. package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
  162. package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
  163. package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
  164. package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
  165. package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
  166. package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
  167. package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
  168. package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
  169. package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
  170. package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
  171. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
  172. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
  173. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
  174. package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +37 -3
  175. package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
  176. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
  177. package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
  178. package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
  179. package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
  180. package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +252 -26
  181. package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
  182. package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
  183. package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
  184. package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
  185. package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
  186. package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
  187. package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
  188. package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
  189. package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
  190. package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
  191. package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
  192. package/src/core/code-flow/scripts/runtime-resources.json +29 -0
  193. package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
  194. package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
  195. package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
  196. package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
  197. package/src/core/code-flow/validation.yml +9 -3
  198. package/src/migrate/runtime.js +213 -0
  199. package/src/migrate/spec-workflow.js +16 -1
  200. package/src/runtime.js +219 -0
  201. package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
  202. package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
  203. package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
  204. package/src/core/code-flow/scripts/cf_task_runtime.py +0 -299
  205. package/src/core/code-flow/scripts/cf_task_state.py +0 -3
  206. package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
  207. /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
package/src/cli.js CHANGED
@@ -8,10 +8,17 @@ const { spawnSync } = require('child_process');
8
8
 
9
9
  const pkg = require('../package.json');
10
10
  const specWorkflowMigration = require('./migrate/spec-workflow');
11
+ const runtime = require('./runtime');
12
+ const runtimeMigration = require('./migrate/runtime');
13
+ const commandRegistry = JSON.parse(fs.readFileSync(path.join(__dirname, 'core/code-flow/runtime-commands.json'), 'utf8'));
14
+ const runtimeFamilies = [...new Set(Object.keys(commandRegistry.commands).map(command => command.split(' ')[0]))];
15
+ const installPlan = JSON.parse(fs.readFileSync(path.join(__dirname, 'adapters/install-plan.json'), 'utf8'));
11
16
 
12
17
  const usage = [
13
18
  'Usage: code-flow init [--force] [--platform=<claude|codex|costrict|opencode>] [--backup-dir=<path>]',
14
19
  ' code-flow migrate --spec-workflow <--dry-run|--prepare|--apply --plan <path>|--rollback <id>>',
20
+ ' code-flow migrate --runtime <--dry-run|--apply|--rollback <id>>',
21
+ ` code-flow <${runtimeFamilies.join('|')}> [action] [options]`,
15
22
  ' code-flow -v | --version',
16
23
  ' code-flow -h | --help'
17
24
  ].join('\n');
@@ -45,6 +52,7 @@ function fileCategory(relPath) {
45
52
  if (p.startsWith('.agents/skills/')) return 'tool';
46
53
  if (p.startsWith('.opencode/commands/')) return 'tool';
47
54
  if (p.startsWith('.code-flow/scripts/')) return 'tool';
55
+ if (p === '.code-flow/runtime-commands.json') return 'tool';
48
56
  if (p === 'CLAUDE.md') return 'merge';
49
57
  if (p === 'AGENTS.md') return 'merge';
50
58
  if (p === '.claude/settings.local.json') return 'merge';
@@ -111,13 +119,9 @@ function writeAdapterVersions(cwd, versions) {
111
119
 
112
120
  // Marker dirs proving a platform was installed before per-platform tracking.
113
121
  function platformFilesExist(cwd, platform) {
114
- const markers = {
115
- claude: ['.claude/commands'],
116
- codex: ['.agents/skills'],
117
- costrict: ['.costrict/commands'],
118
- opencode: ['.opencode/commands'],
119
- };
120
- return (markers[platform] || []).some(rel => fs.existsSync(path.join(cwd, rel)));
122
+ const spec = installPlan.platforms[platform];
123
+ const markers = spec ? spec.markers : [];
124
+ return markers.some(rel => fs.existsSync(path.join(cwd, rel)));
121
125
  }
122
126
 
123
127
  // --- Merge functions ---
@@ -129,11 +133,38 @@ function maskFencedCode(text) {
129
133
  return text.replace(/```[\s\S]*?```/g, block => block.replace(/[^\n]/g, ' '));
130
134
  }
131
135
 
136
+ // A managed HTML marker (`<!-- code-flow:... start -->`) directly above a
137
+ // section heading is part of that section. Slicing from the heading line
138
+ // dropped the start marker, leaving an unpaired end marker that both blocked
139
+ // future migrations (cf_spec_migrate needs both) and appended duplicate blocks.
140
+ function managedSliceStart(masked, index) {
141
+ const before = masked.slice(0, index);
142
+ const marker = before.match(/<!-- code-flow:[\w-]+ schema=\d+ start -->[ \t]*\n$/);
143
+ return marker ? index - marker[0].length : index;
144
+ }
145
+
146
+ function repairManagedStartMarker(srcText, destText, destFile) {
147
+ // dest may already contain the managed section from an older merge that
148
+ // dropped the start marker. Restore it before the section heading so
149
+ // cf_spec_migrate._replace_managed can recognize the block again.
150
+ const srcStart = srcText.match(/<!-- code-flow:[\w-]+ schema=\d+ start -->/);
151
+ if (!srcStart || destText.includes(srcStart[0])) return [];
152
+ const endMatch = /<!-- code-flow:[\w-]+ schema=\d+ end -->/.exec(destText);
153
+ if (!endMatch) return [];
154
+ const headingAt = destText.lastIndexOf('\n## ', endMatch.index);
155
+ const insertAt = headingAt === -1 ? destText.lastIndexOf('\n', endMatch.index) + 1 : headingAt + 1;
156
+ const repaired = destText.slice(0, insertAt) + srcStart[0] + '\n' + destText.slice(insertAt);
157
+ fs.writeFileSync(destFile, repaired);
158
+ return ['repaired managed block start marker'];
159
+ }
160
+
132
161
  function mergeClaudeMd(srcFile, destFile) {
133
162
  const srcText = fs.readFileSync(srcFile, 'utf8');
134
163
  const destText = fs.readFileSync(destFile, 'utf8');
164
+ const repaired = repairManagedStartMarker(srcText, destText, destFile);
165
+ const baseText = repaired.length > 0 ? fs.readFileSync(destFile, 'utf8') : destText;
135
166
  const srcMasked = maskFencedCode(srcText);
136
- const destMasked = maskFencedCode(destText);
167
+ const destMasked = maskFencedCode(baseText);
137
168
 
138
169
  const sectionRegex = /^## .+$/gm;
139
170
  const srcSections = [];
@@ -149,20 +180,37 @@ function mergeClaudeMd(srcFile, destFile) {
149
180
  }
150
181
 
151
182
  const missing = srcSections.filter(s => !destSectionSet.has(s.heading));
152
- if (missing.length === 0) return [];
183
+ if (missing.length === 0) return repaired;
153
184
 
154
185
  const additions = [];
155
186
  for (const { heading, index } of missing) {
156
- // Find next real section heading using the masked text, then slice the
157
- // original text by that offset so we keep any code-block content intact.
187
+ // Slice so a managed marker directly above the heading belongs to this
188
+ // section, and the marker above the NEXT heading does not leak into this
189
+ // section's tail (that produced duplicate start markers).
190
+ const start = managedSliceStart(srcMasked, index);
158
191
  const nextRel = srcMasked.slice(index + heading.length).search(/^## /m);
159
- const end = nextRel === -1 ? srcText.length : index + heading.length + nextRel;
160
- additions.push(srcText.slice(index, end).trimEnd());
192
+ const end = nextRel === -1
193
+ ? srcText.length
194
+ : managedSliceStart(srcMasked, index + heading.length + nextRel);
195
+ additions.push(srcText.slice(start, end).trimEnd());
161
196
  }
162
197
 
163
- const merged = destText.trimEnd() + '\n\n' + additions.join('\n\n') + '\n';
198
+ const merged = baseText.trimEnd() + '\n\n' + additions.join('\n\n') + '\n';
164
199
  fs.writeFileSync(destFile, merged);
165
- return missing.map(m => m.heading);
200
+ return [...repaired, ...missing.map(m => m.heading)];
201
+ }
202
+
203
+ // Managed hook scripts from older releases that are no longer shipped. Hook
204
+ // registration is additive (user commands are never removed), so these stale
205
+ // managed commands would otherwise keep firing on every event forever.
206
+ const RETIRED_HOOK_SCRIPTS = [
207
+ 'cf_inject_hook.py',
208
+ 'cf_session_hook.py',
209
+ 'cf_codex_user_prompt_hook.py',
210
+ ];
211
+
212
+ function isManagedCommand(command) {
213
+ return typeof command === 'string' && RETIRED_HOOK_SCRIPTS.some(name => command.includes(name));
166
214
  }
167
215
 
168
216
  // Deep-merge a single hook event array. Each item has shape
@@ -171,9 +219,9 @@ function mergeClaudeMd(srcFile, destFile) {
171
219
  // dest items; for existing items we union the inner hooks array by command
172
220
  // string. User-added items / commands are never removed or rewritten — the
173
221
  // merge is purely additive, in line with cli/code-standards.md "合并策略
174
- // 必须保证用户自定义内容不被覆盖".
175
- // If a future release changes a managed hook command string, add an explicit
176
- // migration before merging; otherwise old and new commands will both run.
222
+ // 必须保证用户自定义内容不被覆盖". The one exception is managed hook entries
223
+ // for scripts that no longer exist: those are replaced by the current template
224
+ // command for the same event/matcher (RETIRED_HOOK_SCRIPTS whitelist).
177
225
  function mergeHookEventArray(srcArr, destArr, eventName, added) {
178
226
  if (!Array.isArray(srcArr) || !Array.isArray(destArr)) return;
179
227
  for (const srcItem of srcArr) {
@@ -190,6 +238,15 @@ function mergeHookEventArray(srcArr, destArr, eventName, added) {
190
238
  continue;
191
239
  }
192
240
  if (!Array.isArray(destItem.hooks)) destItem.hooks = [];
241
+ const retired = destItem.hooks.filter(h => h && isManagedCommand(h.command));
242
+ if (retired.length > 0) {
243
+ destItem.hooks = destItem.hooks.filter(h => !(h && isManagedCommand(h.command)));
244
+ for (const name of RETIRED_HOOK_SCRIPTS) {
245
+ if (retired.some(h => String(h.command).includes(name))) {
246
+ added.push(`hook: ${eventName} -retired ${name}`);
247
+ }
248
+ }
249
+ }
193
250
  const destCmds = new Set(
194
251
  destItem.hooks.map(h => (h && typeof h.command === 'string' ? h.command : ''))
195
252
  );
@@ -237,6 +294,19 @@ function mergeSettingsJson(srcFile, destFile) {
237
294
  return added;
238
295
  }
239
296
 
297
+ function mergeCodexHooksJson(srcFile, destFile) {
298
+ const dest = JSON.parse(fs.readFileSync(destFile, 'utf8'));
299
+ const managed = /\.code-flow\/scripts\/cf_(?:pre_tool_hook|post_hook|user_prompt_hook|stop_hook|codex_hook)\.py/;
300
+ for (const groups of Object.values(dest.hooks || {})) {
301
+ for (const group of groups) group.hooks = (group.hooks || []).filter(hook => !managed.test(hook.command || ''));
302
+ }
303
+ for (const event of Object.keys(dest.hooks || {})) {
304
+ dest.hooks[event] = dest.hooks[event].filter(group => group.hooks.length);
305
+ }
306
+ fs.writeFileSync(destFile, JSON.stringify(dest, null, 2) + '\n');
307
+ return mergeSettingsJson(srcFile, destFile);
308
+ }
309
+
240
310
  // OpenCode v2 merge: migrate the v1 `plugin` key to `plugins`, drop the
241
311
  // auto-discovered local path (`.opencode/plugins/*` loads without config),
242
312
  // and union template `plugins` entries without overwriting user content.
@@ -394,6 +464,8 @@ function collectFiles(dir, base) {
394
464
  if (!fs.existsSync(dir)) return results;
395
465
  const entries = fs.readdirSync(dir, { withFileTypes: true });
396
466
  for (const entry of entries) {
467
+ // OS metadata junk must never be deployed into user projects.
468
+ if (entry.name === '.DS_Store' || entry.name === 'Thumbs.db') continue;
397
469
  const rel = path.join(base, entry.name);
398
470
  if (entry.isDirectory()) {
399
471
  results.push(...collectFiles(path.join(dir, entry.name), rel));
@@ -408,6 +480,7 @@ function copyDirRecursive(srcDir, destDir, overwrite) {
408
480
  fs.mkdirSync(destDir, { recursive: true });
409
481
  const entries = fs.readdirSync(srcDir, { withFileTypes: true });
410
482
  for (const entry of entries) {
483
+ if (entry.name === '.DS_Store' || entry.name === 'Thumbs.db') continue;
411
484
  const srcPath = path.join(srcDir, entry.name);
412
485
  const destPath = path.join(destDir, entry.name);
413
486
  if (entry.isDirectory()) {
@@ -512,9 +585,48 @@ const MANAGED_LEGACY_SKILL_NAMES = new Set([
512
585
  'cf-validate.md',
513
586
  ]);
514
587
 
515
- // --- pyyaml dependency: probe first, fall back through PEP 668 strategies ---
588
+ // Runtime ignore list for the managed `.code-flow/.gitignore`.
589
+ // npm never ships `.gitignore` files in tarballs, so `src/core/code-flow/.gitignore`
590
+ // cannot reach `code-flow init` from a published package. The packaged
591
+ // `scripts/runtime-resources.json` declaration is the single source shared with
592
+ // cf_spec_migrate; a test keeps the source-tree template aligned with it.
593
+ const RUNTIME_RESOURCES = JSON.parse(fs.readFileSync(
594
+ path.join(__dirname, 'core/code-flow/scripts/runtime-resources.json'), 'utf8'));
595
+ const RUNTIME_IGNORE_START = RUNTIME_RESOURCES.gitignore.start;
596
+ const RUNTIME_IGNORE_END = RUNTIME_RESOURCES.gitignore.end;
597
+ const RUNTIME_GITIGNORE = [
598
+ RUNTIME_IGNORE_START,
599
+ ...RUNTIME_RESOURCES.gitignore.entries,
600
+ RUNTIME_IGNORE_END,
601
+ '',
602
+ ].join('\n');
603
+
604
+ function ensureRuntimeGitignore(cwd, results) {
605
+ const dest = path.join(cwd, '.code-flow', '.gitignore');
606
+ if (!fs.existsSync(dest)) {
607
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
608
+ fs.writeFileSync(dest, RUNTIME_GITIGNORE);
609
+ results.created.push('.code-flow/.gitignore');
610
+ return;
611
+ }
612
+ // Existing managed block: add runtime entries introduced by later releases.
613
+ // Files without our markers are user-owned and left untouched.
614
+ const text = fs.readFileSync(dest, 'utf8');
615
+ const endMarker = RUNTIME_IGNORE_END;
616
+ if (!text.includes(RUNTIME_IGNORE_START) || !text.includes(endMarker)) return;
617
+ const lines = text.split('\n');
618
+ const missing = RUNTIME_RESOURCES.gitignore.entries.filter(
619
+ line => line && !lines.includes(line)
620
+ );
621
+ if (missing.length === 0) return;
622
+ const at = lines.indexOf(endMarker);
623
+ lines.splice(at, 0, ...missing);
624
+ fs.writeFileSync(dest, lines.join('\n'));
625
+ results.merged.push(`.code-flow/.gitignore — added: ${missing.join(', ')}`);
626
+ }
516
627
 
517
628
  // --- pyyaml dependency: probe first, fall back through PEP 668 strategies ---
629
+
518
630
  // Bounded waits: each attempt carries its own timeout inside a total budget,
519
631
  // with progress on stderr. spawnFn/logFn/nowFn are injectable for tests.
520
632
  const PIP_PER_ATTEMPT_MS = 60000;
@@ -553,10 +665,26 @@ function ensurePyYaml(spawnFn, logFn, nowFn) {
553
665
 
554
666
  // --- Adapter file installer: shared by every platform branch ---
555
667
 
668
+ // Run a merge function with a clear, actionable error instead of a raw stack
669
+ // trace. A corrupt destination file must not abort init silently mid-way; the
670
+ // operator gets the file name and the recovery step.
671
+ function runMerge(mergeFn, src, dest, label) {
672
+ try {
673
+ return mergeFn(src, dest);
674
+ } catch (error) {
675
+ const message = error && error.message ? error.message : String(error);
676
+ throw new Error(
677
+ `${label}: merge failed (${message}). Fix or delete the file, then re-run init.`
678
+ );
679
+ }
680
+ }
681
+
556
682
  // Install one adapter file with the standard create / force / upgrade /
557
- // skip semantics. `mergeFn` is consulted only on upgrade for `merge`-class
558
- // files; pass null for `tool`-class files (overwrite on upgrade) or for
559
- // fresh-only files (skip on upgrade).
683
+ // skip semantics. `mergeFn` is consulted on upgrade for `merge`-class files
684
+ // and also in fresh mode when the destination already exists (a pre-existing
685
+ // AGENTS.md/CLAUDE.md must still receive the managed sections instead of
686
+ // being silently skipped — otherwise the Spec Workflow protocol never lands).
687
+ // Pass null for `tool`-class files or fresh-only files.
560
688
  function installAdapterFile(opts) {
561
689
  const { src, dest, label, mode, mergeFn, toolOnUpgrade, results } = opts;
562
690
  if (!fs.existsSync(dest)) {
@@ -570,6 +698,15 @@ function installAdapterFile(opts) {
570
698
  fs.copyFileSync(src, dest);
571
699
  return;
572
700
  }
701
+ if (mode === 'fresh' && mergeFn) {
702
+ const added = runMerge(mergeFn, src, dest, label);
703
+ if (added.length > 0) {
704
+ results.merged.push(`${label} — added: ${added.join(', ')}`);
705
+ } else {
706
+ results.skipped.push(label);
707
+ }
708
+ return;
709
+ }
573
710
  if (mode === 'upgrade') {
574
711
  if (toolOnUpgrade) {
575
712
  results.updated.push(label);
@@ -577,7 +714,7 @@ function installAdapterFile(opts) {
577
714
  return;
578
715
  }
579
716
  if (mergeFn) {
580
- const added = mergeFn(src, dest);
717
+ const added = runMerge(mergeFn, src, dest, label);
581
718
  if (added.length > 0) {
582
719
  results.merged.push(`${label} — added: ${added.join(', ')}`);
583
720
  } else {
@@ -614,263 +751,144 @@ function parseStringFlag(args, name) {
614
751
  return '';
615
752
  }
616
753
 
617
- // --- Main init ---
754
+ // --- Install orchestration driven by adapters/install-plan.json ---
618
755
 
619
- function runInit(force, platform, opts) {
620
- const cwd = process.cwd();
621
- const options = opts || {};
622
- const installedVersion = readInstalledVersion(cwd);
623
- if (installedVersion && compareVersions(installedVersion, '0.4.2') >= 0 && compareVersions(installedVersion, '0.6.0') < 0) {
624
- process.stdout.write(`${JSON.stringify({ status: 'migration_required', version: installedVersion, command: 'code-flow migrate --spec-workflow --dry-run' })}\n`);
625
- process.exit(3);
756
+ function initResults() {
757
+ return { created: [], updated: [], merged: [], skipped: [], removed: [] };
758
+ }
759
+
760
+ const INSTALL_MERGERS = {
761
+ claudeMd: mergeClaudeMd,
762
+ settingsJson: mergeSettingsJson,
763
+ codexHooksJson: mergeCodexHooksJson,
764
+ codexConfigToml: mergeCodexConfigToml,
765
+ opencodeJson: mergeOpencodeJson,
766
+ };
767
+
768
+ // Track + copy helper for directory trees. Core files derive their category
769
+ // per file; declared platform trees pass a single category.
770
+ function installTree(opts) {
771
+ const { srcDir, destDir, prefix, mode, category, ensureDest, runtimeMigration, results } = opts;
772
+ if (ensureDest) fs.mkdirSync(destDir, { recursive: true });
773
+ if (!fs.existsSync(srcDir)) return;
774
+ for (const rel of collectFiles(srcDir, '')) {
775
+ installTreeEntry({
776
+ src: path.join(srcDir, rel),
777
+ dest: path.join(destDir, rel),
778
+ label: path.join(prefix, rel),
779
+ mode,
780
+ category,
781
+ runtimeMigration,
782
+ results,
783
+ });
626
784
  }
627
- ensurePython3();
785
+ }
628
786
 
629
- const baseDir = __dirname;
630
- const coreDir = path.join(baseDir, 'core');
631
- const adaptersDir = path.join(baseDir, 'adapters');
787
+ function installTreeEntry(opts) {
788
+ const { src, dest, label, mode, category, runtimeMigration, results } = opts;
789
+ const cat = category || fileCategory(label);
790
+ if (runtimeMigration && cat === 'user' && !fs.existsSync(dest)) return;
791
+ if (!fs.existsSync(dest)) {
792
+ results.created.push(label);
793
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
794
+ fs.copyFileSync(src, dest);
795
+ } else if (mode === 'force') {
796
+ results.updated.push(label);
797
+ fs.copyFileSync(src, dest);
798
+ } else if (mode === 'upgrade' && cat === 'tool') {
799
+ results.updated.push(label);
800
+ fs.copyFileSync(src, dest);
801
+ } else {
802
+ results.skipped.push(label);
803
+ }
804
+ }
632
805
 
633
- // Determine mode: global mode drives core files and messaging; each
634
- // platform additionally resolves its own mode from per-platform records so
635
- // upgrading one platform can never mask another platform's staleness.
636
- // A global upgrade also refreshes the selected platform (previous behavior).
637
- const mode = modeFor(installedVersion, force);
806
+ function installPlatform(cwd, adaptersDir, platform, mode, runtimeMigration, results) {
807
+ const spec = installPlan.platforms[platform];
808
+ if (!spec) return;
809
+ for (const entry of spec.entries) {
810
+ if (entry.kind === 'file') {
811
+ installAdapterFile({
812
+ src: path.join(adaptersDir, entry.src),
813
+ dest: path.join(cwd, entry.dest),
814
+ label: entry.label,
815
+ mode,
816
+ mergeFn: INSTALL_MERGERS[entry.merger],
817
+ toolOnUpgrade: entry.category === 'tool',
818
+ results,
819
+ });
820
+ } else if (entry.kind === 'dir') {
821
+ installTree({
822
+ srcDir: path.join(adaptersDir, entry.src),
823
+ destDir: path.join(cwd, entry.dest),
824
+ prefix: entry.prefix,
825
+ mode,
826
+ category: entry.category,
827
+ ensureDest: Boolean(entry.ensure),
828
+ runtimeMigration,
829
+ results,
830
+ });
831
+ } else if (entry.kind === 'stamp') {
832
+ stampPackageVersion(path.join(cwd, entry.path));
833
+ }
834
+ }
835
+ }
836
+
837
+ function stampPackageVersion(file) {
838
+ if (!fs.existsSync(file)) return;
839
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
840
+ data.version = pkg.version;
841
+ fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n');
842
+ }
843
+
844
+ function resolvePlatformMode(cwd, platform, mode, force, options) {
638
845
  const adapterVersions = readAdapterVersions(cwd);
639
846
  const adapterPrev = typeof adapterVersions.adapters[platform] === 'string'
640
847
  ? adapterVersions.adapters[platform]
641
848
  : null;
642
- let platformMode;
643
- if (force) {
644
- platformMode = 'force';
645
- } else if (!adapterPrev && !platformFilesExist(cwd, platform)) {
646
- platformMode = 'fresh';
647
- } else if (
849
+ if (options.runtimeMigration) return 'upgrade';
850
+ if (force) return 'force';
851
+ if (!adapterPrev && !platformFilesExist(cwd, platform)) return 'fresh';
852
+ if (
648
853
  (adapterPrev && compareVersions(adapterPrev, pkg.version) < 0) ||
649
854
  (!adapterPrev && platformFilesExist(cwd, platform)) ||
650
855
  mode === 'upgrade'
651
856
  ) {
652
- platformMode = 'upgrade';
653
- } else {
654
- platformMode = 'current';
655
- }
656
-
657
- const created = [];
658
- const updated = [];
659
- const merged = [];
660
- const skipped = [];
661
- const removed = [];
662
- const results = { created, updated, merged, skipped };
663
-
664
- // Track + copy helper for directory trees. dirMode is resolved per area:
665
- // core files follow the global mode, platform files follow platformMode.
666
- const processDir = (srcDir, destDir, prefix, dirMode) => {
667
- const effective = dirMode || mode;
668
- if (!fs.existsSync(srcDir)) return;
669
- const files = collectFiles(srcDir, '');
670
- for (const rel of files) {
671
- const src = path.join(srcDir, rel);
672
- const dest = path.join(destDir, rel);
673
- const label = path.join(prefix, rel);
674
- const cat = fileCategory(label);
675
-
676
- if (!fs.existsSync(dest)) {
677
- created.push(label);
678
- fs.mkdirSync(path.dirname(dest), { recursive: true });
679
- fs.copyFileSync(src, dest);
680
- } else if (effective === 'force') {
681
- updated.push(label);
682
- fs.copyFileSync(src, dest);
683
- } else if (effective === 'upgrade' && cat === 'tool') {
684
- updated.push(label);
685
- fs.copyFileSync(src, dest);
686
- } else {
687
- skipped.push(label);
688
- }
689
- }
690
- };
691
-
692
- // Process .code-flow/ (core)
693
- processDir(path.join(coreDir, 'code-flow'), path.join(cwd, '.code-flow'), '.code-flow', mode);
694
-
695
- // Process Claude adapter
696
- if (platform === 'claude') {
697
- installAdapterFile({
698
- src: path.join(adaptersDir, 'claude', 'CLAUDE.md'),
699
- dest: path.join(cwd, 'CLAUDE.md'),
700
- label: 'CLAUDE.md',
701
- mode: platformMode,
702
- mergeFn: mergeClaudeMd,
703
- toolOnUpgrade: false,
704
- results,
705
- });
706
-
707
- fs.mkdirSync(path.join(cwd, '.claude', 'commands'), { recursive: true });
708
- processDir(
709
- path.join(adaptersDir, 'claude', 'commands'),
710
- path.join(cwd, '.claude', 'commands'),
711
- '.claude/commands', platformMode
712
- );
713
-
714
- installAdapterFile({
715
- src: path.join(adaptersDir, 'claude', 'settings.local.json'),
716
- dest: path.join(cwd, '.claude', 'settings.local.json'),
717
- label: '.claude/settings.local.json',
718
- mode: platformMode,
719
- mergeFn: mergeSettingsJson,
720
- toolOnUpgrade: false,
721
- results,
722
- });
723
- }
724
-
725
- // Process Costrict adapter
726
- if (platform === 'costrict') {
727
- installAdapterFile({
728
- src: path.join(adaptersDir, 'costrict', 'CLAUDE.md'),
729
- dest: path.join(cwd, 'CLAUDE.md'),
730
- label: 'CLAUDE.md',
731
- mode: platformMode,
732
- mergeFn: mergeClaudeMd,
733
- toolOnUpgrade: false,
734
- results,
735
- });
736
-
737
- fs.mkdirSync(path.join(cwd, '.costrict', 'commands'), { recursive: true });
738
- processDir(
739
- path.join(adaptersDir, 'costrict', 'commands'),
740
- path.join(cwd, '.costrict', 'commands'),
741
- '.costrict/commands', platformMode
742
- );
743
-
744
- installAdapterFile({
745
- src: path.join(adaptersDir, 'costrict', 'settings.local.json'),
746
- dest: path.join(cwd, '.costrict', 'settings.local.json'),
747
- label: '.costrict/settings.local.json',
748
- mode: platformMode,
749
- mergeFn: mergeSettingsJson,
750
- toolOnUpgrade: false,
751
- results,
752
- });
753
- }
754
-
755
- // Process Codex adapter
756
- if (platform === 'codex') {
757
- installAdapterFile({
758
- src: path.join(adaptersDir, 'codex', 'AGENTS.md'),
759
- dest: path.join(cwd, 'AGENTS.md'),
760
- label: 'AGENTS.md',
761
- mode: platformMode,
762
- mergeFn: mergeClaudeMd,
763
- toolOnUpgrade: false,
764
- results,
765
- });
766
-
767
- installAdapterFile({
768
- src: path.join(adaptersDir, 'codex', 'hooks.json'),
769
- dest: path.join(cwd, '.codex', 'hooks.json'),
770
- label: '.codex/hooks.json',
771
- mode: platformMode,
772
- mergeFn: mergeSettingsJson,
773
- toolOnUpgrade: false,
774
- results,
775
- });
776
-
777
- installAdapterFile({
778
- src: path.join(adaptersDir, 'codex', 'config.toml'),
779
- dest: path.join(cwd, '.codex', 'config.toml'),
780
- label: '.codex/config.toml',
781
- mode: platformMode,
782
- mergeFn: mergeCodexConfigToml,
783
- toolOnUpgrade: false,
784
- results,
785
- });
786
-
787
- // Project-level .agents/skills/ (version-controlled, committed to repo)
788
- processDir(
789
- path.join(adaptersDir, 'codex', 'skills'),
790
- path.join(cwd, '.agents', 'skills'),
791
- '.agents/skills', platformMode
792
- );
793
- }
794
-
795
- // Process OpenCode adapter
796
- if (platform === 'opencode') {
797
- installAdapterFile({
798
- src: path.join(adaptersDir, 'opencode', 'AGENTS.md'),
799
- dest: path.join(cwd, 'AGENTS.md'),
800
- label: 'AGENTS.md',
801
- mode: platformMode,
802
- mergeFn: mergeClaudeMd,
803
- toolOnUpgrade: false,
804
- results,
805
- });
806
-
807
- // Plugin files under .opencode/plugins/code-flow/
808
- processDir(
809
- path.join(adaptersDir, 'opencode', 'plugins'),
810
- path.join(cwd, '.opencode', 'plugins'),
811
- '.opencode/plugins', platformMode
812
- );
813
-
814
- // Stamp main package version into the plugin's package.json
815
- const pluginPkgPath = path.join(cwd, '.opencode', 'plugins', 'code-flow', 'package.json');
816
- if (fs.existsSync(pluginPkgPath)) {
817
- const pluginPkg = JSON.parse(fs.readFileSync(pluginPkgPath, 'utf8'));
818
- pluginPkg.version = pkg.version;
819
- fs.writeFileSync(pluginPkgPath, JSON.stringify(pluginPkg, null, 2) + '\n');
820
- }
821
-
822
- // Command files under .opencode/commands/
823
- processDir(
824
- path.join(adaptersDir, 'opencode', 'commands'),
825
- path.join(cwd, '.opencode', 'commands'),
826
- '.opencode/commands', platformMode
827
- );
828
-
829
- installAdapterFile({
830
- src: path.join(adaptersDir, 'opencode', 'opencode.json'),
831
- dest: path.join(cwd, 'opencode.json'),
832
- label: 'opencode.json',
833
- mode: platformMode,
834
- mergeFn: mergeOpencodeJson,
835
- toolOnUpgrade: false,
836
- results,
837
- });
857
+ return 'upgrade';
838
858
  }
859
+ return 'current';
860
+ }
839
861
 
840
- // Merge config.yml on upgrade
862
+ function mergeConfigOnUpgrade(cwd, coreDir, mode, options, results) {
841
863
  const configSrc = path.join(coreDir, 'code-flow', 'config.yml');
842
864
  const configDest = path.join(cwd, '.code-flow', 'config.yml');
843
- if (mode === 'upgrade' && fs.existsSync(configDest) && fs.existsSync(configSrc)) {
844
- const added = mergeConfigYml(configSrc, configDest);
845
- if (added.length > 0) {
846
- // Replace the skipped entry with merged
847
- const idx = skipped.indexOf('.code-flow/config.yml');
848
- if (idx !== -1) skipped.splice(idx, 1);
849
- merged.push(`.code-flow/config.yml — added: ${added.join(', ')}`);
850
- }
851
- }
852
-
853
- // Clean up legacy .claude/skills/ (claude domain only; user-owned kept, managed backed up)
854
- removeLegacyClaudeSkills(cwd, removed, { platform, backupDir: options.backupDir || '' });
855
-
856
- // Clean up renamed orphan scripts (only meaningful on upgrade)
857
- if (mode === 'upgrade' || mode === 'force') {
858
- removeOrphanFiles(cwd, removed);
865
+ if (mode !== 'upgrade' || options.runtimeMigration ||
866
+ !fs.existsSync(configDest) || !fs.existsSync(configSrc)) {
867
+ return;
859
868
  }
869
+ const added = mergeConfigYml(configSrc, configDest);
870
+ if (added.length === 0) return;
871
+ // Replace the skipped entry with merged
872
+ const idx = results.skipped.indexOf('.code-flow/config.yml');
873
+ if (idx !== -1) results.skipped.splice(idx, 1);
874
+ results.merged.push(`.code-flow/config.yml — added: ${added.join(', ')}`);
875
+ }
860
876
 
861
- // Install pyyaml — probe first, fall back through PEP 668 strategies
862
- ensurePyYaml();
863
-
864
- // Write version (legacy global file kept for compatibility) + per-platform records
865
- writeVersion(cwd, pkg.version);
877
+ function recordInstall(cwd, platform) {
878
+ const adapterVersions = readAdapterVersions(cwd);
866
879
  adapterVersions.adapters[platform] = pkg.version;
867
880
  adapterVersions.core = pkg.version;
868
881
  writeAdapterVersions(cwd, adapterVersions);
882
+ runtime.writeInstall(cwd, Object.keys(adapterVersions.adapters));
883
+ writeVersion(cwd, pkg.version);
884
+ }
869
885
 
870
- // Output summary
886
+ function printInitSummary(mode, platformMode, installedVersion, results) {
871
887
  if (mode === 'upgrade') {
872
888
  process.stdout.write(`\ncode-flow upgraded: ${installedVersion} → ${pkg.version}\n\n`);
873
- } else if (mode === 'current' && platformMode === 'current' && created.length === 0 && updated.length === 0 && merged.length === 0) {
889
+ } else if (mode === 'current' && platformMode === 'current' &&
890
+ results.created.length === 0 && results.updated.length === 0 &&
891
+ results.merged.length === 0) {
874
892
  process.stdout.write(`code-flow v${pkg.version} already up to date.\n`);
875
893
  process.exit(0);
876
894
  } else if (mode === 'force') {
@@ -878,69 +896,81 @@ function runInit(force, platform, opts) {
878
896
  } else {
879
897
  process.stdout.write(`\ncode-flow v${pkg.version} initialized!\n\n`);
880
898
  }
881
-
882
- if (updated.length > 0) {
883
- process.stdout.write('Updated (tool-managed):\n');
884
- for (const f of updated) process.stdout.write(` ↑ ${f}\n`);
885
- }
886
- if (merged.length > 0) {
887
- process.stdout.write('Merged (new sections added):\n');
888
- for (const f of merged) process.stdout.write(` ⊕ ${f}\n`);
899
+ for (const group of installPlan.summary) {
900
+ const items = results[group.key] || [];
901
+ if (items.length === 0) continue;
902
+ process.stdout.write(`${group.title}\n`);
903
+ for (const item of items) process.stdout.write(` ${group.marker} ${item}\n`);
889
904
  }
890
- if (created.length > 0) {
891
- process.stdout.write('Created:\n');
892
- for (const f of created) process.stdout.write(` + ${f}\n`);
893
- }
894
- if (skipped.length > 0) {
895
- process.stdout.write('Skipped (user-customized):\n');
896
- for (const f of skipped) process.stdout.write(` · ${f}\n`);
905
+ }
906
+
907
+ function printNextSteps(platform, mode) {
908
+ const spec = installPlan.platforms[platform];
909
+ const steps = spec && spec.next_steps[mode === 'fresh' ? 'fresh' : 'upgrade'];
910
+ if (!steps || steps.length === 0) return;
911
+ process.stdout.write('\nNext steps:\n');
912
+ for (const line of steps) process.stdout.write(`${line}\n`);
913
+ }
914
+
915
+ // --- Main init ---
916
+
917
+ function runInit(force, platform, opts) {
918
+ const cwd = process.cwd();
919
+ const options = opts || {};
920
+ const installedVersion = readInstalledVersion(cwd);
921
+ if (!options.runtimeMigration && installedVersion && compareVersions(installedVersion, '0.6.0') >= 0 &&
922
+ !fs.existsSync(path.join(cwd, '.code-flow/runtime-commands.json'))) {
923
+ process.stdout.write(`${JSON.stringify({ status: 'migration_required', command: 'code-flow migrate --runtime --dry-run' })}\n`);
924
+ process.exit(3);
897
925
  }
898
- if (removed.length > 0) {
899
- process.stdout.write('Removed (deprecated):\n');
900
- for (const f of removed) process.stdout.write(` ✕ ${f}\n`);
926
+ if (installedVersion && compareVersions(installedVersion, '0.4.2') >= 0 && compareVersions(installedVersion, '0.6.0') < 0) {
927
+ process.stdout.write(`${JSON.stringify({ status: 'migration_required', version: installedVersion, command: 'code-flow migrate --spec-workflow --dry-run' })}\n`);
928
+ process.exit(3);
901
929
  }
930
+ ensurePython3();
902
931
 
903
- process.stdout.write('\nNext steps:\n');
904
- if (platform === 'codex') {
905
- if (mode === 'fresh') {
906
- process.stdout.write(' 1. Edit AGENTS.md — fill in team/project info\n');
907
- process.stdout.write(' 2. Run /hooks in Codex CLI and trust the code-flow hooks if prompted\n');
908
- process.stdout.write(' 3. Run $cf-init in Codex CLI to auto-scan and populate specs\n');
909
- process.stdout.write(' Or manually edit .code-flow/specs/ to fill in your coding standards\n');
910
- } else {
911
- process.stdout.write(' Run /hooks in Codex CLI and trust changed code-flow hooks if prompted\n');
912
- process.stdout.write(' Run $cf-learn in Codex CLI to update specs with project conventions\n');
913
- process.stdout.write(' Run $cf-learn --map to update retrieval maps\n');
914
- }
915
- } else if (platform === 'costrict') {
916
- if (mode === 'fresh') {
917
- process.stdout.write(' 1. Edit CLAUDE.md — fill in team/project info\n');
918
- process.stdout.write(' 2. Run /cf-init in Costrict to auto-scan and populate specs\n');
919
- process.stdout.write(' Or manually edit .code-flow/specs/ to fill in your coding standards\n');
920
- } else {
921
- process.stdout.write(' Run /cf-learn in Costrict to update specs with project conventions\n');
922
- process.stdout.write(' Run /cf-learn --map to update retrieval maps\n');
923
- }
924
- } else if (platform === 'opencode') {
925
- if (mode === 'fresh') {
926
- process.stdout.write(' 1. Edit AGENTS.md — fill in team/project info\n');
927
- process.stdout.write(' 2. Start opencode in this directory — the plugin auto-loads\n');
928
- process.stdout.write(' 3. Run /cf-init in OpenCode to auto-scan and populate specs\n');
929
- process.stdout.write(' Or manually edit .code-flow/specs/ to fill in your coding standards\n');
930
- } else {
931
- process.stdout.write(' Run /cf-learn in OpenCode to update specs with project conventions\n');
932
- process.stdout.write(' Run /cf-learn --map to update retrieval maps\n');
933
- }
934
- } else {
935
- if (mode === 'fresh') {
936
- process.stdout.write(' 1. Edit CLAUDE.md — fill in team/project info\n');
937
- process.stdout.write(' 2. Run /cf-init in Claude Code to auto-scan and populate specs\n');
938
- process.stdout.write(' Or manually edit .code-flow/specs/ to fill in your coding standards\n');
939
- } else {
940
- process.stdout.write(' Run /cf-learn in Claude Code to update specs with project conventions\n');
941
- process.stdout.write(' Run /cf-learn --map to update retrieval maps\n');
942
- }
932
+ const baseDir = __dirname;
933
+ const coreDir = path.join(baseDir, 'core');
934
+ const adaptersDir = path.join(baseDir, 'adapters');
935
+
936
+ // Determine mode: global mode drives core files and messaging; each
937
+ // platform additionally resolves its own mode from per-platform records so
938
+ // upgrading one platform can never mask another platform's staleness.
939
+ // A global upgrade also refreshes the selected platform (previous behavior).
940
+ const mode = options.runtimeMigration ? 'upgrade' : modeFor(installedVersion, force);
941
+ const platformMode = resolvePlatformMode(cwd, platform, mode, force, options);
942
+ const results = initResults();
943
+
944
+ // Process .code-flow/ (core)
945
+ installTree({
946
+ srcDir: path.join(baseDir, installPlan.core.source),
947
+ destDir: path.join(cwd, installPlan.core.dest),
948
+ prefix: installPlan.core.prefix,
949
+ mode,
950
+ runtimeMigration: options.runtimeMigration,
951
+ results,
952
+ });
953
+ // npm tarballs never include .gitignore, so the published package must write
954
+ // the runtime ignore list itself (or extend it on upgrade).
955
+ ensureRuntimeGitignore(cwd, results);
956
+ installPlatform(cwd, adaptersDir, platform, platformMode, options.runtimeMigration, results);
957
+ mergeConfigOnUpgrade(cwd, coreDir, mode, options, results);
958
+
959
+ // Clean up legacy .claude/skills/ (claude domain only; user-owned kept, managed backed up)
960
+ removeLegacyClaudeSkills(cwd, results.removed, { platform, backupDir: options.backupDir || '' });
961
+
962
+ // Clean up renamed orphan scripts (only meaningful on upgrade)
963
+ if (mode === 'upgrade' || mode === 'force') {
964
+ removeOrphanFiles(cwd, results.removed);
943
965
  }
966
+
967
+ // Install pyyaml — probe first, fall back through PEP 668 strategies
968
+ ensurePyYaml();
969
+
970
+ recordInstall(cwd, platform);
971
+
972
+ printInitSummary(mode, platformMode, installedVersion, results);
973
+ printNextSteps(platform, mode);
944
974
  process.exit(0);
945
975
  }
946
976
 
@@ -948,6 +978,10 @@ function runInit(force, platform, opts) {
948
978
 
949
979
  const args = process.argv.slice(2);
950
980
 
981
+ if (runtimeFamilies.includes(args[0]) || runtime.projectCommandFamily(args[0])) {
982
+ process.exit(runtime.run(args));
983
+ }
984
+
951
985
  function argumentValue(values, name) {
952
986
  const index = values.indexOf(name);
953
987
  return index >= 0 ? values[index + 1] || '' : '';
@@ -971,7 +1005,7 @@ function runMigrate(values) {
971
1005
  if (actions[0] === '--apply') {
972
1006
  const plan = argumentValue(values, '--plan');
973
1007
  if (!plan) fail('Error: --apply requires --plan <path>.');
974
- result = specWorkflowMigration.apply(path.resolve(plan));
1008
+ result = specWorkflowMigration.apply(path.resolve(plan), { projectRoot: process.cwd() });
975
1009
  }
976
1010
  if (actions[0] === '--rollback') {
977
1011
  const id = argumentValue(values, '--rollback');
@@ -1005,10 +1039,29 @@ if (args[0] === 'init') {
1005
1039
  if (platform !== 'claude' && platform !== 'codex' && platform !== 'costrict' && platform !== 'opencode') {
1006
1040
  fail(`Error: --platform must be "claude", "codex", "costrict", or "opencode", got "${platform}".`);
1007
1041
  }
1008
- runInit(force, platform, { backupDir: parseStringFlag(args, 'backup-dir') });
1042
+ try {
1043
+ const staging = process.env.CODE_FLOW_RUNTIME_STAGE === process.cwd() &&
1044
+ fs.existsSync(path.join(process.cwd(), '.runtime-stage.json'));
1045
+ runInit(force, platform, { backupDir: parseStringFlag(args, 'backup-dir'), runtimeMigration: staging });
1046
+ } catch (error) {
1047
+ // Report merge/IO failures as a readable error, never a raw JS stack.
1048
+ const message = error && error.message ? error.message : String(error);
1049
+ process.stderr.write(`Error: init failed — ${message}\n`);
1050
+ process.exit(1);
1051
+ }
1009
1052
  }
1010
1053
 
1011
1054
  if (args[0] === 'migrate') {
1055
+ if (args[1] === '--runtime') {
1056
+ try {
1057
+ const result = runtimeMigration.run(process.cwd(), args.slice(2));
1058
+ process.stdout.write(`${JSON.stringify(result)}\n`);
1059
+ process.exit(result.status === 'rolled_back' ? 4 : 0);
1060
+ } catch (error) {
1061
+ process.stdout.write(`${JSON.stringify({ status: 'blocked', error: error.message })}\n`);
1062
+ process.exit(3);
1063
+ }
1064
+ }
1012
1065
  runMigrate(args.slice(1));
1013
1066
  }
1014
1067