@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
@@ -2,11 +2,13 @@
2
2
 
3
3
  /**
4
4
  * EZ Logger — Centralized logging module for EZ workflow
5
- *
5
+ *
6
6
  * Provides structured logging with levels (ERROR, WARN, INFO, DEBUG)
7
- * Writes to .planning/logs/ez-{timestamp}.log
7
+ * Writes to .planning/logs/{category}/ez-{YYYY-MM-DD}.log
8
+ * - One file per day per category (reduces file waste)
9
+ * - Categories: error, warn, info, debug
8
10
  * Replaces silent catch {} blocks with proper error logging
9
- *
11
+ *
10
12
  * Usage:
11
13
  * const Logger = require('./logger.cjs');
12
14
  * const logger = new Logger();
@@ -23,28 +25,58 @@ class Logger {
23
25
  */
24
26
  constructor(logDir = '.planning/logs') {
25
27
  this.logDir = logDir;
26
- this.logFile = null;
28
+ this.logFiles = {
29
+ error: null,
30
+ warn: null,
31
+ info: null,
32
+ debug: null
33
+ };
34
+ this.currentDate = null;
35
+ }
36
+
37
+ /**
38
+ * Get current date string in YYYY-MM-DD format
39
+ * @returns {string} - Date string
40
+ */
41
+ _getDateStr() {
42
+ return new Date().toISOString().split('T')[0];
27
43
  }
28
44
 
29
45
  /**
30
- * Ensure log directory exists and initialize log file
46
+ * Ensure log directory exists
31
47
  */
32
48
  _ensureLogDir() {
33
49
  if (!fs.existsSync(this.logDir)) {
34
50
  fs.mkdirSync(this.logDir, { recursive: true });
35
51
  }
36
- this.logFile = path.join(this.logDir, `ez-${Date.now()}.log`);
37
52
  }
38
53
 
39
54
  /**
40
- * Get current log file path
55
+ * Get log file path for a specific category
56
+ * Creates new file when date changes (daily rotation)
57
+ * @param {string} category - Log category (error, warn, info, debug)
41
58
  * @returns {string} - Path to log file
42
59
  */
