@howlil/ez-agents 3.4.1 → 3.4.2

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 (102) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +7 -18
  3. package/bin/install.js +52 -10
  4. package/commands/ez/join-discord.md +18 -18
  5. package/ez-agents/bin/lib/assistant-adapter.cjs +264 -264
  6. package/ez-agents/bin/lib/audit-exec.cjs +7 -2
  7. package/ez-agents/bin/lib/circuit-breaker.cjs +118 -118
  8. package/ez-agents/bin/lib/config.cjs +190 -190
  9. package/ez-agents/bin/lib/file-lock.cjs +236 -236
  10. package/ez-agents/bin/lib/frontmatter.cjs +299 -299
  11. package/ez-agents/bin/lib/fs-utils.cjs +153 -153
  12. package/ez-agents/bin/lib/git-utils.cjs +203 -203
  13. package/ez-agents/bin/lib/index.cjs +113 -113
  14. package/ez-agents/bin/lib/init.cjs +757 -757
  15. package/ez-agents/bin/lib/logger.cjs +47 -17
  16. package/ez-agents/bin/lib/milestone.cjs +241 -241
  17. package/ez-agents/bin/lib/model-provider.cjs +241 -241
  18. package/ez-agents/bin/lib/phase.cjs +925 -925
  19. package/ez-agents/bin/lib/planning-write.cjs +107 -107
  20. package/ez-agents/bin/lib/retry.cjs +119 -119
  21. package/ez-agents/bin/lib/roadmap.cjs +306 -306
  22. package/ez-agents/bin/lib/safe-exec.cjs +90 -4
  23. package/ez-agents/bin/lib/safe-path.cjs +130 -130
  24. package/ez-agents/bin/lib/state.cjs +736 -736
  25. package/ez-agents/bin/lib/temp-file.cjs +239 -239
  26. package/ez-agents/bin/lib/template.cjs +223 -223
  27. package/ez-agents/bin/lib/test-file-lock.cjs +112 -112
  28. package/ez-agents/bin/lib/test-graceful.cjs +93 -93
  29. package/ez-agents/bin/lib/test-logger.cjs +60 -60
  30. package/ez-agents/bin/lib/test-safe-exec.cjs +38 -38
  31. package/ez-agents/bin/lib/test-safe-path.cjs +33 -33
  32. package/ez-agents/bin/lib/test-temp-file.cjs +125 -125
  33. package/ez-agents/bin/lib/timeout-exec.cjs +63 -63
  34. package/ez-agents/bin/lib/verify.cjs +15 -1
  35. package/ez-agents/references/checkpoints.md +776 -776
  36. package/ez-agents/references/continuation-format.md +249 -249
  37. package/ez-agents/references/questioning.md +162 -162
  38. package/ez-agents/references/tdd.md +263 -263
  39. package/ez-agents/templates/codebase/concerns.md +310 -310
  40. package/ez-agents/templates/codebase/conventions.md +307 -307
  41. package/ez-agents/templates/codebase/integrations.md +280 -280
  42. package/ez-agents/templates/codebase/stack.md +186 -186
  43. package/ez-agents/templates/codebase/testing.md +480 -480
  44. package/ez-agents/templates/config.json +37 -37
  45. package/ez-agents/templates/continue-here.md +78 -78
  46. package/ez-agents/templates/milestone-archive.md +123 -123
  47. package/ez-agents/templates/milestone.md +115 -115
  48. package/ez-agents/templates/requirements.md +231 -231
  49. package/ez-agents/templates/research-project/ARCHITECTURE.md +204 -204
  50. package/ez-agents/templates/research-project/FEATURES.md +147 -147
  51. package/ez-agents/templates/research-project/PITFALLS.md +200 -200
  52. package/ez-agents/templates/research-project/STACK.md +120 -120
  53. package/ez-agents/templates/research-project/SUMMARY.md +170 -170
  54. package/ez-agents/templates/retrospective.md +54 -54
  55. package/ez-agents/templates/roadmap.md +202 -202
  56. package/ez-agents/templates/summary-minimal.md +41 -41
  57. package/ez-agents/templates/summary-standard.md +48 -48
  58. package/ez-agents/templates/summary.md +248 -248
  59. package/ez-agents/templates/user-setup.md +311 -311
  60. package/ez-agents/templates/verification-report.md +322 -322
  61. package/ez-agents/workflows/add-phase.md +112 -112
  62. package/ez-agents/workflows/add-tests.md +351 -351
  63. package/ez-agents/workflows/add-todo.md +158 -158
  64. package/ez-agents/workflows/audit-milestone.md +332 -332
  65. package/ez-agents/workflows/autonomous.md +743 -743
  66. package/ez-agents/workflows/check-todos.md +177 -177
  67. package/ez-agents/workflows/cleanup.md +152 -152
  68. package/ez-agents/workflows/complete-milestone.md +766 -766
  69. package/ez-agents/workflows/diagnose-issues.md +219 -219
  70. package/ez-agents/workflows/discovery-phase.md +289 -289
  71. package/ez-agents/workflows/discuss-phase.md +762 -762
  72. package/ez-agents/workflows/execute-phase.md +468 -468
  73. package/ez-agents/workflows/execute-plan.md +483 -483
  74. package/ez-agents/workflows/health.md +159 -159
  75. package/ez-agents/workflows/help.md +492 -492
  76. package/ez-agents/workflows/insert-phase.md +130 -130
  77. package/ez-agents/workflows/list-phase-assumptions.md +178 -178
  78. package/ez-agents/workflows/map-codebase.md +316 -316
  79. package/ez-agents/workflows/new-milestone.md +384 -384
  80. package/ez-agents/workflows/new-project.md +1113 -1113
  81. package/ez-agents/workflows/node-repair.md +92 -92
  82. package/ez-agents/workflows/pause-work.md +122 -122
  83. package/ez-agents/workflows/plan-milestone-gaps.md +274 -274
  84. package/ez-agents/workflows/plan-phase.md +651 -651
  85. package/ez-agents/workflows/progress.md +382 -382
  86. package/ez-agents/workflows/quick.md +610 -610
  87. package/ez-agents/workflows/remove-phase.md +155 -155
  88. package/ez-agents/workflows/research-phase.md +74 -74
  89. package/ez-agents/workflows/resume-project.md +307 -307
  90. package/ez-agents/workflows/set-profile.md +81 -81
  91. package/ez-agents/workflows/settings.md +242 -242
  92. package/ez-agents/workflows/stats.md +57 -57
  93. package/ez-agents/workflows/transition.md +544 -544
  94. package/ez-agents/workflows/ui-phase.md +290 -290
  95. package/ez-agents/workflows/ui-review.md +157 -157
  96. package/ez-agents/workflows/update.md +320 -320
  97. package/ez-agents/workflows/validate-phase.md +167 -167
  98. package/ez-agents/workflows/verify-phase.md +243 -243
  99. package/ez-agents/workflows/verify-work.md +584 -584
  100. package/package.json +2 -3
  101. package/scripts/build-hooks.js +43 -43
  102. package/scripts/run-tests.cjs +29 -29
