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