43
- getLogFile() {
44
- if (!this.logFile) {
60
+ _getLogFile(category) {
61
+ const dateStr = this._getDateStr();
62
+
63
+ // Rotate log file if date changed
64
+ if (this.currentDate !== dateStr) {
65
+ this.currentDate = dateStr;
66
+ this.logFiles = { error: null, warn: null, info: null, debug: null };
67
+ }
68
+
69
+ if (!this.logFiles[category]) {
45
70
  this._ensureLogDir();
71
+ // Create category subdirectory
72
+ const categoryDir = path.join(this.logDir, category);
73
+ if (!fs.existsSync(categoryDir)) {
74
+ fs.mkdirSync(categoryDir, { recursive: true });
75
+ }
76
+ this.logFiles[category] = path.join(categoryDir, `ez-${dateStr}.log`);
46
77
  }
47
- return this.logFile;
78
+
79
+ return this.logFiles[category];
48
80
  }
49
81
 
50
82
  /**
@@ -54,11 +86,9 @@ class Logger {
54
86
  * @param {Object} context - Additional context data
55
87
  */
56
88
  log(level, message, context = {}) {
57
- // Ensure log directory exists before first write
58
- if (!this.logFile) {
59
- this._ensureLogDir();
60
- }
61
-
89
+ const category = level.toLowerCase();
90
+ const logFile = this._getLogFile(category);
91
+
62
92
  const entry = {
63
93
  timestamp: new Date().toISOString(),
64
94
  level,
@@ -68,8 +98,8 @@ class Logger {
68
98
  };
69
99
 
70
100
  try {
71
- fs.appendFileSync(this.logFile, JSON.stringify(entry) + '\n');
72
-
101
+ fs.appendFileSync(logFile, JSON.stringify(entry) + '\n');
102
+
73
103
  // Always output ERROR level to console for visibility
74
104
  if (level === 'ERROR') {
75
105
  console.error(`[EZ ${level}] ${message}`);
@@ -1,241 +1,241 @@
1
- /**
2
- * Milestone — Milestone and requirements lifecycle operations
3
- */
4
-
5
- const fs = require('fs');
6
- const path = require('path');
7
- const { escapeRegex, getMilestonePhaseFilter, output, error } = require('./core.cjs');
8
- const { extractFrontmatter } = require('./frontmatter.cjs');
9
- const { writeStateMd } = require('./state.cjs');
10
-
11
- function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
12
- if (!reqIdsRaw || reqIdsRaw.length === 0) {
13
- error('requirement IDs required. Usage: requirements mark-complete REQ-01,REQ-02 or REQ-01 REQ-02');
14
- }
15
-
16
- // Accept comma-separated, space-separated, or bracket-wrapped: [REQ-01, REQ-02]
17
- const reqIds = reqIdsRaw
18
- .join(' ')
19
- .replace(/[\[\]]/g, '')
20
- .split(/[,\s]+/)
21
- .map(r => r.trim())
22
- .filter(Boolean);
23
-
24
- if (reqIds.length === 0) {
25
- error('no valid requirement IDs found');
26
- }
27
-
28
- const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
29
- if (!fs.existsSync(reqPath)) {
30
- output({ updated: false, reason: 'REQUIREMENTS.md not found', ids: reqIds }, raw, 'no requirements file');
31
- return;
32
- }
33
-
34
- let reqContent = fs.readFileSync(reqPath, 'utf-8');
35
- const updated = [];
36
- const notFound = [];
37
-
38
- for (const reqId of reqIds) {
39
- let found = false;
40
- const reqEscaped = escapeRegex(reqId);
41
-
42
- // Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
43
- const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi');
44
- if (checkboxPattern.test(reqContent)) {
45
- reqContent = reqContent.replace(checkboxPattern, '$1x$2');
46
- found = true;
47
- }
48
-
49
- // Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete |
50
- const tablePattern = new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi');
51
- if (tablePattern.test(reqContent)) {
52
- // Re-read since test() advances lastIndex for global regex
53
- reqContent = reqContent.replace(
54
- new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
55
- '$1 Complete $2'
56
- );
57
- found = true;
58
- }
59
-
60
- if (found) {
61
- updated.push(reqId);
62
- } else {
63
- notFound.push(reqId);
64
- }
65
- }
66
-
67
- if (updated.length > 0) {
68
- fs.writeFileSync(reqPath, reqContent, 'utf-8');
69
- }
70
-
71
- output({
72
- updated: updated.length > 0,
73
- marked_complete: updated,
74
- not_found: notFound,
75
- total: reqIds.length,
76
- }, raw, `${updated.length}/${reqIds.length} requirements marked complete`);
77
- }
78
-
79
- function cmdMilestoneComplete(cwd, version, options, raw) {
80
- if (!version) {
81
- error('version required for milestone complete (e.g., v1.0)');
82
- }
83
-
84
- const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
85
- const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
86
- const statePath = path.join(cwd, '.planning', 'STATE.md');
87
- const milestonesPath = path.join(cwd, '.planning', 'MILESTONES.md');
88
- const archiveDir = path.join(cwd, '.planning', 'milestones');
89
- const phasesDir = path.join(cwd, '.planning', 'phases');
90
- const today = new Date().toISOString().split('T')[0];
91
- const milestoneName = options.name || version;
92
-
93
- // Ensure archive directory exists
94
- fs.mkdirSync(archiveDir, { recursive: true });
95
-
96
- // Scope stats and accomplishments to only the phases belonging to the
97
- // current milestone's ROADMAP. Uses the shared filter from core.cjs
98
- // (same logic used by cmdPhasesList and other callers).
99
- const isDirInMilestone = getMilestonePhaseFilter(cwd);
100
-
101
- // Gather stats from phases (scoped to current milestone only)
102
- let phaseCount = 0;
103
- let totalPlans = 0;
104
- let totalTasks = 0;
105
- const accomplishments = [];
106
-
107
- try {
108
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
109
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
110
-
111
- for (const dir of dirs) {
112
- if (!isDirInMilestone(dir)) continue;
113
-
114
- phaseCount++;
115
- const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
116
- const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
117
- const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
118
- totalPlans += plans.length;
119
-
120
- // Extract one-liners from summaries
121
- for (const s of summaries) {
122
- try {
123
- const content = fs.readFileSync(path.join(phasesDir, dir, s), 'utf-8');
124
- const fm = extractFrontmatter(content);
125
- if (fm['one-liner']) {
126
- accomplishments.push(fm['one-liner']);
127
- }
128
- // Count tasks
129
- const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
130
- totalTasks += taskMatches.length;
131
- } catch {}
132
- }
133
- }
134
- } catch {}
135
-
136
- // Archive ROADMAP.md
137
- if (fs.existsSync(roadmapPath)) {
138
- const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
139
- fs.writeFileSync(path.join(archiveDir, `${version}-ROADMAP.md`), roadmapContent, 'utf-8');
140
- }
141
-
142
- // Archive REQUIREMENTS.md
143
- if (fs.existsSync(reqPath)) {
144
- const reqContent = fs.readFileSync(reqPath, 'utf-8');
145
- const archiveHeader = `# Requirements Archive: ${version} ${milestoneName}\n\n**Archived:** ${today}\n**Status:** SHIPPED\n\nFor current requirements, see \`.planning/REQUIREMENTS.md\`.\n\n---\n\n`;
146
- fs.writeFileSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`), archiveHeader + reqContent, 'utf-8');
147
- }
148
-
149
- // Archive audit file if exists
150
- const auditFile = path.join(cwd, '.planning', `${version}-MILESTONE-AUDIT.md`);
151
- if (fs.existsSync(auditFile)) {
152
- fs.renameSync(auditFile, path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`));
153
- }
154
-
155
- // Create/append MILESTONES.md entry
156
- const accomplishmentsList = accomplishments.map(a => `- ${a}`).join('\n');
157
- const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n---\n\n`;
158
-
159
- if (fs.existsSync(milestonesPath)) {
160
- const existing = fs.readFileSync(milestonesPath, 'utf-8');
161
- if (!existing.trim()) {
162
- // Empty file — treat like new
163
- fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
164
- } else {
165
- // Insert after the header line(s) for reverse chronological order (newest first)
166
- const headerMatch = existing.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
167
- if (headerMatch) {
168
- const header = headerMatch[1];
169
- const rest = existing.slice(header.length);
170
- fs.writeFileSync(milestonesPath, header + milestoneEntry + rest, 'utf-8');
171
- } else {
172
- // No recognizable header — prepend the entry
173
- fs.writeFileSync(milestonesPath, milestoneEntry + existing, 'utf-8');
174
- }
175
- }
176
- } else {
177
- fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
178
- }
179
-
180
- // Update STATE.md
181
- if (fs.existsSync(statePath)) {
182
- let stateContent = fs.readFileSync(statePath, 'utf-8');
183
- stateContent = stateContent.replace(
184
- /(\*\*Status:\*\*\s*).*/,
185
- `$1${version} milestone complete`
186
- );
187
- stateContent = stateContent.replace(
188
- /(\*\*Last Activity:\*\*\s*).*/,
189
- `$1${today}`
190
- );
191
- stateContent = stateContent.replace(
192
- /(\*\*Last Activity Description:\*\*\s*).*/,
193
- `$1${version} milestone completed and archived`
194
- );
195
- writeStateMd(statePath, stateContent, cwd);
196
- }
197
-
198
- // Archive phase directories if requested
199
- let phasesArchived = false;
200
- if (options.archivePhases) {
201
- try {
202
- const phaseArchiveDir = path.join(archiveDir, `${version}-phases`);
203
- fs.mkdirSync(phaseArchiveDir, { recursive: true });
204
-
205
- const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
206
- const phaseDirNames = phaseEntries.filter(e => e.isDirectory()).map(e => e.name);
207
- let archivedCount = 0;
208
- for (const dir of phaseDirNames) {
209
- if (!isDirInMilestone(dir)) continue;
210
- fs.renameSync(path.join(phasesDir, dir), path.join(phaseArchiveDir, dir));
211
- archivedCount++;
212
- }
213
- phasesArchived = archivedCount > 0;
214
- } catch {}
215
- }
216
-
217
- const result = {
218
- version,
219
- name: milestoneName,
220
- date: today,
221
- phases: phaseCount,
222
- plans: totalPlans,
223
- tasks: totalTasks,
224
- accomplishments,
225
- archived: {
226
- roadmap: fs.existsSync(path.join(archiveDir, `${version}-ROADMAP.md`)),
227
- requirements: fs.existsSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`)),
228
- audit: fs.existsSync(path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)),
229
- phases: phasesArchived,
230
- },
231
- milestones_updated: true,
232
- state_updated: fs.existsSync(statePath),
233
- };
234
-
235
- output(result, raw);
236
- }
237
-
238
- module.exports = {
239
- cmdRequirementsMarkComplete,
240
- cmdMilestoneComplete,
241
- };
1
+ /**
2
+ * Milestone — Milestone and requirements lifecycle operations
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { escapeRegex, getMilestonePhaseFilter, output, error } = require('./core.cjs');
8
+ const { extractFrontmatter } = require('./frontmatter.cjs');
9
+ const { writeStateMd } = require('./state.cjs');
10
+
11
+ function cmdRequirementsMarkComplete(cwd, reqIdsRaw, raw) {
12
+ if (!reqIdsRaw || reqIdsRaw.length === 0) {
13
+ error('requirement IDs required. Usage: requirements mark-complete REQ-01,REQ-02 or REQ-01 REQ-02');
14
+ }
15
+
16
+ // Accept comma-separated, space-separated, or bracket-wrapped: [REQ-01, REQ-02]
17
+ const reqIds = reqIdsRaw
18
+ .join(' ')
19
+ .replace(/[\[\]]/g, '')
20
+ .split(/[,\s]+/)
21
+ .map(r => r.trim())
22
+ .filter(Boolean);
23
+
24
+ if (reqIds.length === 0) {
25
+ error('no valid requirement IDs found');
26
+ }
27
+
28
+ const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
29
+ if (!fs.existsSync(reqPath)) {
30
+ output({ updated: false, reason: 'REQUIREMENTS.md not found', ids: reqIds }, raw, 'no requirements file');
31
+ return;
32
+ }
33
+
34
+ let reqContent = fs.readFileSync(reqPath, 'utf-8');
35
+ const updated = [];
36
+ const notFound = [];
37
+
38
+ for (const reqId of reqIds) {
39
+ let found = false;
40
+ const reqEscaped = escapeRegex(reqId);
41
+
42
+ // Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID**
43
+ const checkboxPattern = new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi');
44
+ if (checkboxPattern.test(reqContent)) {
45
+ reqContent = reqContent.replace(checkboxPattern, '$1x$2');
46
+ found = true;
47
+ }
48
+
49
+ // Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete |
50
+ const tablePattern = new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi');
51
+ if (tablePattern.test(reqContent)) {
52
+ // Re-read since test() advances lastIndex for global regex
53
+ reqContent = reqContent.replace(
54
+ new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'),
55
+ '$1 Complete $2'
56
+ );
57
+ found = true;
58
+ }
59
+
60
+ if (found) {
61
+ updated.push(reqId);
62
+ } else {
63
+ notFound.push(reqId);
64
+ }
65
+ }
66
+
67
+ if (updated.length > 0) {
68
+ fs.writeFileSync(reqPath, reqContent, 'utf-8');
69
+ }
70
+
71
+ output({
72
+ updated: updated.length > 0,
73
+ marked_complete: updated,
74
+ not_found: notFound,
75
+ total: reqIds.length,
76
+ }, raw, `${updated.length}/${reqIds.length} requirements marked complete`);
77
+ }
78
+
79
+ function cmdMilestoneComplete(cwd, version, options, raw) {
80
+ if (!version) {
81
+ error('version required for milestone complete (e.g., v1.0)');
82
+ }
83
+
84
+ const roadmapPath = path.join(cwd, '.planning', 'ROADMAP.md');
85
+ const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md');
86
+ const statePath = path.join(cwd, '.planning', 'STATE.md');
87
+ const milestonesPath = path.join(cwd, '.planning', 'MILESTONES.md');
88
+ const archiveDir = path.join(cwd, '.planning', 'milestones');
89
+ const phasesDir = path.join(cwd, '.planning', 'phases');
90
+ const today = new Date().toISOString().split('T')[0];
91
+ const milestoneName = options.name || version;
92
+
93
+ // Ensure archive directory exists
94
+ fs.mkdirSync(archiveDir, { recursive: true });
95
+
96
+ // Scope stats and accomplishments to only the phases belonging to the
97
+ // current milestone's ROADMAP. Uses the shared filter from core.cjs
98
+ // (same logic used by cmdPhasesList and other callers).
99
+ const isDirInMilestone = getMilestonePhaseFilter(cwd);
100
+
101
+ // Gather stats from phases (scoped to current milestone only)
102
+ let phaseCount = 0;
103
+ let totalPlans = 0;
104
+ let totalTasks = 0;
105
+ const accomplishments = [];
106
+
107
+ try {
108
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
109
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
110
+
111
+ for (const dir of dirs) {
112
+ if (!isDirInMilestone(dir)) continue;
113
+
114
+ phaseCount++;
115
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
116
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
117
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
118
+ totalPlans += plans.length;
119
+
120
+ // Extract one-liners from summaries
121
+ for (const s of summaries) {
122
+ try {
123
+ const content = fs.readFileSync(path.join(phasesDir, dir, s), 'utf-8');
124
+ const fm = extractFrontmatter(content);
125
+ if (fm['one-liner']) {
126
+ accomplishments.push(fm['one-liner']);
127
+ }
128
+ // Count tasks
129
+ const taskMatches = content.match(/##\s*Task\s*\d+/gi) || [];
130
+ totalTasks += taskMatches.length;
131
+ } catch {}
132
+ }
133
+ }
134
+ } catch {}
135
+
136
+ // Archive ROADMAP.md
137
+ if (fs.existsSync(roadmapPath)) {
138
+ const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
139
+ fs.writeFileSync(path.join(archiveDir, `${version}-ROADMAP.md`), roadmapContent, 'utf-8');
140
+ }
141
+
142
+ // Archive REQUIREMENTS.md
143
+ if (fs.existsSync(reqPath)) {
144
+ const reqContent = fs.readFileSync(reqPath, 'utf-8');
145
+ const archiveHeader = `# Requirements Archive: ${version} ${milestoneName}\n\n**Archived:** ${today}\n**Status:** SHIPPED\n\nFor current requirements, see \`.planning/REQUIREMENTS.md\`.\n\n---\n\n`;
146
+ fs.writeFileSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`), archiveHeader + reqContent, 'utf-8');
147
+ }
148
+
149
+ // Archive audit file if exists
150
+ const auditFile = path.join(cwd, '.planning', `${version}-MILESTONE-AUDIT.md`);
151
+ if (fs.existsSync(auditFile)) {
152
+ fs.renameSync(auditFile, path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`));
153
+ }
154
+
155
+ // Create/append MILESTONES.md entry
156
+ const accomplishmentsList = accomplishments.map(a => `- ${a}`).join('\n');
157
+ const milestoneEntry = `## ${version} ${milestoneName} (Shipped: ${today})\n\n**Phases completed:** ${phaseCount} phases, ${totalPlans} plans, ${totalTasks} tasks\n\n**Key accomplishments:**\n${accomplishmentsList || '- (none recorded)'}\n\n---\n\n`;
158
+
159
+ if (fs.existsSync(milestonesPath)) {
160
+ const existing = fs.readFileSync(milestonesPath, 'utf-8');
161
+ if (!existing.trim()) {
162
+ // Empty file — treat like new
163
+ fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
164
+ } else {
165
+ // Insert after the header line(s) for reverse chronological order (newest first)
166
+ const headerMatch = existing.match(/^(#{1,3}\s+[^\n]*\n\n?)/);
167
+ if (headerMatch) {
168
+ const header = headerMatch[1];
169
+ const rest = existing.slice(header.length);
170
+ fs.writeFileSync(milestonesPath, header + milestoneEntry + rest, 'utf-8');
171
+ } else {
172
+ // No recognizable header — prepend the entry
173
+ fs.writeFileSync(milestonesPath, milestoneEntry + existing, 'utf-8');
174
+ }
175
+ }
176
+ } else {
177
+ fs.writeFileSync(milestonesPath, `# Milestones\n\n${milestoneEntry}`, 'utf-8');
178
+ }
179
+
180
+ // Update STATE.md
181
+ if (fs.existsSync(statePath)) {
182
+ let stateContent = fs.readFileSync(statePath, 'utf-8');
183
+ stateContent = stateContent.replace(
184
+ /(\*\*Status:\*\*\s*).*/,
185
+ `$1${version} milestone complete`
186
+ );
187
+ stateContent = stateContent.replace(
188
+ /(\*\*Last Activity:\*\*\s*).*/,
189
+ `$1${today}`
190
+ );
191
+ stateContent = stateContent.replace(
192
+ /(\*\*Last Activity Description:\*\*\s*).*/,
193
+ `$1${version} milestone completed and archived`
194
+ );
195
+ writeStateMd(statePath, stateContent, cwd);
196
+ }
197
+
198
+ // Archive phase directories if requested
199
+ let phasesArchived = false;
200
+ if (options.archivePhases) {
201
+ try {
202
+ const phaseArchiveDir = path.join(archiveDir, `${version}-phases`);
203
+ fs.mkdirSync(phaseArchiveDir, { recursive: true });
204
+
205
+ const phaseEntries = fs.readdirSync(phasesDir, { withFileTypes: true });
206
+ const phaseDirNames = phaseEntries.filter(e => e.isDirectory()).map(e => e.name);
207
+ let archivedCount = 0;
208
+ for (const dir of phaseDirNames) {
209
+ if (!isDirInMilestone(dir)) continue;
210
+ fs.renameSync(path.join(phasesDir, dir), path.join(phaseArchiveDir, dir));
211
+ archivedCount++;
212
+ }
213
+ phasesArchived = archivedCount > 0;
214
+ } catch {}
215
+ }
216
+
217
+ const result = {
218
+ version,
219
+ name: milestoneName,
220
+ date: today,
221
+ phases: phaseCount,
222
+ plans: totalPlans,
223
+ tasks: totalTasks,
224
+ accomplishments,
225
+ archived: {
226
+ roadmap: fs.existsSync(path.join(archiveDir, `${version}-ROADMAP.md`)),
227
+ requirements: fs.existsSync(path.join(archiveDir, `${version}-REQUIREMENTS.md`)),
228
+ audit: fs.existsSync(path.join(archiveDir, `${version}-MILESTONE-AUDIT.md`)),
229
+ phases: phasesArchived,
230
+ },
231
+ milestones_updated: true,
232
+ state_updated: fs.existsSync(statePath),
233
+ };
234
+
235
+ output(result, raw);
236
+ }
237
+
238
+ module.exports = {
239
+ cmdRequirementsMarkComplete,
240
+ cmdMilestoneComplete,
241
+ };