@@ -1,306 +1,306 @@
1
- /**
2
- * Roadmap — Roadmap parsing and update operations
3
- */
4
-
5
- const fs = require('fs');
6
- const path = require('path');
7
- const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
8
- const { safePlanningWriteSync } = require('./planning-write.cjs');
9
-
10
- function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
11
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
12
-
13
- if (!fs.existsSync(roadmapPath)) {
14
- output({ found: false, error: 'ROADMAP.md not found' }, raw, '');
15
- return;
16
- }
17
-
18
- try {
19
- const content = fs.readFileSync(roadmapPath, 'utf-8');
20
-
21
- // Escape special regex chars in phase number, handle decimal
22
- const escapedPhase = escapeRegex(phaseNum);
23
-
24
- // Match "## Phase X:", "### Phase X:", or "#### Phase X:" with optional name
25
- const phasePattern = new RegExp(
26
- `#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`,
27
- 'i'
28
- );
29
- const headerMatch = content.match(phasePattern);
30
-
31
- if (!headerMatch) {
32
- // Fallback: check if phase exists in summary list but missing detail section
33
- const checklistPattern = new RegExp(
34
- `-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${escapedPhase}:\\s*([^*]+)\\*\\*`,
35
- 'i'
36
- );
37
- const checklistMatch = content.match(checklistPattern);
38
-
39
- if (checklistMatch) {
40
- // Phase exists in summary but missing detail section - malformed ROADMAP
41
- output({
42
- found: false,
43
- phase_number: phaseNum,
44
- phase_name: checklistMatch[1].trim(),
45
- error: 'malformed_roadmap',
46
- message: `Phase ${phaseNum} exists in summary list but missing "### Phase ${phaseNum}:" detail section. ROADMAP.md needs both formats.`
47
- }, raw, '');
48
- return;
49
- }
50
-
51
- output({ found: false, phase_number: phaseNum }, raw, '');
52
- return;
53
- }
54
-
55
- const phaseName = headerMatch[1].trim();
56
- const headerIndex = headerMatch.index;
57
-
58
- // Find the end of this section (next ## or ### phase header, or end of file)
59
- const restOfContent = content.slice(headerIndex);
60
- const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
61
- const sectionEnd = nextHeaderMatch
62
- ? headerIndex + nextHeaderMatch.index
63
- : content.length;
64
-
65
- const section = content.slice(headerIndex, sectionEnd).trim();
66
-
67
- // Extract goal if present (supports both **Goal:** and **Goal**: formats)
68
- const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
69
- const goal = goalMatch ? goalMatch[1].trim() : null;
70
-
71
- // Extract success criteria as structured array
72
- const criteriaMatch = section.match(/\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\s*\d+\.\s*[^\n]+\n?)+)/i);
73
- const success_criteria = criteriaMatch
74
- ? criteriaMatch[1].trim().split('\n').map(line => line.replace(/^\s*\d+\.\s*/, '').trim()).filter(Boolean)
75
- : [];
76
-
77
- output(
78
- {
79
- found: true,
80
- phase_number: phaseNum,
81
- phase_name: phaseName,
82
- goal,
83
- success_criteria,
84
- section,
85
- },
86
- raw,
87
- section
88
- );
89
- } catch (e) {
90
- error('Failed to read ROADMAP.md: ' + e.message);
91
- }
92
- }
93
-
94
- function cmdRoadmapAnalyze(cwd, raw) {
95
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
96
-
97
- if (!fs.existsSync(roadmapPath)) {
98
- output({ error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null }, raw);
99
- return;
100
- }
101
-
102
- const content = fs.readFileSync(roadmapPath, 'utf-8');
103
- const phasesDir = path.join(cwd, '.planning', 'phases');
104
-
105
- // Extract all phase headings: ## Phase N: Name or ### Phase N: Name
106
- const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
107
- const phases = [];
108
- let match;
109
-
110
- while ((match = phasePattern.exec(content)) !== null) {
111
- const phaseNum = match[1];
112
- const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
113
-
114
- // Extract goal from the section
115
- const sectionStart = match.index;
116
- const restOfContent = content.slice(sectionStart);
117
- const nextHeader = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
118
- const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
119
- const section = content.slice(sectionStart, sectionEnd);
120
-
121
- const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
122
- const goal = goalMatch ? goalMatch[1].trim() : null;
123
-
124
- const dependsMatch = section.match(/\*\*Depends on(?::\*\*|\*\*:)\s*([^\n]+)/i);
125
- const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
126
-
127
- // Check completion on disk
128
- const normalized = normalizePhaseName(phaseNum);
129
- let diskStatus = 'no_directory';
130
- let planCount = 0;
131
- let summaryCount = 0;
132
- let hasContext = false;
133
- let hasResearch = false;
134
-
135
- try {
136
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
137
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
138
- const dirMatch = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
139
-
140
- if (dirMatch) {
141
- const phaseFiles = fs.readdirSync(path.join(phasesDir, dirMatch));
142
- planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
143
- summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
144
- hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
145
- hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
146
-
147
- if (summaryCount >= planCount && planCount > 0) diskStatus = 'complete';
148
- else if (summaryCount > 0) diskStatus = 'partial';
149
- else if (planCount > 0) diskStatus = 'planned';
150
- else if (hasResearch) diskStatus = 'researched';
151
- else if (hasContext) diskStatus = 'discussed';
152
- else diskStatus = 'empty';
153
- }
154
- } catch {}
155
-
156
- // Check ROADMAP checkbox status
157
- const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}`, 'i');
158
- const checkboxMatch = content.match(checkboxPattern);
159
- const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
160
-
161
- // If roadmap marks phase complete, trust that over disk file structure.
162
- // Phases completed before EZ tracking (or via external tools) may lack
163
- // the standard PLAN/SUMMARY pairs but are still done.
164
- if (roadmapComplete && diskStatus !== 'complete') {
165
- diskStatus = 'complete';
166
- }
167
-
168
- phases.push({
169
- number: phaseNum,
170
- name: phaseName,
171
- goal,
172
- depends_on,
173
- plan_count: planCount,
174
- summary_count: summaryCount,
175
- has_context: hasContext,
176
- has_research: hasResearch,
177
- disk_status: diskStatus,
178
- roadmap_complete: roadmapComplete,
179
- });
180
- }
181
-
182
- // Extract milestone info
183
- const milestones = [];
184
- const milestonePattern = /##\s*(.*v(\d+\.\d+)[^(\n]*)/gi;
185
- let mMatch;
186
- while ((mMatch = milestonePattern.exec(content)) !== null) {
187
- milestones.push({
188
- heading: mMatch[1].trim(),
189
- version: 'v' + mMatch[2],
190
- });
191
- }
192
-
193
- // Find current and next phase
194
- const currentPhase = phases.find(p => p.disk_status === 'planned' || p.disk_status === 'partial') || null;
195
- const nextPhase = phases.find(p => p.disk_status === 'empty' || p.disk_status === 'no_directory' || p.disk_status === 'discussed' || p.disk_status === 'researched') || null;
196
-
197
- // Aggregated stats
198
- const totalPlans = phases.reduce((sum, p) => sum + p.plan_count, 0);
199
- const totalSummaries = phases.reduce((sum, p) => sum + p.summary_count, 0);
200
- const completedPhases = phases.filter(p => p.disk_status === 'complete').length;
201
-
202
- // Detect phases in summary list without detail sections (malformed ROADMAP)
203
- const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:\.\d+)*)/gi;
204
- const checklistPhases = new Set();
205
- let checklistMatch;
206
- while ((checklistMatch = checklistPattern.exec(content)) !== null) {
207
- checklistPhases.add(checklistMatch[1]);
208
- }
209
- const detailPhases = new Set(phases.map(p => p.number));
210
- const missingDetails = [...checklistPhases].filter(p => !detailPhases.has(p));
211
-
212
- const result = {
213
- milestones,
214
- phases,
215
- phase_count: phases.length,
216
- completed_phases: completedPhases,
217
- total_plans: totalPlans,
218
- total_summaries: totalSummaries,
219
- progress_percent: totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0,
220
- current_phase: currentPhase ? currentPhase.number : null,
221
- next_phase: nextPhase ? nextPhase.number : null,
222
- missing_phase_details: missingDetails.length > 0 ? missingDetails : null,
223
- };
224
-
225
- output(result, raw);
226
- }
227
-
228
- function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
229
- if (!phaseNum) {
230
- error('phase number required for roadmap update-plan-progress');
231
- }
232
-
233
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
234
-
235
- const phaseInfo = findPhaseInternal(cwd, phaseNum);
236
- if (!phaseInfo) {
237
- error(`Phase ${phaseNum} not found`);
238
- }
239
-
240
- const planCount = phaseInfo.plans.length;
241
- const summaryCount = phaseInfo.summaries.length;
242
-
243
- if (planCount === 0) {
244
- output({ updated: false, reason: 'No plans found', plan_count: 0, summary_count: 0 }, raw, 'no plans');
245
- return;
246
- }
247
-
248
- const isComplete = summaryCount >= planCount;
249
- const status = isComplete ? 'Complete' : summaryCount > 0 ? 'In Progress' : 'Planned';
250
- const today = new Date().toISOString().split('T')[0];
251
-
252
- if (!fs.existsSync(roadmapPath)) {
253
- output({ updated: false, reason: 'ROADMAP.md not found', plan_count: planCount, summary_count: summaryCount }, raw, 'no roadmap');
254
- return;
255
- }
256
-
257
- let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
258
- const phaseEscaped = escapeRegex(phaseNum);
259
-
260
- // Progress table row: update Plans column (summaries/plans) and Status column
261
- const tablePattern = new RegExp(
262
- `(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|)[^|]*(\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
263
- 'i'
264
- );
265
- const dateField = isComplete ? ` ${today} ` : ' ';
266
- roadmapContent = roadmapContent.replace(
267
- tablePattern,
268
- `$1 ${summaryCount}/${planCount} $2 ${status.padEnd(11)}$3${dateField}$4`
269
- );
270
-
271
- // Update plan count in phase detail section
272
- const planCountPattern = new RegExp(
273
- `(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
274
- 'i'
275
- );
276
- const planCountText = isComplete
277
- ? `${summaryCount}/${planCount} plans complete`
278
- : `${summaryCount}/${planCount} plans executed`;
279
- roadmapContent = roadmapContent.replace(planCountPattern, `$1${planCountText}`);
280
-
281
- // If complete: check checkbox
282
- if (isComplete) {
283
- const checkboxPattern = new RegExp(
284
- `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
285
- 'i'
286
- );
287
- roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
288
- }
289
-
290
- safePlanningWriteSync(roadmapPath, roadmapContent);
291
-
292
- output({
293
- updated: true,
294
- phase: phaseNum,
295
- plan_count: planCount,
296
- summary_count: summaryCount,
297
- status,
298
- complete: isComplete,
299
- }, raw, `${summaryCount}/${planCount} ${status}`);
300
- }
301
-
302
- module.exports = {
303
- cmdRoadmapGetPhase,
304
- cmdRoadmapAnalyze,
305
- cmdRoadmapUpdatePlanProgress,
306
- };
1
+ /**
2
+ * Roadmap — Roadmap parsing and update operations
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { escapeRegex, normalizePhaseName, output, error, findPhaseInternal } = require('./core.cjs');
8
+ const { safePlanningWriteSync } = require('./planning-write.cjs');
9
+
10
+ function cmdRoadmapGetPhase(cwd, phaseNum, raw) {
11
+ const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
12
+
13
+ if (!fs.existsSync(roadmapPath)) {
14
+ output({ found: false, error: 'ROADMAP.md not found' }, raw, '');
15
+ return;
16
+ }
17
+
18
+ try {
19
+ const content = fs.readFileSync(roadmapPath, 'utf-8');
20
+
21
+ // Escape special regex chars in phase number, handle decimal
22
+ const escapedPhase = escapeRegex(phaseNum);
23
+
24
+ // Match "## Phase X:", "### Phase X:", or "#### Phase X:" with optional name
25
+ const phasePattern = new RegExp(
26
+ `#{2,4}\\s*Phase\\s+${escapedPhase}:\\s*([^\\n]+)`,
27
+ 'i'
28
+ );
29
+ const headerMatch = content.match(phasePattern);
30
+
31
+ if (!headerMatch) {
32
+ // Fallback: check if phase exists in summary list but missing detail section
33
+ const checklistPattern = new RegExp(
34
+ `-\\s*\\[[ x]\\]\\s*\\*\\*Phase\\s+${escapedPhase}:\\s*([^*]+)\\*\\*`,
35
+ 'i'
36
+ );
37
+ const checklistMatch = content.match(checklistPattern);
38
+
39
+ if (checklistMatch) {
40
+ // Phase exists in summary but missing detail section - malformed ROADMAP
41
+ output({
42
+ found: false,
43
+ phase_number: phaseNum,
44
+ phase_name: checklistMatch[1].trim(),
45
+ error: 'malformed_roadmap',
46
+ message: `Phase ${phaseNum} exists in summary list but missing "### Phase ${phaseNum}:" detail section. ROADMAP.md needs both formats.`
47
+ }, raw, '');
48
+ return;
49
+ }
50
+
51
+ output({ found: false, phase_number: phaseNum }, raw, '');
52
+ return;
53
+ }
54
+
55
+ const phaseName = headerMatch[1].trim();
56
+ const headerIndex = headerMatch.index;
57
+
58
+ // Find the end of this section (next ## or ### phase header, or end of file)
59
+ const restOfContent = content.slice(headerIndex);
60
+ const nextHeaderMatch = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
61
+ const sectionEnd = nextHeaderMatch
62
+ ? headerIndex + nextHeaderMatch.index
63
+ : content.length;
64
+
65
+ const section = content.slice(headerIndex, sectionEnd).trim();
66
+
67
+ // Extract goal if present (supports both **Goal:** and **Goal**: formats)
68
+ const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
69
+ const goal = goalMatch ? goalMatch[1].trim() : null;
70
+
71
+ // Extract success criteria as structured array
72
+ const criteriaMatch = section.match(/\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\s*\d+\.\s*[^\n]+\n?)+)/i);
73
+ const success_criteria = criteriaMatch
74
+ ? criteriaMatch[1].trim().split('\n').map(line => line.replace(/^\s*\d+\.\s*/, '').trim()).filter(Boolean)
75
+ : [];
76
+
77
+ output(
78
+ {
79
+ found: true,
80
+ phase_number: phaseNum,
81
+ phase_name: phaseName,
82
+ goal,
83
+ success_criteria,
84
+ section,
85
+ },
86
+ raw,
87
+ section
88
+ );
89
+ } catch (e) {
90
+ error('Failed to read ROADMAP.md: ' + e.message);
91
+ }
92
+ }
93
+
94
+ function cmdRoadmapAnalyze(cwd, raw) {
95
+ const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
96
+
97
+ if (!fs.existsSync(roadmapPath)) {
98
+ output({ error: 'ROADMAP.md not found', milestones: [], phases: [], current_phase: null }, raw);
99
+ return;
100
+ }
101
+
102
+ const content = fs.readFileSync(roadmapPath, 'utf-8');
103
+ const phasesDir = path.join(cwd, '.planning', 'phases');
104
+
105
+ // Extract all phase headings: ## Phase N: Name or ### Phase N: Name
106
+ const phasePattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
107
+ const phases = [];
108
+ let match;
109
+
110
+ while ((match = phasePattern.exec(content)) !== null) {
111
+ const phaseNum = match[1];
112
+ const phaseName = match[2].replace(/\(INSERTED\)/i, '').trim();
113
+
114
+ // Extract goal from the section
115
+ const sectionStart = match.index;
116
+ const restOfContent = content.slice(sectionStart);
117
+ const nextHeader = restOfContent.match(/\n#{2,4}\s+Phase\s+\d/i);
118
+ const sectionEnd = nextHeader ? sectionStart + nextHeader.index : content.length;
119
+ const section = content.slice(sectionStart, sectionEnd);
120
+
121
+ const goalMatch = section.match(/\*\*Goal(?::\*\*|\*\*:)\s*([^\n]+)/i);
122
+ const goal = goalMatch ? goalMatch[1].trim() : null;
123
+
124
+ const dependsMatch = section.match(/\*\*Depends on(?::\*\*|\*\*:)\s*([^\n]+)/i);
125
+ const depends_on = dependsMatch ? dependsMatch[1].trim() : null;
126
+
127
+ // Check completion on disk
128
+ const normalized = normalizePhaseName(phaseNum);
129
+ let diskStatus = 'no_directory';
130
+ let planCount = 0;
131
+ let summaryCount = 0;
132
+ let hasContext = false;
133
+ let hasResearch = false;
134
+
135
+ try {
136
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
137
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
138
+ const dirMatch = dirs.find(d => d.startsWith(normalized + '-') || d === normalized);
139
+
140
+ if (dirMatch) {
141
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, dirMatch));
142
+ planCount = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md').length;
143
+ summaryCount = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md').length;
144
+ hasContext = phaseFiles.some(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
145
+ hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
146
+
147
+ if (summaryCount >= planCount && planCount > 0) diskStatus = 'complete';
148
+ else if (summaryCount > 0) diskStatus = 'partial';
149
+ else if (planCount > 0) diskStatus = 'planned';
150
+ else if (hasResearch) diskStatus = 'researched';
151
+ else if (hasContext) diskStatus = 'discussed';
152
+ else diskStatus = 'empty';
153
+ }
154
+ } catch {}
155
+
156
+ // Check ROADMAP checkbox status
157
+ const checkboxPattern = new RegExp(`-\\s*\\[(x| )\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}`, 'i');
158
+ const checkboxMatch = content.match(checkboxPattern);
159
+ const roadmapComplete = checkboxMatch ? checkboxMatch[1] === 'x' : false;
160
+
161
+ // If roadmap marks phase complete, trust that over disk file structure.
162
+ // Phases completed before EZ tracking (or via external tools) may lack
163
+ // the standard PLAN/SUMMARY pairs but are still done.
164
+ if (roadmapComplete && diskStatus !== 'complete') {
165
+ diskStatus = 'complete';
166
+ }
167
+
168
+ phases.push({
169
+ number: phaseNum,
170
+ name: phaseName,
171
+ goal,
172
+ depends_on,
173
+ plan_count: planCount,
174
+ summary_count: summaryCount,
175
+ has_context: hasContext,
176
+ has_research: hasResearch,
177
+ disk_status: diskStatus,
178
+ roadmap_complete: roadmapComplete,
179
+ });
180
+ }
181
+
182
+ // Extract milestone info
183
+ const milestones = [];
184
+ const milestonePattern = /##\s*(.*v(\d+\.\d+)[^(\n]*)/gi;
185
+ let mMatch;
186
+ while ((mMatch = milestonePattern.exec(content)) !== null) {
187
+ milestones.push({
188
+ heading: mMatch[1].trim(),
189
+ version: 'v' + mMatch[2],
190
+ });
191
+ }
192
+
193
+ // Find current and next phase
194
+ const currentPhase = phases.find(p => p.disk_status === 'planned' || p.disk_status === 'partial') || null;
195
+ const nextPhase = phases.find(p => p.disk_status === 'empty' || p.disk_status === 'no_directory' || p.disk_status === 'discussed' || p.disk_status === 'researched') || null;
196
+
197
+ // Aggregated stats
198
+ const totalPlans = phases.reduce((sum, p) => sum + p.plan_count, 0);
199
+ const totalSummaries = phases.reduce((sum, p) => sum + p.summary_count, 0);
200
+ const completedPhases = phases.filter(p => p.disk_status === 'complete').length;
201
+
202
+ // Detect phases in summary list without detail sections (malformed ROADMAP)
203
+ const checklistPattern = /-\s*\[[ x]\]\s*\*\*Phase\s+(\d+[A-Z]?(?:\.\d+)*)/gi;
204
+ const checklistPhases = new Set();
205
+ let checklistMatch;
206
+ while ((checklistMatch = checklistPattern.exec(content)) !== null) {
207
+ checklistPhases.add(checklistMatch[1]);
208
+ }
209
+ const detailPhases = new Set(phases.map(p => p.number));
210
+ const missingDetails = [...checklistPhases].filter(p => !detailPhases.has(p));
211
+
212
+ const result = {
213
+ milestones,
214
+ phases,
215
+ phase_count: phases.length,
216
+ completed_phases: completedPhases,
217
+ total_plans: totalPlans,
218
+ total_summaries: totalSummaries,
219
+ progress_percent: totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0,
220
+ current_phase: currentPhase ? currentPhase.number : null,
221
+ next_phase: nextPhase ? nextPhase.number : null,
222
+ missing_phase_details: missingDetails.length > 0 ? missingDetails : null,
223
+ };
224
+
225
+ output(result, raw);
226
+ }
227
+
228
+ function cmdRoadmapUpdatePlanProgress(cwd, phaseNum, raw) {
229
+ if (!phaseNum) {
230
+ error('phase number required for roadmap update-plan-progress');
231
+ }
232
+
233
+ const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
234
+
235
+ const phaseInfo = findPhaseInternal(cwd, phaseNum);
236
+ if (!phaseInfo) {
237
+ error(`Phase ${phaseNum} not found`);
238
+ }
239
+
240
+ const planCount = phaseInfo.plans.length;
241
+ const summaryCount = phaseInfo.summaries.length;
242
+
243
+ if (planCount === 0) {
244
+ output({ updated: false, reason: 'No plans found', plan_count: 0, summary_count: 0 }, raw, 'no plans');
245
+ return;
246
+ }
247
+
248
+ const isComplete = summaryCount >= planCount;
249
+ const status = isComplete ? 'Complete' : summaryCount > 0 ? 'In Progress' : 'Planned';
250
+ const today = new Date().toISOString().split('T')[0];
251
+
252
+ if (!fs.existsSync(roadmapPath)) {
253
+ output({ updated: false, reason: 'ROADMAP.md not found', plan_count: planCount, summary_count: summaryCount }, raw, 'no roadmap');
254
+ return;
255
+ }
256
+
257
+ let roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
258
+ const phaseEscaped = escapeRegex(phaseNum);
259
+
260
+ // Progress table row: update Plans column (summaries/plans) and Status column
261
+ const tablePattern = new RegExp(
262
+ `(\\|\\s*${phaseEscaped}\\.?\\s[^|]*\\|)[^|]*(\\|)\\s*[^|]*(\\|)\\s*[^|]*(\\|)`,
263
+ 'i'
264
+ );
265
+ const dateField = isComplete ? ` ${today} ` : ' ';
266
+ roadmapContent = roadmapContent.replace(
267
+ tablePattern,
268
+ `$1 ${summaryCount}/${planCount} $2 ${status.padEnd(11)}$3${dateField}$4`
269
+ );
270
+
271
+ // Update plan count in phase detail section
272
+ const planCountPattern = new RegExp(
273
+ `(#{2,4}\\s*Phase\\s+${phaseEscaped}[\\s\\S]*?\\*\\*Plans:\\*\\*\\s*)[^\\n]+`,
274
+ 'i'
275
+ );
276
+ const planCountText = isComplete
277
+ ? `${summaryCount}/${planCount} plans complete`
278
+ : `${summaryCount}/${planCount} plans executed`;
279
+ roadmapContent = roadmapContent.replace(planCountPattern, `$1${planCountText}`);
280
+
281
+ // If complete: check checkbox
282
+ if (isComplete) {
283
+ const checkboxPattern = new RegExp(
284
+ `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${phaseEscaped}[:\\s][^\\n]*)`,
285
+ 'i'
286
+ );
287
+ roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`);
288
+ }
289
+
290
+ safePlanningWriteSync(roadmapPath, roadmapContent);
291
+
292
+ output({
293
+ updated: true,
294
+ phase: phaseNum,
295
+ plan_count: planCount,
296
+ summary_count: summaryCount,
297
+ status,
298
+ complete: isComplete,
299
+ }, raw, `${summaryCount}/${planCount} ${status}`);
300
+ }
301
+
302
+ module.exports = {
303
+ cmdRoadmapGetPhase,
304
+ cmdRoadmapAnalyze,
305
+ cmdRoadmapUpdatePlanProgress,
306
+ };