@luanpdd/kit-mcp 1.19.0 → 1.20.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 (231) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +648 -648
  3. package/kit/COMANDOS.md +138 -138
  4. package/kit/README.md +52 -52
  5. package/kit/agents/advisor-researcher.md +106 -106
  6. package/kit/agents/assumptions-analyzer.md +107 -107
  7. package/kit/agents/codebase-mapper.md +768 -768
  8. package/kit/agents/debugger.md +772 -772
  9. package/kit/agents/example-reviewer.md +21 -21
  10. package/kit/agents/executor.md +523 -523
  11. package/kit/agents/integration-checker.md +200 -200
  12. package/kit/agents/nyquist-auditor.md +178 -178
  13. package/kit/agents/phase-researcher.md +696 -696
  14. package/kit/agents/plan-checker.md +272 -272
  15. package/kit/agents/planner.md +891 -891
  16. package/kit/agents/project-researcher.md +652 -652
  17. package/kit/agents/research-synthesizer.md +245 -245
  18. package/kit/agents/roadmapper.md +677 -677
  19. package/kit/agents/ui-auditor.md +437 -437
  20. package/kit/agents/ui-checker.md +302 -302
  21. package/kit/agents/ui-researcher.md +355 -355
  22. package/kit/agents/user-profiler.md +175 -175
  23. package/kit/agents/verifier.md +728 -728
  24. package/kit/commands/adicionar-backlog.md +75 -75
  25. package/kit/commands/adicionar-fase.md +42 -42
  26. package/kit/commands/adicionar-tarefa.md +45 -45
  27. package/kit/commands/adicionar-testes.md +41 -41
  28. package/kit/commands/ajuda.md +21 -21
  29. package/kit/commands/atualizar.md +37 -37
  30. package/kit/commands/auditar-marco.md +179 -179
  31. package/kit/commands/auditar-uat.md +23 -23
  32. package/kit/commands/autonomo.md +40 -40
  33. package/kit/commands/branch-pr.md +24 -24
  34. package/kit/commands/burn-rate-status.md +237 -121
  35. package/kit/commands/concluir-marco.md +247 -247
  36. package/kit/commands/configuracoes.md +36 -36
  37. package/kit/commands/definir-perfil.md +10 -10
  38. package/kit/commands/depurar.md +190 -190
  39. package/kit/commands/discutir-fase.md +131 -131
  40. package/kit/commands/entrar-discord.md +17 -17
  41. package/kit/commands/estatisticas.md +18 -18
  42. package/kit/commands/example-greeting.md +33 -33
  43. package/kit/commands/executar-fase.md +58 -58
  44. package/kit/commands/expresso.md +56 -56
  45. package/kit/commands/fase-ui.md +34 -34
  46. package/kit/commands/fazer.md +57 -57
  47. package/kit/commands/fio.md +125 -125
  48. package/kit/commands/fluxos-trabalho.md +64 -64
  49. package/kit/commands/forense.md +176 -176
  50. package/kit/commands/gerenciador.md +38 -38
  51. package/kit/commands/inserir-fase.md +31 -31
  52. package/kit/commands/limpeza.md +17 -17
  53. package/kit/commands/listar-hipoteses-fase.md +45 -45
  54. package/kit/commands/listar-workspaces.md +18 -18
  55. package/kit/commands/mapear-codebase.md +70 -70
  56. package/kit/commands/nota.md +33 -33
  57. package/kit/commands/novo-marco.md +43 -43
  58. package/kit/commands/novo-projeto.md +41 -41
  59. package/kit/commands/novo-workspace.md +43 -43
  60. package/kit/commands/pausar-trabalho.md +37 -37
  61. package/kit/commands/perfil-usuario.md +45 -45
  62. package/kit/commands/pesquisar-fase.md +195 -195
  63. package/kit/commands/planejar-fase.md +67 -67
  64. package/kit/commands/planejar-lacunas.md +33 -33
  65. package/kit/commands/plantar-ideia.md +25 -25
  66. package/kit/commands/progresso.md +24 -24
  67. package/kit/commands/proximo.md +30 -30
  68. package/kit/commands/publicar.md +490 -490
  69. package/kit/commands/rapido.md +35 -35
  70. package/kit/commands/reaplicar-patches.md +124 -124
  71. package/kit/commands/relatorio-sessao.md +19 -19
  72. package/kit/commands/remover-fase.md +31 -31
  73. package/kit/commands/remover-workspace.md +26 -26
  74. package/kit/commands/resumo-marco.md +50 -50
  75. package/kit/commands/retomar-trabalho.md +40 -40
  76. package/kit/commands/revisar-backlog.md +60 -60
  77. package/kit/commands/revisar-ui.md +32 -32
  78. package/kit/commands/revisar.md +37 -37
  79. package/kit/commands/saude.md +21 -21
  80. package/kit/commands/setup-notion.md +93 -93
  81. package/kit/commands/sync-main.md +68 -68
  82. package/kit/commands/validar-fase.md +35 -35
  83. package/kit/commands/verificar-tarefas.md +44 -44
  84. package/kit/commands/verificar-trabalho.md +64 -64
  85. package/kit/file-manifest.json +3 -3
  86. package/kit/framework/bin/lib/commands.cjs +959 -959
  87. package/kit/framework/bin/lib/config.cjs +442 -442
  88. package/kit/framework/bin/lib/core.cjs +1230 -1230
  89. package/kit/framework/bin/lib/frontmatter.cjs +336 -336
  90. package/kit/framework/bin/lib/init.cjs +1442 -1442
  91. package/kit/framework/bin/lib/milestone.cjs +252 -252
  92. package/kit/framework/bin/lib/model-profiles.cjs +68 -68
  93. package/kit/framework/bin/lib/phase.cjs +888 -888
  94. package/kit/framework/bin/lib/profile-output.cjs +952 -952
  95. package/kit/framework/bin/lib/profile-pipeline.cjs +539 -539
  96. package/kit/framework/bin/lib/roadmap.cjs +329 -329
  97. package/kit/framework/bin/lib/security.cjs +382 -382
  98. package/kit/framework/bin/lib/state.cjs +1031 -1031
  99. package/kit/framework/bin/lib/template.cjs +222 -222
  100. package/kit/framework/bin/lib/uat.cjs +282 -282
  101. package/kit/framework/bin/lib/verify.cjs +888 -888
  102. package/kit/framework/bin/lib/workstream.cjs +491 -491
  103. package/kit/framework/bin/tools.cjs +918 -918
  104. package/kit/framework/commands/workstreams.md +63 -63
  105. package/kit/framework/references/checkpoints.md +778 -778
  106. package/kit/framework/references/continuation-format.md +249 -249
  107. package/kit/framework/references/decimal-phase-calculation.md +64 -64
  108. package/kit/framework/references/git-integration.md +295 -295
  109. package/kit/framework/references/git-planning-commit.md +38 -38
  110. package/kit/framework/references/model-profile-resolution.md +36 -36
  111. package/kit/framework/references/model-profiles.md +139 -139
  112. package/kit/framework/references/phase-argument-parsing.md +61 -61
  113. package/kit/framework/references/planning-config.md +202 -202
  114. package/kit/framework/references/questioning.md +162 -162
  115. package/kit/framework/references/tdd.md +263 -263
  116. package/kit/framework/references/ui-brand.md +160 -160
  117. package/kit/framework/references/user-profiling.md +657 -657
  118. package/kit/framework/references/verification-patterns.md +612 -612
  119. package/kit/framework/references/workstream-flag.md +58 -58
  120. package/kit/framework/templates/DEBUG.md +164 -164
  121. package/kit/framework/templates/UAT.md +265 -265
  122. package/kit/framework/templates/UI-SPEC.md +100 -100
  123. package/kit/framework/templates/VALIDATION.md +76 -76
  124. package/kit/framework/templates/claude-md.md +122 -122
  125. package/kit/framework/templates/codebase/architecture.md +185 -185
  126. package/kit/framework/templates/codebase/concerns.md +205 -205
  127. package/kit/framework/templates/codebase/conventions.md +204 -204
  128. package/kit/framework/templates/codebase/integrations.md +192 -192
  129. package/kit/framework/templates/codebase/stack.md +158 -158
  130. package/kit/framework/templates/codebase/structure.md +199 -199
  131. package/kit/framework/templates/codebase/testing.md +301 -301
  132. package/kit/framework/templates/config.json +44 -44
  133. package/kit/framework/templates/context.md +352 -352
  134. package/kit/framework/templates/continue-here.md +78 -78
  135. package/kit/framework/templates/copilot-instructions.md +7 -7
  136. package/kit/framework/templates/debug-subagent-prompt.md +91 -91
  137. package/kit/framework/templates/dev-preferences.md +20 -20
  138. package/kit/framework/templates/discovery.md +146 -146
  139. package/kit/framework/templates/discussion-log.md +63 -63
  140. package/kit/framework/templates/milestone-archive.md +123 -123
  141. package/kit/framework/templates/milestone.md +115 -115
  142. package/kit/framework/templates/phase-prompt.md +610 -610
  143. package/kit/framework/templates/planner-subagent-prompt.md +117 -117
  144. package/kit/framework/templates/project.md +186 -186
  145. package/kit/framework/templates/requirements.md +231 -231
  146. package/kit/framework/templates/research-project/ARCHITECTURE.md +204 -204
  147. package/kit/framework/templates/research-project/FEATURES.md +147 -147
  148. package/kit/framework/templates/research-project/PITFALLS.md +200 -200
  149. package/kit/framework/templates/research-project/STACK.md +120 -120
  150. package/kit/framework/templates/research-project/SUMMARY.md +170 -170
  151. package/kit/framework/templates/research.md +419 -419
  152. package/kit/framework/templates/retrospective.md +54 -54
  153. package/kit/framework/templates/roadmap.md +202 -202
  154. package/kit/framework/templates/state.md +176 -176
  155. package/kit/framework/templates/summary-complex.md +59 -59
  156. package/kit/framework/templates/summary-minimal.md +41 -41
  157. package/kit/framework/templates/summary-standard.md +48 -48
  158. package/kit/framework/templates/summary.md +209 -209
  159. package/kit/framework/templates/user-profile.md +146 -146
  160. package/kit/framework/templates/user-setup.md +256 -256
  161. package/kit/framework/templates/verification-report.md +258 -258
  162. package/kit/framework/workflows/add-phase.md +112 -112
  163. package/kit/framework/workflows/add-tests.md +351 -351
  164. package/kit/framework/workflows/add-todo.md +158 -158
  165. package/kit/framework/workflows/audit-milestone.md +340 -340
  166. package/kit/framework/workflows/audit-uat.md +109 -109
  167. package/kit/framework/workflows/autonomous.md +891 -891
  168. package/kit/framework/workflows/check-todos.md +177 -177
  169. package/kit/framework/workflows/cleanup.md +152 -152
  170. package/kit/framework/workflows/complete-milestone.md +696 -696
  171. package/kit/framework/workflows/diagnose-issues.md +231 -231
  172. package/kit/framework/workflows/discovery-phase.md +289 -289
  173. package/kit/framework/workflows/discuss-phase-assumptions.md +653 -653
  174. package/kit/framework/workflows/discuss-phase.md +784 -784
  175. package/kit/framework/workflows/do.md +104 -104
  176. package/kit/framework/workflows/execute-phase.md +838 -838
  177. package/kit/framework/workflows/execute-plan.md +510 -510
  178. package/kit/framework/workflows/fast.md +102 -102
  179. package/kit/framework/workflows/forensics.md +265 -265
  180. package/kit/framework/workflows/health.md +181 -181
  181. package/kit/framework/workflows/help.md +619 -619
  182. package/kit/framework/workflows/insert-phase.md +130 -130
  183. package/kit/framework/workflows/list-phase-assumptions.md +178 -178
  184. package/kit/framework/workflows/list-workspaces.md +56 -56
  185. package/kit/framework/workflows/manager.md +362 -362
  186. package/kit/framework/workflows/map-codebase.md +377 -377
  187. package/kit/framework/workflows/milestone-summary.md +223 -223
  188. package/kit/framework/workflows/new-milestone.md +486 -486
  189. package/kit/framework/workflows/new-project.md +1159 -1159
  190. package/kit/framework/workflows/new-workspace.md +237 -237
  191. package/kit/framework/workflows/next.md +97 -97
  192. package/kit/framework/workflows/node-repair.md +92 -92
  193. package/kit/framework/workflows/note.md +156 -156
  194. package/kit/framework/workflows/pause-work.md +176 -176
  195. package/kit/framework/workflows/plan-milestone-gaps.md +273 -273
  196. package/kit/framework/workflows/plan-phase.md +765 -765
  197. package/kit/framework/workflows/plant-seed.md +169 -169
  198. package/kit/framework/workflows/pr-branch.md +129 -129
  199. package/kit/framework/workflows/profile-user.md +450 -450
  200. package/kit/framework/workflows/progress.md +507 -507
  201. package/kit/framework/workflows/quick.md +757 -757
  202. package/kit/framework/workflows/remove-phase.md +155 -155
  203. package/kit/framework/workflows/remove-workspace.md +90 -90
  204. package/kit/framework/workflows/research-phase.md +82 -82
  205. package/kit/framework/workflows/resume-project.md +326 -326
  206. package/kit/framework/workflows/review.md +228 -228
  207. package/kit/framework/workflows/session-report.md +146 -146
  208. package/kit/framework/workflows/settings.md +283 -283
  209. package/kit/framework/workflows/ship.md +228 -228
  210. package/kit/framework/workflows/stats.md +60 -60
  211. package/kit/framework/workflows/transition.md +671 -671
  212. package/kit/framework/workflows/ui-phase.md +302 -302
  213. package/kit/framework/workflows/ui-review.md +165 -165
  214. package/kit/framework/workflows/update.md +323 -323
  215. package/kit/framework/workflows/validate-phase.md +174 -174
  216. package/kit/framework/workflows/verify-phase.md +252 -252
  217. package/kit/framework/workflows/verify-work.md +637 -637
  218. package/kit/hooks/check-update.js +118 -118
  219. package/kit/hooks/context-monitor.js +163 -163
  220. package/kit/hooks/prompt-guard.js +103 -103
  221. package/kit/hooks/statusline.js +125 -125
  222. package/kit/hooks/workflow-guard.js +101 -101
  223. package/kit/settings.json +45 -45
  224. package/kit/skills/example-skill/SKILL.md +42 -42
  225. package/package.json +63 -59
  226. package/src/core/kit.js +216 -216
  227. package/src/core/reflect.js +247 -247
  228. package/src/core/reverse-sync.js +372 -372
  229. package/src/core/sync.js +418 -418
  230. package/src/core/watch.js +121 -121
  231. package/src/mcp-server/index.js +34 -3
@@ -1,491 +1,491 @@
1
- /**
2
- * Workstream — CRUD operations for workstream namespacing
3
- *
4
- * Workstreams enable parallel milestones by scoping ROADMAP.md, STATE.md,
5
- * REQUIREMENTS.md, and phases/ into .planning/workstreams/{name}/ directories.
6
- *
7
- * When no workstreams/ directory exists, framework operates in "flat mode" with
8
- * everything at .planning/ — backward compatible with pre-workstream installs.
9
- */
10
-
11
- const fs = require('fs');
12
- const path = require('path');
13
- const { output, error, planningPaths, planningRoot, toPosixPath, getMilestoneInfo, generateSlugInternal, setActiveWorkstream, getActiveWorkstream, filterPlanFiles, filterSummaryFiles, readSubdirectories } = require('./core.cjs');
14
- const { stateExtractField } = require('./state.cjs');
15
-
16
- // ─── Migration ──────────────────────────────────────────────────────────────
17
-
18
- /**
19
- * Migrate flat .planning/ layout to workstream mode.
20
- * Moves per-workstream files (ROADMAP.md, STATE.md, REQUIREMENTS.md, phases/)
21
- * into .planning/workstreams/{name}/. Shared files (PROJECT.md, config.json,
22
- * milestones/, research/, codebase/, todos/) stay in place.
23
- */
24
- function migrateToWorkstreams(cwd, workstreamName) {
25
- if (!workstreamName || /[/\\]/.test(workstreamName) || workstreamName === '.' || workstreamName === '..') {
26
- throw new Error('Invalid workstream name for migration');
27
- }
28
-
29
- const baseDir = planningRoot(cwd);
30
- const wsDir = path.join(baseDir, 'workstreams', workstreamName);
31
-
32
- if (fs.existsSync(path.join(baseDir, 'workstreams'))) {
33
- throw new Error('Already in workstream mode — .planning/workstreams/ exists');
34
- }
35
-
36
- const toMove = [
37
- { name: 'ROADMAP.md', type: 'file' },
38
- { name: 'STATE.md', type: 'file' },
39
- { name: 'REQUIREMENTS.md', type: 'file' },
40
- { name: 'phases', type: 'dir' },
41
- ];
42
-
43
- fs.mkdirSync(wsDir, { recursive: true });
44
-
45
- const filesMoved = [];
46
- try {
47
- for (const item of toMove) {
48
- const src = path.join(baseDir, item.name);
49
- if (fs.existsSync(src)) {
50
- const dest = path.join(wsDir, item.name);
51
- fs.renameSync(src, dest);
52
- filesMoved.push(item.name);
53
- }
54
- }
55
- } catch (err) {
56
- for (const name of filesMoved) {
57
- try { fs.renameSync(path.join(wsDir, name), path.join(baseDir, name)); } catch {}
58
- }
59
- try { fs.rmSync(wsDir, { recursive: true }); } catch {}
60
- try { fs.rmdirSync(path.join(baseDir, 'workstreams')); } catch {}
61
- throw err;
62
- }
63
-
64
- return { migrated: true, workstream: workstreamName, files_moved: filesMoved };
65
- }
66
-
67
- // ─── CRUD Commands ──────────────────────────────────────────────────────────
68
-
69
- function cmdWorkstreamCreate(cwd, name, options, raw) {
70
- if (!name) {
71
- error('workstream name required. Usage: workstream create <name>');
72
- }
73
-
74
- const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
75
- if (!slug) {
76
- error('Invalid workstream name — must contain at least one alphanumeric character');
77
- }
78
-
79
- const baseDir = planningRoot(cwd);
80
- if (!fs.existsSync(baseDir)) {
81
- error('.planning/ directory not found — run /new-project first');
82
- }
83
-
84
- const wsRoot = path.join(baseDir, 'workstreams');
85
- const wsDir = path.join(wsRoot, slug);
86
-
87
- if (fs.existsSync(wsDir) && fs.existsSync(path.join(wsDir, 'STATE.md'))) {
88
- output({ created: false, error: 'already_exists', workstream: slug, path: toPosixPath(path.relative(cwd, wsDir)) }, raw);
89
- return;
90
- }
91
-
92
- const isFlatMode = !fs.existsSync(wsRoot);
93
- let migration = null;
94
- if (isFlatMode && options.migrate !== false) {
95
- const hasExistingWork = fs.existsSync(path.join(baseDir, 'ROADMAP.md')) ||
96
- fs.existsSync(path.join(baseDir, 'STATE.md')) ||
97
- fs.existsSync(path.join(baseDir, 'phases'));
98
-
99
- if (hasExistingWork) {
100
- const migrateName = options.migrateName || null;
101
- let existingWsName;
102
- if (migrateName) {
103
- existingWsName = migrateName;
104
- } else {
105
- try {
106
- const milestone = getMilestoneInfo(cwd);
107
- existingWsName = generateSlugInternal(milestone.name) || 'default';
108
- } catch {
109
- existingWsName = 'default';
110
- }
111
- }
112
-
113
- try {
114
- migration = migrateToWorkstreams(cwd, existingWsName);
115
- } catch (e) {
116
- output({ created: false, error: 'migration_failed', message: e.message }, raw);
117
- return;
118
- }
119
- } else {
120
- fs.mkdirSync(wsRoot, { recursive: true });
121
- }
122
- }
123
-
124
- fs.mkdirSync(wsDir, { recursive: true });
125
- fs.mkdirSync(path.join(wsDir, 'phases'), { recursive: true });
126
-
127
- const today = new Date().toISOString().split('T')[0];
128
- const stateContent = [
129
- '---',
130
- `workstream: ${slug}`,
131
- `created: ${today}`,
132
- '---',
133
- '',
134
- '# Project State',
135
- '',
136
- '## Current Position',
137
- '**Status:** Not started',
138
- '**Current Phase:** None',
139
- `**Last Activity:** ${today}`,
140
- '**Last Activity Description:** Workstream created',
141
- '',
142
- '## Progress',
143
- '**Phases Complete:** 0',
144
- '**Current Plan:** N/A',
145
- '',
146
- '## Session Continuity',
147
- '**Stopped At:** N/A',
148
- '**Resume File:** None',
149
- '',
150
- ].join('\n');
151
-
152
- const statePath = path.join(wsDir, 'STATE.md');
153
- if (!fs.existsSync(statePath)) {
154
- fs.writeFileSync(statePath, stateContent, 'utf-8');
155
- }
156
-
157
- setActiveWorkstream(cwd, slug);
158
-
159
- const relPath = toPosixPath(path.relative(cwd, wsDir));
160
- output({
161
- created: true,
162
- workstream: slug,
163
- path: relPath,
164
- state_path: relPath + '/STATE.md',
165
- phases_path: relPath + '/phases',
166
- migration: migration || null,
167
- active: true,
168
- }, raw);
169
- }
170
-
171
- function cmdWorkstreamList(cwd, raw) {
172
- const wsRoot = path.join(planningRoot(cwd), 'workstreams');
173
-
174
- if (!fs.existsSync(wsRoot)) {
175
- output({ mode: 'flat', workstreams: [], message: 'No workstreams — operating in flat mode' }, raw);
176
- return;
177
- }
178
-
179
- const entries = fs.readdirSync(wsRoot, { withFileTypes: true });
180
- const workstreams = [];
181
-
182
- for (const entry of entries) {
183
- if (!entry.isDirectory()) continue;
184
-
185
- const wsDir = path.join(wsRoot, entry.name);
186
- const phasesDir = path.join(wsDir, 'phases');
187
-
188
- const phaseDirs = readSubdirectories(phasesDir);
189
- const phaseCount = phaseDirs.length;
190
- let completedCount = 0;
191
- for (const d of phaseDirs) {
192
- try {
193
- const phaseFiles = fs.readdirSync(path.join(phasesDir, d));
194
- const plans = filterPlanFiles(phaseFiles);
195
- const summaries = filterSummaryFiles(phaseFiles);
196
- if (plans.length > 0 && summaries.length >= plans.length) completedCount++;
197
- } catch {}
198
- }
199
-
200
- let status = 'unknown', currentPhase = null;
201
- try {
202
- const stateContent = fs.readFileSync(path.join(wsDir, 'STATE.md'), 'utf-8');
203
- status = stateExtractField(stateContent, 'Status') || 'unknown';
204
- currentPhase = stateExtractField(stateContent, 'Current Phase');
205
- } catch {}
206
-
207
- workstreams.push({
208
- name: entry.name,
209
- path: toPosixPath(path.relative(cwd, wsDir)),
210
- has_roadmap: fs.existsSync(path.join(wsDir, 'ROADMAP.md')),
211
- has_state: fs.existsSync(path.join(wsDir, 'STATE.md')),
212
- status,
213
- current_phase: currentPhase,
214
- phase_count: phaseCount,
215
- completed_phases: completedCount,
216
- });
217
- }
218
-
219
- output({ mode: 'workstream', workstreams, count: workstreams.length }, raw);
220
- }
221
-
222
- function cmdWorkstreamStatus(cwd, name, raw) {
223
- if (!name) error('workstream name required. Usage: workstream status <name>');
224
- if (/[/\\]/.test(name) || name === '.' || name === '..') error('Invalid workstream name');
225
-
226
- const wsDir = path.join(planningRoot(cwd), 'workstreams', name);
227
- if (!fs.existsSync(wsDir)) {
228
- output({ found: false, workstream: name }, raw);
229
- return;
230
- }
231
-
232
- const p = planningPaths(cwd, name);
233
- const relPath = toPosixPath(path.relative(cwd, wsDir));
234
-
235
- const files = {
236
- roadmap: fs.existsSync(p.roadmap),
237
- state: fs.existsSync(p.state),
238
- requirements: fs.existsSync(p.requirements),
239
- };
240
-
241
- const phases = [];
242
- for (const dir of readSubdirectories(p.phases).sort()) {
243
- try {
244
- const phaseFiles = fs.readdirSync(path.join(p.phases, dir));
245
- const plans = filterPlanFiles(phaseFiles);
246
- const summaries = filterSummaryFiles(phaseFiles);
247
- phases.push({
248
- directory: dir,
249
- status: summaries.length >= plans.length && plans.length > 0 ? 'complete' :
250
- plans.length > 0 ? 'in_progress' : 'pending',
251
- plan_count: plans.length,
252
- summary_count: summaries.length,
253
- });
254
- } catch {}
255
- }
256
-
257
- let stateInfo = {};
258
- try {
259
- const stateContent = fs.readFileSync(p.state, 'utf-8');
260
- stateInfo = {
261
- status: stateExtractField(stateContent, 'Status') || 'unknown',
262
- current_phase: stateExtractField(stateContent, 'Current Phase'),
263
- last_activity: stateExtractField(stateContent, 'Last Activity'),
264
- };
265
- } catch {}
266
-
267
- output({
268
- found: true,
269
- workstream: name,
270
- path: relPath,
271
- files,
272
- phases,
273
- phase_count: phases.length,
274
- completed_phases: phases.filter(ph => ph.status === 'complete').length,
275
- ...stateInfo,
276
- }, raw);
277
- }
278
-
279
- function cmdWorkstreamComplete(cwd, name, options, raw) {
280
- if (!name) error('workstream name required. Usage: workstream complete <name>');
281
- if (/[/\\]/.test(name) || name === '.' || name === '..') error('Invalid workstream name');
282
-
283
- const root = planningRoot(cwd);
284
- const wsRoot = path.join(root, 'workstreams');
285
- const wsDir = path.join(wsRoot, name);
286
-
287
- if (!fs.existsSync(wsDir)) {
288
- output({ completed: false, error: 'not_found', workstream: name }, raw);
289
- return;
290
- }
291
-
292
- const active = getActiveWorkstream(cwd);
293
- if (active === name) setActiveWorkstream(cwd, null);
294
-
295
- const archiveDir = path.join(root, 'milestones');
296
- const today = new Date().toISOString().split('T')[0];
297
- let archivePath = path.join(archiveDir, `ws-${name}-${today}`);
298
- let suffix = 1;
299
- while (fs.existsSync(archivePath)) {
300
- archivePath = path.join(archiveDir, `ws-${name}-${today}-${suffix++}`);
301
- }
302
-
303
- fs.mkdirSync(archivePath, { recursive: true });
304
-
305
- const filesMoved = [];
306
- try {
307
- const entries = fs.readdirSync(wsDir, { withFileTypes: true });
308
- for (const entry of entries) {
309
- fs.renameSync(path.join(wsDir, entry.name), path.join(archivePath, entry.name));
310
- filesMoved.push(entry.name);
311
- }
312
- } catch (err) {
313
- for (const fname of filesMoved) {
314
- try { fs.renameSync(path.join(archivePath, fname), path.join(wsDir, fname)); } catch {}
315
- }
316
- try { fs.rmSync(archivePath, { recursive: true }); } catch {}
317
- if (active === name) setActiveWorkstream(cwd, name);
318
- output({ completed: false, error: 'archive_failed', message: err.message, workstream: name }, raw);
319
- return;
320
- }
321
-
322
- try { fs.rmdirSync(wsDir); } catch {}
323
-
324
- let remainingWs = 0;
325
- try {
326
- remainingWs = fs.readdirSync(wsRoot, { withFileTypes: true }).filter(e => e.isDirectory()).length;
327
- if (remainingWs === 0) fs.rmdirSync(wsRoot);
328
- } catch {}
329
-
330
- output({
331
- completed: true,
332
- workstream: name,
333
- archived_to: toPosixPath(path.relative(cwd, archivePath)),
334
- remaining_workstreams: remainingWs,
335
- reverted_to_flat: remainingWs === 0,
336
- }, raw);
337
- }
338
-
339
- // ─── Active Workstream Commands ──────────────────────────────────────────────
340
-
341
- function cmdWorkstreamSet(cwd, name, raw) {
342
- if (!name) {
343
- setActiveWorkstream(cwd, null);
344
- output({ active: null, cleared: true }, raw);
345
- return;
346
- }
347
-
348
- if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
349
- output({ active: null, error: 'invalid_name', message: 'Workstream name must be alphanumeric, hyphens, and underscores only' }, raw);
350
- return;
351
- }
352
-
353
- const wsDir = path.join(planningRoot(cwd), 'workstreams', name);
354
- if (!fs.existsSync(wsDir)) {
355
- output({ active: null, error: 'not_found', workstream: name }, raw);
356
- return;
357
- }
358
-
359
- setActiveWorkstream(cwd, name);
360
- output({ active: name, set: true }, raw, name);
361
- }
362
-
363
- function cmdWorkstreamGet(cwd, raw) {
364
- const active = getActiveWorkstream(cwd);
365
- const wsRoot = path.join(planningRoot(cwd), 'workstreams');
366
- output({ active, mode: fs.existsSync(wsRoot) ? 'workstream' : 'flat' }, raw, active || 'none');
367
- }
368
-
369
- function cmdWorkstreamProgress(cwd, raw) {
370
- const root = planningRoot(cwd);
371
- const wsRoot = path.join(root, 'workstreams');
372
-
373
- if (!fs.existsSync(wsRoot)) {
374
- output({ mode: 'flat', workstreams: [], message: 'No workstreams — operating in flat mode' }, raw);
375
- return;
376
- }
377
-
378
- const active = getActiveWorkstream(cwd);
379
- const entries = fs.readdirSync(wsRoot, { withFileTypes: true });
380
- const workstreams = [];
381
-
382
- for (const entry of entries) {
383
- if (!entry.isDirectory()) continue;
384
-
385
- const wsDir = path.join(wsRoot, entry.name);
386
- const phasesDir = path.join(wsDir, 'phases');
387
-
388
- const phaseDirsProgress = readSubdirectories(phasesDir);
389
- const phaseCount = phaseDirsProgress.length;
390
- let completedCount = 0, totalPlans = 0, completedPlans = 0;
391
- for (const d of phaseDirsProgress) {
392
- try {
393
- const phaseFiles = fs.readdirSync(path.join(phasesDir, d));
394
- const plans = filterPlanFiles(phaseFiles);
395
- const summaries = filterSummaryFiles(phaseFiles);
396
- totalPlans += plans.length;
397
- completedPlans += Math.min(summaries.length, plans.length);
398
- if (plans.length > 0 && summaries.length >= plans.length) completedCount++;
399
- } catch {}
400
- }
401
-
402
- let roadmapPhaseCount = phaseCount;
403
- try {
404
- const roadmapContent = fs.readFileSync(path.join(wsDir, 'ROADMAP.md'), 'utf-8');
405
- const phaseMatches = roadmapContent.match(/^###?\s+Phase\s+\d/gm);
406
- if (phaseMatches) roadmapPhaseCount = phaseMatches.length;
407
- } catch {}
408
-
409
- let status = 'unknown', currentPhase = null;
410
- try {
411
- const stateContent = fs.readFileSync(path.join(wsDir, 'STATE.md'), 'utf-8');
412
- status = stateExtractField(stateContent, 'Status') || 'unknown';
413
- currentPhase = stateExtractField(stateContent, 'Current Phase');
414
- } catch {}
415
-
416
- workstreams.push({
417
- name: entry.name,
418
- active: entry.name === active,
419
- status,
420
- current_phase: currentPhase,
421
- phases: `${completedCount}/${roadmapPhaseCount}`,
422
- plans: `${completedPlans}/${totalPlans}`,
423
- progress_percent: roadmapPhaseCount > 0 ? Math.round((completedCount / roadmapPhaseCount) * 100) : 0,
424
- });
425
- }
426
-
427
- output({ mode: 'workstream', active, workstreams, count: workstreams.length }, raw);
428
- }
429
-
430
- // ─── Collision Detection ────────────────────────────────────────────────────
431
-
432
- /**
433
- * Return other workstreams that are NOT complete.
434
- * Used to detect whether the milestone has active parallel work
435
- * when a workstream finishes its last phase.
436
- */
437
- function getOtherActiveWorkstreams(cwd, excludeWs) {
438
- const wsRoot = path.join(planningRoot(cwd), 'workstreams');
439
- if (!fs.existsSync(wsRoot)) return [];
440
-
441
- const entries = fs.readdirSync(wsRoot, { withFileTypes: true });
442
- const others = [];
443
-
444
- for (const entry of entries) {
445
- if (!entry.isDirectory() || entry.name === excludeWs) continue;
446
-
447
- const wsDir = path.join(wsRoot, entry.name);
448
- const statePath = path.join(wsDir, 'STATE.md');
449
-
450
- let status = 'unknown', currentPhase = null;
451
- try {
452
- const content = fs.readFileSync(statePath, 'utf-8');
453
- status = stateExtractField(content, 'Status') || 'unknown';
454
- currentPhase = stateExtractField(content, 'Current Phase');
455
- } catch {}
456
-
457
- if (status.toLowerCase().includes('milestone complete') ||
458
- status.toLowerCase().includes('archived')) {
459
- continue;
460
- }
461
-
462
- const phasesDir = path.join(wsDir, 'phases');
463
- const phaseDirsOther = readSubdirectories(phasesDir);
464
- const phaseCount = phaseDirsOther.length;
465
- let completedCount = 0;
466
- for (const d of phaseDirsOther) {
467
- try {
468
- const phaseFiles = fs.readdirSync(path.join(phasesDir, d));
469
- const plans = filterPlanFiles(phaseFiles);
470
- const summaries = filterSummaryFiles(phaseFiles);
471
- if (plans.length > 0 && summaries.length >= plans.length) completedCount++;
472
- } catch {}
473
- }
474
-
475
- others.push({ name: entry.name, status, current_phase: currentPhase, phases: `${completedCount}/${phaseCount}` });
476
- }
477
-
478
- return others;
479
- }
480
-
481
- module.exports = {
482
- migrateToWorkstreams,
483
- cmdWorkstreamCreate,
484
- cmdWorkstreamList,
485
- cmdWorkstreamStatus,
486
- cmdWorkstreamComplete,
487
- cmdWorkstreamSet,
488
- cmdWorkstreamGet,
489
- cmdWorkstreamProgress,
490
- getOtherActiveWorkstreams,
491
- };
1
+ /**
2
+ * Workstream — CRUD operations for workstream namespacing
3
+ *
4
+ * Workstreams enable parallel milestones by scoping ROADMAP.md, STATE.md,
5
+ * REQUIREMENTS.md, and phases/ into .planning/workstreams/{name}/ directories.
6
+ *
7
+ * When no workstreams/ directory exists, framework operates in "flat mode" with
8
+ * everything at .planning/ — backward compatible with pre-workstream installs.
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { output, error, planningPaths, planningRoot, toPosixPath, getMilestoneInfo, generateSlugInternal, setActiveWorkstream, getActiveWorkstream, filterPlanFiles, filterSummaryFiles, readSubdirectories } = require('./core.cjs');
14
+ const { stateExtractField } = require('./state.cjs');
15
+
16
+ // ─── Migration ──────────────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Migrate flat .planning/ layout to workstream mode.
20
+ * Moves per-workstream files (ROADMAP.md, STATE.md, REQUIREMENTS.md, phases/)
21
+ * into .planning/workstreams/{name}/. Shared files (PROJECT.md, config.json,
22
+ * milestones/, research/, codebase/, todos/) stay in place.
23
+ */
24
+ function migrateToWorkstreams(cwd, workstreamName) {
25
+ if (!workstreamName || /[/\\]/.test(workstreamName) || workstreamName === '.' || workstreamName === '..') {
26
+ throw new Error('Invalid workstream name for migration');
27
+ }
28
+
29
+ const baseDir = planningRoot(cwd);
30
+ const wsDir = path.join(baseDir, 'workstreams', workstreamName);
31
+
32
+ if (fs.existsSync(path.join(baseDir, 'workstreams'))) {
33
+ throw new Error('Already in workstream mode — .planning/workstreams/ exists');
34
+ }
35
+
36
+ const toMove = [
37
+ { name: 'ROADMAP.md', type: 'file' },
38
+ { name: 'STATE.md', type: 'file' },
39
+ { name: 'REQUIREMENTS.md', type: 'file' },
40
+ { name: 'phases', type: 'dir' },
41
+ ];
42
+
43
+ fs.mkdirSync(wsDir, { recursive: true });
44
+
45
+ const filesMoved = [];
46
+ try {
47
+ for (const item of toMove) {
48
+ const src = path.join(baseDir, item.name);
49
+ if (fs.existsSync(src)) {
50
+ const dest = path.join(wsDir, item.name);
51
+ fs.renameSync(src, dest);
52
+ filesMoved.push(item.name);
53
+ }
54
+ }
55
+ } catch (err) {
56
+ for (const name of filesMoved) {
57
+ try { fs.renameSync(path.join(wsDir, name), path.join(baseDir, name)); } catch {}
58
+ }
59
+ try { fs.rmSync(wsDir, { recursive: true }); } catch {}
60
+ try { fs.rmdirSync(path.join(baseDir, 'workstreams')); } catch {}
61
+ throw err;
62
+ }
63
+
64
+ return { migrated: true, workstream: workstreamName, files_moved: filesMoved };
65
+ }
66
+
67
+ // ─── CRUD Commands ──────────────────────────────────────────────────────────
68
+
69
+ function cmdWorkstreamCreate(cwd, name, options, raw) {
70
+ if (!name) {
71
+ error('workstream name required. Usage: workstream create <name>');
72
+ }
73
+
74
+ const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
75
+ if (!slug) {
76
+ error('Invalid workstream name — must contain at least one alphanumeric character');
77
+ }
78
+
79
+ const baseDir = planningRoot(cwd);
80
+ if (!fs.existsSync(baseDir)) {
81
+ error('.planning/ directory not found — run /new-project first');
82
+ }
83
+
84
+ const wsRoot = path.join(baseDir, 'workstreams');
85
+ const wsDir = path.join(wsRoot, slug);
86
+
87
+ if (fs.existsSync(wsDir) && fs.existsSync(path.join(wsDir, 'STATE.md'))) {
88
+ output({ created: false, error: 'already_exists', workstream: slug, path: toPosixPath(path.relative(cwd, wsDir)) }, raw);
89
+ return;
90
+ }
91
+
92
+ const isFlatMode = !fs.existsSync(wsRoot);
93
+ let migration = null;
94
+ if (isFlatMode && options.migrate !== false) {
95
+ const hasExistingWork = fs.existsSync(path.join(baseDir, 'ROADMAP.md')) ||
96
+ fs.existsSync(path.join(baseDir, 'STATE.md')) ||
97
+ fs.existsSync(path.join(baseDir, 'phases'));
98
+
99
+ if (hasExistingWork) {
100
+ const migrateName = options.migrateName || null;
101
+ let existingWsName;
102
+ if (migrateName) {
103
+ existingWsName = migrateName;
104
+ } else {
105
+ try {
106
+ const milestone = getMilestoneInfo(cwd);
107
+ existingWsName = generateSlugInternal(milestone.name) || 'default';
108
+ } catch {
109
+ existingWsName = 'default';
110
+ }
111
+ }
112
+
113
+ try {
114
+ migration = migrateToWorkstreams(cwd, existingWsName);
115
+ } catch (e) {
116
+ output({ created: false, error: 'migration_failed', message: e.message }, raw);
117
+ return;
118
+ }
119
+ } else {
120
+ fs.mkdirSync(wsRoot, { recursive: true });
121
+ }
122
+ }
123
+
124
+ fs.mkdirSync(wsDir, { recursive: true });
125
+ fs.mkdirSync(path.join(wsDir, 'phases'), { recursive: true });
126
+
127
+ const today = new Date().toISOString().split('T')[0];
128
+ const stateContent = [
129
+ '---',
130
+ `workstream: ${slug}`,
131
+ `created: ${today}`,
132
+ '---',
133
+ '',
134
+ '# Project State',
135
+ '',
136
+ '## Current Position',
137
+ '**Status:** Not started',
138
+ '**Current Phase:** None',
139
+ `**Last Activity:** ${today}`,
140
+ '**Last Activity Description:** Workstream created',
141
+ '',
142
+ '## Progress',
143
+ '**Phases Complete:** 0',
144
+ '**Current Plan:** N/A',
145
+ '',
146
+ '## Session Continuity',
147
+ '**Stopped At:** N/A',
148
+ '**Resume File:** None',
149
+ '',
150
+ ].join('\n');
151
+
152
+ const statePath = path.join(wsDir, 'STATE.md');
153
+ if (!fs.existsSync(statePath)) {
154
+ fs.writeFileSync(statePath, stateContent, 'utf-8');
155
+ }
156
+
157
+ setActiveWorkstream(cwd, slug);
158
+
159
+ const relPath = toPosixPath(path.relative(cwd, wsDir));
160
+ output({
161
+ created: true,
162
+ workstream: slug,
163
+ path: relPath,
164
+ state_path: relPath + '/STATE.md',
165
+ phases_path: relPath + '/phases',
166
+ migration: migration || null,
167
+ active: true,
168
+ }, raw);
169
+ }
170
+
171
+ function cmdWorkstreamList(cwd, raw) {
172
+ const wsRoot = path.join(planningRoot(cwd), 'workstreams');
173
+
174
+ if (!fs.existsSync(wsRoot)) {
175
+ output({ mode: 'flat', workstreams: [], message: 'No workstreams — operating in flat mode' }, raw);
176
+ return;
177
+ }
178
+
179
+ const entries = fs.readdirSync(wsRoot, { withFileTypes: true });
180
+ const workstreams = [];
181
+
182
+ for (const entry of entries) {
183
+ if (!entry.isDirectory()) continue;
184
+
185
+ const wsDir = path.join(wsRoot, entry.name);
186
+ const phasesDir = path.join(wsDir, 'phases');
187
+
188
+ const phaseDirs = readSubdirectories(phasesDir);
189
+ const phaseCount = phaseDirs.length;
190
+ let completedCount = 0;
191
+ for (const d of phaseDirs) {
192
+ try {
193
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, d));
194
+ const plans = filterPlanFiles(phaseFiles);
195
+ const summaries = filterSummaryFiles(phaseFiles);
196
+ if (plans.length > 0 && summaries.length >= plans.length) completedCount++;
197
+ } catch {}
198
+ }
199
+
200
+ let status = 'unknown', currentPhase = null;
201
+ try {
202
+ const stateContent = fs.readFileSync(path.join(wsDir, 'STATE.md'), 'utf-8');
203
+ status = stateExtractField(stateContent, 'Status') || 'unknown';
204
+ currentPhase = stateExtractField(stateContent, 'Current Phase');
205
+ } catch {}
206
+
207
+ workstreams.push({
208
+ name: entry.name,
209
+ path: toPosixPath(path.relative(cwd, wsDir)),
210
+ has_roadmap: fs.existsSync(path.join(wsDir, 'ROADMAP.md')),
211
+ has_state: fs.existsSync(path.join(wsDir, 'STATE.md')),
212
+ status,
213
+ current_phase: currentPhase,
214
+ phase_count: phaseCount,
215
+ completed_phases: completedCount,
216
+ });
217
+ }
218
+
219
+ output({ mode: 'workstream', workstreams, count: workstreams.length }, raw);
220
+ }
221
+
222
+ function cmdWorkstreamStatus(cwd, name, raw) {
223
+ if (!name) error('workstream name required. Usage: workstream status <name>');
224
+ if (/[/\\]/.test(name) || name === '.' || name === '..') error('Invalid workstream name');
225
+
226
+ const wsDir = path.join(planningRoot(cwd), 'workstreams', name);
227
+ if (!fs.existsSync(wsDir)) {
228
+ output({ found: false, workstream: name }, raw);
229
+ return;
230
+ }
231
+
232
+ const p = planningPaths(cwd, name);
233
+ const relPath = toPosixPath(path.relative(cwd, wsDir));
234
+
235
+ const files = {
236
+ roadmap: fs.existsSync(p.roadmap),
237
+ state: fs.existsSync(p.state),
238
+ requirements: fs.existsSync(p.requirements),
239
+ };
240
+
241
+ const phases = [];
242
+ for (const dir of readSubdirectories(p.phases).sort()) {
243
+ try {
244
+ const phaseFiles = fs.readdirSync(path.join(p.phases, dir));
245
+ const plans = filterPlanFiles(phaseFiles);
246
+ const summaries = filterSummaryFiles(phaseFiles);
247
+ phases.push({
248
+ directory: dir,
249
+ status: summaries.length >= plans.length && plans.length > 0 ? 'complete' :
250
+ plans.length > 0 ? 'in_progress' : 'pending',
251
+ plan_count: plans.length,
252
+ summary_count: summaries.length,
253
+ });
254
+ } catch {}
255
+ }
256
+
257
+ let stateInfo = {};
258
+ try {
259
+ const stateContent = fs.readFileSync(p.state, 'utf-8');
260
+ stateInfo = {
261
+ status: stateExtractField(stateContent, 'Status') || 'unknown',
262
+ current_phase: stateExtractField(stateContent, 'Current Phase'),
263
+ last_activity: stateExtractField(stateContent, 'Last Activity'),
264
+ };
265
+ } catch {}
266
+
267
+ output({
268
+ found: true,
269
+ workstream: name,
270
+ path: relPath,
271
+ files,
272
+ phases,
273
+ phase_count: phases.length,
274
+ completed_phases: phases.filter(ph => ph.status === 'complete').length,
275
+ ...stateInfo,
276
+ }, raw);
277
+ }
278
+
279
+ function cmdWorkstreamComplete(cwd, name, options, raw) {
280
+ if (!name) error('workstream name required. Usage: workstream complete <name>');
281
+ if (/[/\\]/.test(name) || name === '.' || name === '..') error('Invalid workstream name');
282
+
283
+ const root = planningRoot(cwd);
284
+ const wsRoot = path.join(root, 'workstreams');
285
+ const wsDir = path.join(wsRoot, name);
286
+
287
+ if (!fs.existsSync(wsDir)) {
288
+ output({ completed: false, error: 'not_found', workstream: name }, raw);
289
+ return;
290
+ }
291
+
292
+ const active = getActiveWorkstream(cwd);
293
+ if (active === name) setActiveWorkstream(cwd, null);
294
+
295
+ const archiveDir = path.join(root, 'milestones');
296
+ const today = new Date().toISOString().split('T')[0];
297
+ let archivePath = path.join(archiveDir, `ws-${name}-${today}`);
298
+ let suffix = 1;
299
+ while (fs.existsSync(archivePath)) {
300
+ archivePath = path.join(archiveDir, `ws-${name}-${today}-${suffix++}`);
301
+ }
302
+
303
+ fs.mkdirSync(archivePath, { recursive: true });
304
+
305
+ const filesMoved = [];
306
+ try {
307
+ const entries = fs.readdirSync(wsDir, { withFileTypes: true });
308
+ for (const entry of entries) {
309
+ fs.renameSync(path.join(wsDir, entry.name), path.join(archivePath, entry.name));
310
+ filesMoved.push(entry.name);
311
+ }
312
+ } catch (err) {
313
+ for (const fname of filesMoved) {
314
+ try { fs.renameSync(path.join(archivePath, fname), path.join(wsDir, fname)); } catch {}
315
+ }
316
+ try { fs.rmSync(archivePath, { recursive: true }); } catch {}
317
+ if (active === name) setActiveWorkstream(cwd, name);
318
+ output({ completed: false, error: 'archive_failed', message: err.message, workstream: name }, raw);
319
+ return;
320
+ }
321
+
322
+ try { fs.rmdirSync(wsDir); } catch {}
323
+
324
+ let remainingWs = 0;
325
+ try {
326
+ remainingWs = fs.readdirSync(wsRoot, { withFileTypes: true }).filter(e => e.isDirectory()).length;
327
+ if (remainingWs === 0) fs.rmdirSync(wsRoot);
328
+ } catch {}
329
+
330
+ output({
331
+ completed: true,
332
+ workstream: name,
333
+ archived_to: toPosixPath(path.relative(cwd, archivePath)),
334
+ remaining_workstreams: remainingWs,
335
+ reverted_to_flat: remainingWs === 0,
336
+ }, raw);
337
+ }
338
+
339
+ // ─── Active Workstream Commands ──────────────────────────────────────────────
340
+
341
+ function cmdWorkstreamSet(cwd, name, raw) {
342
+ if (!name) {
343
+ setActiveWorkstream(cwd, null);
344
+ output({ active: null, cleared: true }, raw);
345
+ return;
346
+ }
347
+
348
+ if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
349
+ output({ active: null, error: 'invalid_name', message: 'Workstream name must be alphanumeric, hyphens, and underscores only' }, raw);
350
+ return;
351
+ }
352
+
353
+ const wsDir = path.join(planningRoot(cwd), 'workstreams', name);
354
+ if (!fs.existsSync(wsDir)) {
355
+ output({ active: null, error: 'not_found', workstream: name }, raw);
356
+ return;
357
+ }
358
+
359
+ setActiveWorkstream(cwd, name);
360
+ output({ active: name, set: true }, raw, name);
361
+ }
362
+
363
+ function cmdWorkstreamGet(cwd, raw) {
364
+ const active = getActiveWorkstream(cwd);
365
+ const wsRoot = path.join(planningRoot(cwd), 'workstreams');
366
+ output({ active, mode: fs.existsSync(wsRoot) ? 'workstream' : 'flat' }, raw, active || 'none');
367
+ }
368
+
369
+ function cmdWorkstreamProgress(cwd, raw) {
370
+ const root = planningRoot(cwd);
371
+ const wsRoot = path.join(root, 'workstreams');
372
+
373
+ if (!fs.existsSync(wsRoot)) {
374
+ output({ mode: 'flat', workstreams: [], message: 'No workstreams — operating in flat mode' }, raw);
375
+ return;
376
+ }
377
+
378
+ const active = getActiveWorkstream(cwd);
379
+ const entries = fs.readdirSync(wsRoot, { withFileTypes: true });
380
+ const workstreams = [];
381
+
382
+ for (const entry of entries) {
383
+ if (!entry.isDirectory()) continue;
384
+
385
+ const wsDir = path.join(wsRoot, entry.name);
386
+ const phasesDir = path.join(wsDir, 'phases');
387
+
388
+ const phaseDirsProgress = readSubdirectories(phasesDir);
389
+ const phaseCount = phaseDirsProgress.length;
390
+ let completedCount = 0, totalPlans = 0, completedPlans = 0;
391
+ for (const d of phaseDirsProgress) {
392
+ try {
393
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, d));
394
+ const plans = filterPlanFiles(phaseFiles);
395
+ const summaries = filterSummaryFiles(phaseFiles);
396
+ totalPlans += plans.length;
397
+ completedPlans += Math.min(summaries.length, plans.length);
398
+ if (plans.length > 0 && summaries.length >= plans.length) completedCount++;
399
+ } catch {}
400
+ }
401
+
402
+ let roadmapPhaseCount = phaseCount;
403
+ try {
404
+ const roadmapContent = fs.readFileSync(path.join(wsDir, 'ROADMAP.md'), 'utf-8');
405
+ const phaseMatches = roadmapContent.match(/^###?\s+Phase\s+\d/gm);
406
+ if (phaseMatches) roadmapPhaseCount = phaseMatches.length;
407
+ } catch {}
408
+
409
+ let status = 'unknown', currentPhase = null;
410
+ try {
411
+ const stateContent = fs.readFileSync(path.join(wsDir, 'STATE.md'), 'utf-8');
412
+ status = stateExtractField(stateContent, 'Status') || 'unknown';
413
+ currentPhase = stateExtractField(stateContent, 'Current Phase');
414
+ } catch {}
415
+
416
+ workstreams.push({
417
+ name: entry.name,
418
+ active: entry.name === active,
419
+ status,
420
+ current_phase: currentPhase,
421
+ phases: `${completedCount}/${roadmapPhaseCount}`,
422
+ plans: `${completedPlans}/${totalPlans}`,
423
+ progress_percent: roadmapPhaseCount > 0 ? Math.round((completedCount / roadmapPhaseCount) * 100) : 0,
424
+ });
425
+ }
426
+
427
+ output({ mode: 'workstream', active, workstreams, count: workstreams.length }, raw);
428
+ }
429
+
430
+ // ─── Collision Detection ────────────────────────────────────────────────────
431
+
432
+ /**
433
+ * Return other workstreams that are NOT complete.
434
+ * Used to detect whether the milestone has active parallel work
435
+ * when a workstream finishes its last phase.
436
+ */
437
+ function getOtherActiveWorkstreams(cwd, excludeWs) {
438
+ const wsRoot = path.join(planningRoot(cwd), 'workstreams');
439
+ if (!fs.existsSync(wsRoot)) return [];
440
+
441
+ const entries = fs.readdirSync(wsRoot, { withFileTypes: true });
442
+ const others = [];
443
+
444
+ for (const entry of entries) {
445
+ if (!entry.isDirectory() || entry.name === excludeWs) continue;
446
+
447
+ const wsDir = path.join(wsRoot, entry.name);
448
+ const statePath = path.join(wsDir, 'STATE.md');
449
+
450
+ let status = 'unknown', currentPhase = null;
451
+ try {
452
+ const content = fs.readFileSync(statePath, 'utf-8');
453
+ status = stateExtractField(content, 'Status') || 'unknown';
454
+ currentPhase = stateExtractField(content, 'Current Phase');
455
+ } catch {}
456
+
457
+ if (status.toLowerCase().includes('milestone complete') ||
458
+ status.toLowerCase().includes('archived')) {
459
+ continue;
460
+ }
461
+
462
+ const phasesDir = path.join(wsDir, 'phases');
463
+ const phaseDirsOther = readSubdirectories(phasesDir);
464
+ const phaseCount = phaseDirsOther.length;
465
+ let completedCount = 0;
466
+ for (const d of phaseDirsOther) {
467
+ try {
468
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, d));
469
+ const plans = filterPlanFiles(phaseFiles);
470
+ const summaries = filterSummaryFiles(phaseFiles);
471
+ if (plans.length > 0 && summaries.length >= plans.length) completedCount++;
472
+ } catch {}
473
+ }
474
+
475
+ others.push({ name: entry.name, status, current_phase: currentPhase, phases: `${completedCount}/${phaseCount}` });
476
+ }
477
+
478
+ return others;
479
+ }
480
+
481
+ module.exports = {
482
+ migrateToWorkstreams,
483
+ cmdWorkstreamCreate,
484
+ cmdWorkstreamList,
485
+ cmdWorkstreamStatus,
486
+ cmdWorkstreamComplete,
487
+ cmdWorkstreamSet,
488
+ cmdWorkstreamGet,
489
+ cmdWorkstreamProgress,
490
+ getOtherActiveWorkstreams,
491
+ };