@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,757 +1,757 @@
1
- /**
2
- * Init — Compound init commands for workflow bootstrapping
3
- */
4
-
5
- const fs = require('fs');
6
- const path = require('path');
7
- const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs');
8
- const { defaultLogger: logger } = require('./logger.cjs');
9
- const { findFiles } = require('./fs-utils.cjs');
10
- const { execWithTimeout } = require('./timeout-exec.cjs');
11
-
12
- function cmdInitExecutePhase(cwd, phase, raw) {
13
- if (!phase) {
14
- error('phase required for init execute-phase');
15
- }
16
-
17
- const config = loadConfig(cwd);
18
- const phaseInfo = findPhaseInternal(cwd, phase);
19
- const milestone = getMilestoneInfo(cwd);
20
-
21
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
22
- const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
23
- const reqExtracted = reqMatch
24
- ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
25
- : null;
26
- const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
27
-
28
- const result = {
29
- // Models
30
- executor_model: resolveModelInternal(cwd, 'ez-executor'),
31
- verifier_model: resolveModelInternal(cwd, 'ez-verifier'),
32
-
33
- // Config flags
34
- commit_docs: config.commit_docs,
35
- parallelization: config.parallelization,
36
- branching_strategy: config.branching_strategy,
37
- phase_branch_template: config.phase_branch_template,
38
- milestone_branch_template: config.milestone_branch_template,
39
- verifier_enabled: config.verifier,
40
-
41
- // Phase info
42
- phase_found: !!phaseInfo,
43
- phase_dir: phaseInfo?.directory || null,
44
- phase_number: phaseInfo?.phase_number || null,
45
- phase_name: phaseInfo?.phase_name || null,
46
- phase_slug: phaseInfo?.phase_slug || null,
47
- phase_req_ids,
48
-
49
- // Plan inventory
50
- plans: phaseInfo?.plans || [],
51
- summaries: phaseInfo?.summaries || [],
52
- incomplete_plans: phaseInfo?.incomplete_plans || [],
53
- plan_count: phaseInfo?.plans?.length || 0,
54
- incomplete_count: phaseInfo?.incomplete_plans?.length || 0,
55
-
56
- // Branch name (pre-computed)
57
- branch_name: config.branching_strategy === 'phase' && phaseInfo
58
- ? config.phase_branch_template
59
- .replace('{phase}', phaseInfo.phase_number)
60
- .replace('{slug}', phaseInfo.phase_slug || 'phase')
61
- : config.branching_strategy === 'milestone'
62
- ? config.milestone_branch_template
63
- .replace('{milestone}', milestone.version)
64
- .replace('{slug}', generateSlugInternal(milestone.name) || 'milestone')
65
- : null,
66
-
67
- // Milestone info
68
- milestone_version: milestone.version,
69
- milestone_name: milestone.name,
70
- milestone_slug: generateSlugInternal(milestone.name),
71
-
72
- // File existence
73
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
74
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
75
- config_exists: pathExistsInternal(cwd, '.planning/config.json'),
76
- // File paths
77
- state_path: '.planning/STATE.md',
78
- roadmap_path: '.planning/ROADMAP.md',
79
- config_path: '.planning/config.json',
80
- };
81
-
82
- output(result, raw);
83
- }
84
-
85
- function cmdInitPlanPhase(cwd, phase, raw) {
86
- if (!phase) {
87
- error('phase required for init plan-phase');
88
- }
89
-
90
- const config = loadConfig(cwd);
91
- const phaseInfo = findPhaseInternal(cwd, phase);
92
-
93
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
94
- const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
95
- const reqExtracted = reqMatch
96
- ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
97
- : null;
98
- const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
99
-
100
- const result = {
101
- // Models
102
- researcher_model: resolveModelInternal(cwd, 'ez-phase-researcher'),
103
- planner_model: resolveModelInternal(cwd, 'ez-planner'),
104
- checker_model: resolveModelInternal(cwd, 'ez-plan-checker'),
105
-
106
- // Workflow flags
107
- research_enabled: config.research,
108
- plan_checker_enabled: config.plan_checker,
109
- nyquist_validation_enabled: config.nyquist_validation,
110
- commit_docs: config.commit_docs,
111
-
112
- // Phase info
113
- phase_found: !!phaseInfo,
114
- phase_dir: phaseInfo?.directory || null,
115
- phase_number: phaseInfo?.phase_number || null,
116
- phase_name: phaseInfo?.phase_name || null,
117
- phase_slug: phaseInfo?.phase_slug || null,
118
- padded_phase: phaseInfo?.phase_number ? normalizePhaseName(phaseInfo.phase_number) : null,
119
- phase_req_ids,
120
-
121
- // Existing artifacts
122
- has_research: phaseInfo?.has_research || false,
123
- has_context: phaseInfo?.has_context || false,
124
- has_plans: (phaseInfo?.plans?.length || 0) > 0,
125
- plan_count: phaseInfo?.plans?.length || 0,
126
-
127
- // Environment
128
- planning_exists: pathExistsInternal(cwd, '.planning'),
129
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
130
-
131
- // File paths
132
- state_path: '.planning/STATE.md',
133
- roadmap_path: '.planning/ROADMAP.md',
134
- requirements_path: '.planning/REQUIREMENTS.md',
135
- };
136
-
137
- if (phaseInfo?.directory) {
138
- // Find *-CONTEXT.md in phase directory
139
- const phaseDirFull = path.join(cwd, phaseInfo.directory);
140
- try {
141
- const files = fs.readdirSync(phaseDirFull);
142
- const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
143
- if (contextFile) {
144
- result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
145
- }
146
- const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
147
- if (researchFile) {
148
- result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
149
- }
150
- const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
151
- if (verificationFile) {
152
- result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
153
- }
154
- const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
155
- if (uatFile) {
156
- result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
157
- }
158
- } catch (err) {
159
- logger.warn('Failed to inspect phase artifacts in cmdInitPlanPhase', { phaseDirFull, error: err.message });
160
- }
161
- }
162
-
163
- output(result, raw);
164
- }
165
-
166
- async function cmdInitNewProject(cwd, raw) {
167
- const config = loadConfig(cwd);
168
-
169
- // Detect Brave Search API key availability (prefer ~/.ez)
170
- const homedir = require('os').homedir();
171
- const braveKeyCandidates = [
172
- path.join(homedir, '.ez', 'brave_api_key'),
173
- ];
174
- const hasBraveSearch = !!(process.env.BRAVE_API_KEY || braveKeyCandidates.some(p => fs.existsSync(p)));
175
-
176
- // Detect existing code
177
- let hasCode = false;
178
- let hasPackageFile = false;
179
- try {
180
- const codeFiles = findFiles(cwd, [
181
- /\.ts$/,
182
- /\.js$/,
183
- /\.py$/,
184
- /\.go$/,
185
- /\.rs$/,
186
- /\.swift$/,
187
- /\.java$/,
188
- ], {
189
- maxDepth: 3,
190
- exclude: ['node_modules', '.git'],
191
- });
192
- hasCode = codeFiles.length > 0;
193
- } catch (err) {
194
- logger.warn('Failed to detect existing source files in cmdInitNewProject', { cwd, error: err.message });
195
- }
196
-
197
- hasPackageFile = pathExistsInternal(cwd, 'package.json') ||
198
- pathExistsInternal(cwd, 'requirements.txt') ||
199
- pathExistsInternal(cwd, 'Cargo.toml') ||
200
- pathExistsInternal(cwd, 'go.mod') ||
201
- pathExistsInternal(cwd, 'Package.swift');
202
-
203
- let hasGit = pathExistsInternal(cwd, '.git');
204
- try {
205
- const gitProbe = await execWithTimeout('git', ['rev-parse', '--is-inside-work-tree'], { timeout: 5000, fallback: '' });
206
- if (gitProbe === '') {
207
- logger.info('Fallback activated during init new-project git probe', { command: 'git rev-parse --is-inside-work-tree' });
208
- } else {
209
- hasGit = gitProbe.trim() === 'true' || hasGit;
210
- }
211
- } catch (err) {
212
- logger.warn('Init new-project git probe failed without fallback', { error: err.message });
213
- }
214
-
215
- const result = {
216
- // Models
217
- researcher_model: resolveModelInternal(cwd, 'ez-project-researcher'),
218
- synthesizer_model: resolveModelInternal(cwd, 'ez-research-synthesizer'),
219
- roadmapper_model: resolveModelInternal(cwd, 'ez-roadmapper'),
220
-
221
- // Config
222
- commit_docs: config.commit_docs,
223
-
224
- // Existing state
225
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
226
- has_codebase_map: pathExistsInternal(cwd, '.planning/codebase'),
227
- planning_exists: pathExistsInternal(cwd, '.planning'),
228
-
229
- // Brownfield detection
230
- has_existing_code: hasCode,
231
- has_package_file: hasPackageFile,
232
- is_brownfield: hasCode || hasPackageFile,
233
- needs_codebase_map: (hasCode || hasPackageFile) && !pathExistsInternal(cwd, '.planning/codebase'),
234
-
235
- // Git state
236
- has_git: hasGit,
237
-
238
- // Enhanced search
239
- brave_search_available: hasBraveSearch,
240
-
241
- // File paths
242
- project_path: '.planning/PROJECT.md',
243
- };
244
-
245
- output(result, raw);
246
- }
247
-
248
- function cmdInitNewMilestone(cwd, raw) {
249
- const config = loadConfig(cwd);
250
- const milestone = getMilestoneInfo(cwd);
251
-
252
- const result = {
253
- // Models
254
- researcher_model: resolveModelInternal(cwd, 'ez-project-researcher'),
255
- synthesizer_model: resolveModelInternal(cwd, 'ez-research-synthesizer'),
256
- roadmapper_model: resolveModelInternal(cwd, 'ez-roadmapper'),
257
-
258
- // Config
259
- commit_docs: config.commit_docs,
260
- research_enabled: config.research,
261
-
262
- // Current milestone
263
- current_milestone: milestone.version,
264
- current_milestone_name: milestone.name,
265
-
266
- // File existence
267
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
268
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
269
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
270
-
271
- // File paths
272
- project_path: '.planning/PROJECT.md',
273
- roadmap_path: '.planning/ROADMAP.md',
274
- state_path: '.planning/STATE.md',
275
- };
276
-
277
- output(result, raw);
278
- }
279
-
280
- function cmdInitQuick(cwd, description, raw) {
281
- const config = loadConfig(cwd);
282
- const now = new Date();
283
- const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
284
-
285
- // Generate collision-resistant quick task ID: YYMMDD-xxx
286
- // xxx = 2-second precision blocks since midnight, encoded as 3-char Base36 (lowercase)
287
- // Range: 000 (00:00:00) to xbz (23:59:58), guaranteed 3 chars for any time of day.
288
- // Provides ~2s uniqueness window per user — practically collision-free across a team.
289
- const yy = String(now.getFullYear()).slice(-2);
290
- const mm = String(now.getMonth() + 1).padStart(2, '0');
291
- const dd = String(now.getDate()).padStart(2, '0');
292
- const dateStr = yy + mm + dd;
293
- const secondsSinceMidnight = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
294
- const timeBlocks = Math.floor(secondsSinceMidnight / 2);
295
- const timeEncoded = timeBlocks.toString(36).padStart(3, '0');
296
- const quickId = dateStr + '-' + timeEncoded;
297
-
298
- const result = {
299
- // Models
300
- planner_model: resolveModelInternal(cwd, 'ez-planner'),
301
- executor_model: resolveModelInternal(cwd, 'ez-executor'),
302
- checker_model: resolveModelInternal(cwd, 'ez-plan-checker'),
303
- verifier_model: resolveModelInternal(cwd, 'ez-verifier'),
304
-
305
- // Config
306
- commit_docs: config.commit_docs,
307
-
308
- // Quick task info
309
- quick_id: quickId,
310
- slug: slug,
311
- description: description || null,
312
-
313
- // Timestamps
314
- date: now.toISOString().split('T')[0],
315
- timestamp: now.toISOString(),
316
-
317
- // Paths
318
- quick_dir: '.planning/quick',
319
- task_dir: slug ? `.planning/quick/${quickId}-${slug}` : null,
320
-
321
- // File existence
322
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
323
- planning_exists: pathExistsInternal(cwd, '.planning'),
324
-
325
- };
326
-
327
- output(result, raw);
328
- }
329
-
330
- function cmdInitResume(cwd, raw) {
331
- const config = loadConfig(cwd);
332
-
333
- // Check for interrupted agent
334
- let interruptedAgentId = null;
335
- try {
336
- interruptedAgentId = fs.readFileSync(path.join(cwd, '.planning', 'current-agent-id.txt'), 'utf-8').trim();
337
- } catch (err) {
338
- logger.warn('Failed to read current-agent-id marker in cmdInitResume', { cwd, error: err.message });
339
- }
340
-
341
- const result = {
342
- // File existence
343
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
344
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
345
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
346
- planning_exists: pathExistsInternal(cwd, '.planning'),
347
-
348
- // File paths
349
- state_path: '.planning/STATE.md',
350
- roadmap_path: '.planning/ROADMAP.md',
351
- project_path: '.planning/PROJECT.md',
352
-
353
- // Agent state
354
- has_interrupted_agent: !!interruptedAgentId,
355
- interrupted_agent_id: interruptedAgentId,
356
-
357
- // Config
358
- commit_docs: config.commit_docs,
359
- };
360
-
361
- output(result, raw);
362
- }
363
-
364
- function cmdInitVerifyWork(cwd, phase, raw) {
365
- if (!phase) {
366
- error('phase required for init verify-work');
367
- }
368
-
369
- const config = loadConfig(cwd);
370
- const phaseInfo = findPhaseInternal(cwd, phase);
371
-
372
- const result = {
373
- // Models
374
- planner_model: resolveModelInternal(cwd, 'ez-planner'),
375
- checker_model: resolveModelInternal(cwd, 'ez-plan-checker'),
376
-
377
- // Config
378
- commit_docs: config.commit_docs,
379
-
380
- // Phase info
381
- phase_found: !!phaseInfo,
382
- phase_dir: phaseInfo?.directory || null,
383
- phase_number: phaseInfo?.phase_number || null,
384
- phase_name: phaseInfo?.phase_name || null,
385
-
386
- // Existing artifacts
387
- has_verification: phaseInfo?.has_verification || false,
388
- };
389
-
390
- output(result, raw);
391
- }
392
-
393
- function cmdInitPhaseOp(cwd, phase, raw) {
394
- const config = loadConfig(cwd);
395
- let phaseInfo = findPhaseInternal(cwd, phase);
396
-
397
- // Fallback to ROADMAP.md if no directory exists (e.g., Plans: TBD)
398
- if (!phaseInfo) {
399
- const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
400
- if (roadmapPhase?.found) {
401
- const phaseName = roadmapPhase.phase_name;
402
- phaseInfo = {
403
- found: true,
404
- directory: null,
405
- phase_number: roadmapPhase.phase_number,
406
- phase_name: phaseName,
407
- phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
408
- plans: [],
409
- summaries: [],
410
- incomplete_plans: [],
411
- has_research: false,
412
- has_context: false,
413
- has_verification: false,
414
- };
415
- }
416
- }
417
-
418
- const result = {
419
- // Config
420
- commit_docs: config.commit_docs,
421
- brave_search: config.brave_search,
422
-
423
- // Phase info
424
- phase_found: !!phaseInfo,
425
- phase_dir: phaseInfo?.directory || null,
426
- phase_number: phaseInfo?.phase_number || null,
427
- phase_name: phaseInfo?.phase_name || null,
428
- phase_slug: phaseInfo?.phase_slug || null,
429
- padded_phase: phaseInfo?.phase_number ? normalizePhaseName(phaseInfo.phase_number) : null,
430
-
431
- // Existing artifacts
432
- has_research: phaseInfo?.has_research || false,
433
- has_context: phaseInfo?.has_context || false,
434
- has_plans: (phaseInfo?.plans?.length || 0) > 0,
435
- has_verification: phaseInfo?.has_verification || false,
436
- plan_count: phaseInfo?.plans?.length || 0,
437
-
438
- // File existence
439
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
440
- planning_exists: pathExistsInternal(cwd, '.planning'),
441
-
442
- // File paths
443
- state_path: '.planning/STATE.md',
444
- roadmap_path: '.planning/ROADMAP.md',
445
- requirements_path: '.planning/REQUIREMENTS.md',
446
- };
447
-
448
- if (phaseInfo?.directory) {
449
- const phaseDirFull = path.join(cwd, phaseInfo.directory);
450
- try {
451
- const files = fs.readdirSync(phaseDirFull);
452
- const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
453
- if (contextFile) {
454
- result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
455
- }
456
- const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
457
- if (researchFile) {
458
- result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
459
- }
460
- const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
461
- if (verificationFile) {
462
- result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
463
- }
464
- const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
465
- if (uatFile) {
466
- result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
467
- }
468
- } catch (err) {
469
- logger.warn('Failed to inspect phase artifacts in cmdInitPhaseOp', { phaseDirFull, error: err.message });
470
- }
471
- }
472
-
473
- output(result, raw);
474
- }
475
-
476
- function cmdInitTodos(cwd, area, raw) {
477
- const config = loadConfig(cwd);
478
- const now = new Date();
479
-
480
- // List todos (reuse existing logic)
481
- const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
482
- let count = 0;
483
- const todos = [];
484
-
485
- try {
486
- const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
487
- for (const file of files) {
488
- try {
489
- const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
490
- const createdMatch = content.match(/^created:\s*(.+)$/m);
491
- const titleMatch = content.match(/^title:\s*(.+)$/m);
492
- const areaMatch = content.match(/^area:\s*(.+)$/m);
493
- const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
494
-
495
- if (area && todoArea !== area) continue;
496
-
497
- count++;
498
- todos.push({
499
- file,
500
- created: createdMatch ? createdMatch[1].trim() : 'unknown',
501
- title: titleMatch ? titleMatch[1].trim() : 'Untitled',
502
- area: todoArea,
503
- path: '.planning/todos/pending/' + file,
504
- });
505
- } catch (err) {
506
- logger.warn('Failed to parse todo file in cmdInitTodos', { file, error: err.message });
507
- }
508
- }
509
- } catch (err) {
510
- logger.warn('Failed to list pending todos in cmdInitTodos', { pendingDir, error: err.message });
511
- }
512
-
513
- const result = {
514
- // Config
515
- commit_docs: config.commit_docs,
516
-
517
- // Timestamps
518
- date: now.toISOString().split('T')[0],
519
- timestamp: now.toISOString(),
520
-
521
- // Todo inventory
522
- todo_count: count,
523
- todos,
524
- area_filter: area || null,
525
-
526
- // Paths
527
- pending_dir: '.planning/todos/pending',
528
- completed_dir: '.planning/todos/completed',
529
-
530
- // File existence
531
- planning_exists: pathExistsInternal(cwd, '.planning'),
532
- todos_dir_exists: pathExistsInternal(cwd, '.planning/todos'),
533
- pending_dir_exists: pathExistsInternal(cwd, '.planning/todos/pending'),
534
- };
535
-
536
- output(result, raw);
537
- }
538
-
539
- function cmdInitMilestoneOp(cwd, raw) {
540
- const config = loadConfig(cwd);
541
- const milestone = getMilestoneInfo(cwd);
542
-
543
- // Count phases
544
- let phaseCount = 0;
545
- let completedPhases = 0;
546
- const phasesDir = path.join(cwd, '.planning', 'phases');
547
- try {
548
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
549
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
550
- phaseCount = dirs.length;
551
-
552
- // Count phases with summaries (completed)
553
- for (const dir of dirs) {
554
- try {
555
- const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
556
- const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
557
- if (hasSummary) completedPhases++;
558
- } catch (err) {
559
- logger.warn('Failed to inspect phase directory in cmdInitMilestoneOp', { dir, error: err.message });
560
- }
561
- }
562
- } catch (err) {
563
- logger.warn('Failed to list phase directories in cmdInitMilestoneOp', { phasesDir, error: err.message });
564
- }
565
-
566
- // Check archive
567
- const archiveDir = path.join(cwd, '.planning', 'archive');
568
- let archivedMilestones = [];
569
- try {
570
- archivedMilestones = fs.readdirSync(archiveDir, { withFileTypes: true })
571
- .filter(e => e.isDirectory())
572
- .map(e => e.name);
573
- } catch (err) {
574
- logger.warn('Failed to list archived milestones in cmdInitMilestoneOp', { archiveDir, error: err.message });
575
- }
576
-
577
- const result = {
578
- // Config
579
- commit_docs: config.commit_docs,
580
-
581
- // Current milestone
582
- milestone_version: milestone.version,
583
- milestone_name: milestone.name,
584
- milestone_slug: generateSlugInternal(milestone.name),
585
-
586
- // Phase counts
587
- phase_count: phaseCount,
588
- completed_phases: completedPhases,
589
- all_phases_complete: phaseCount > 0 && phaseCount === completedPhases,
590
-
591
- // Archive
592
- archived_milestones: archivedMilestones,
593
- archive_count: archivedMilestones.length,
594
-
595
- // File existence
596
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
597
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
598
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
599
- archive_exists: pathExistsInternal(cwd, '.planning/archive'),
600
- phases_dir_exists: pathExistsInternal(cwd, '.planning/phases'),
601
- };
602
-
603
- output(result, raw);
604
- }
605
-
606
- function cmdInitMapCodebase(cwd, raw) {
607
- const config = loadConfig(cwd);
608
-
609
- // Check for existing codebase maps
610
- const codebaseDir = path.join(cwd, '.planning', 'codebase');
611
- let existingMaps = [];
612
- try {
613
- existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
614
- } catch (err) {
615
- logger.warn('Failed to list codebase map files in cmdInitMapCodebase', { codebaseDir, error: err.message });
616
- }
617
-
618
- const result = {
619
- // Models
620
- mapper_model: resolveModelInternal(cwd, 'ez-codebase-mapper'),
621
-
622
- // Config
623
- commit_docs: config.commit_docs,
624
- search_gitignored: config.search_gitignored,
625
- parallelization: config.parallelization,
626
-
627
- // Paths
628
- codebase_dir: '.planning/codebase',
629
-
630
- // Existing maps
631
- existing_maps: existingMaps,
632
- has_maps: existingMaps.length > 0,
633
-
634
- // File existence
635
- planning_exists: pathExistsInternal(cwd, '.planning'),
636
- codebase_dir_exists: pathExistsInternal(cwd, '.planning/codebase'),
637
- };
638
-
639
- output(result, raw);
640
- }
641
-
642
- function cmdInitProgress(cwd, raw) {
643
- const config = loadConfig(cwd);
644
- const milestone = getMilestoneInfo(cwd);
645
-
646
- // Analyze phases
647
- const phasesDir = path.join(cwd, '.planning', 'phases');
648
- const phases = [];
649
- let currentPhase = null;
650
- let nextPhase = null;
651
-
652
- try {
653
- const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
654
- const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
655
-
656
- for (const dir of dirs) {
657
- const match = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
658
- const phaseNumber = match ? match[1] : dir;
659
- const phaseName = match && match[2] ? match[2] : null;
660
-
661
- const phasePath = path.join(phasesDir, dir);
662
- const phaseFiles = fs.readdirSync(phasePath);
663
-
664
- const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
665
- const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
666
- const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
667
-
668
- const status = summaries.length >= plans.length && plans.length > 0 ? 'complete' :
669
- plans.length > 0 ? 'in_progress' :
670
- hasResearch ? 'researched' : 'pending';
671
-
672
- const phaseInfo = {
673
- number: phaseNumber,
674
- name: phaseName,
675
- directory: '.planning/phases/' + dir,
676
- status,
677
- plan_count: plans.length,
678
- summary_count: summaries.length,
679
- has_research: hasResearch,
680
- };
681
-
682
- phases.push(phaseInfo);
683
-
684
- // Find current (first incomplete with plans) and next (first pending)
685
- if (!currentPhase && (status === 'in_progress' || status === 'researched')) {
686
- currentPhase = phaseInfo;
687
- }
688
- if (!nextPhase && status === 'pending') {
689
- nextPhase = phaseInfo;
690
- }
691
- }
692
- } catch (err) {
693
- logger.warn('Failed to analyze phase progress in cmdInitProgress', { phasesDir, error: err.message });
694
- }
695
-
696
- // Check for paused work
697
- let pausedAt = null;
698
- try {
699
- const state = fs.readFileSync(path.join(cwd, '.planning', 'STATE.md'), 'utf-8');
700
- const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/);
701
- if (pauseMatch) pausedAt = pauseMatch[1].trim();
702
- } catch (err) {
703
- logger.warn('Failed to read paused state in cmdInitProgress', { cwd, error: err.message });
704
- }
705
-
706
- const result = {
707
- // Models
708
- executor_model: resolveModelInternal(cwd, 'ez-executor'),
709
- planner_model: resolveModelInternal(cwd, 'ez-planner'),
710
-
711
- // Config
712
- commit_docs: config.commit_docs,
713
-
714
- // Milestone
715
- milestone_version: milestone.version,
716
- milestone_name: milestone.name,
717
-
718
- // Phase overview
719
- phases,
720
- phase_count: phases.length,
721
- completed_count: phases.filter(p => p.status === 'complete').length,
722
- in_progress_count: phases.filter(p => p.status === 'in_progress').length,
723
-
724
- // Current state
725
- current_phase: currentPhase,
726
- next_phase: nextPhase,
727
- paused_at: pausedAt,
728
- has_work_in_progress: !!currentPhase,
729
-
730
- // File existence
731
- project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
732
- roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
733
- state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
734
- // File paths
735
- state_path: '.planning/STATE.md',
736
- roadmap_path: '.planning/ROADMAP.md',
737
- project_path: '.planning/PROJECT.md',
738
- config_path: '.planning/config.json',
739
- };
740
-
741
- output(result, raw);
742
- }
743
-
744
- module.exports = {
745
- cmdInitExecutePhase,
746
- cmdInitPlanPhase,
747
- cmdInitNewProject,
748
- cmdInitNewMilestone,
749
- cmdInitQuick,
750
- cmdInitResume,
751
- cmdInitVerifyWork,
752
- cmdInitPhaseOp,
753
- cmdInitTodos,
754
- cmdInitMilestoneOp,
755
- cmdInitMapCodebase,
756
- cmdInitProgress,
757
- };
1
+ /**
2
+ * Init — Compound init commands for workflow bootstrapping
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { loadConfig, resolveModelInternal, findPhaseInternal, getRoadmapPhaseInternal, pathExistsInternal, generateSlugInternal, getMilestoneInfo, normalizePhaseName, toPosixPath, output, error } = require('./core.cjs');
8
+ const { defaultLogger: logger } = require('./logger.cjs');
9
+ const { findFiles } = require('./fs-utils.cjs');
10
+ const { execWithTimeout } = require('./timeout-exec.cjs');
11
+
12
+ function cmdInitExecutePhase(cwd, phase, raw) {
13
+ if (!phase) {
14
+ error('phase required for init execute-phase');
15
+ }
16
+
17
+ const config = loadConfig(cwd);
18
+ const phaseInfo = findPhaseInternal(cwd, phase);
19
+ const milestone = getMilestoneInfo(cwd);
20
+
21
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
22
+ const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
23
+ const reqExtracted = reqMatch
24
+ ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
25
+ : null;
26
+ const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
27
+
28
+ const result = {
29
+ // Models
30
+ executor_model: resolveModelInternal(cwd, 'ez-executor'),
31
+ verifier_model: resolveModelInternal(cwd, 'ez-verifier'),
32
+
33
+ // Config flags
34
+ commit_docs: config.commit_docs,
35
+ parallelization: config.parallelization,
36
+ branching_strategy: config.branching_strategy,
37
+ phase_branch_template: config.phase_branch_template,
38
+ milestone_branch_template: config.milestone_branch_template,
39
+ verifier_enabled: config.verifier,
40
+
41
+ // Phase info
42
+ phase_found: !!phaseInfo,
43
+ phase_dir: phaseInfo?.directory || null,
44
+ phase_number: phaseInfo?.phase_number || null,
45
+ phase_name: phaseInfo?.phase_name || null,
46
+ phase_slug: phaseInfo?.phase_slug || null,
47
+ phase_req_ids,
48
+
49
+ // Plan inventory
50
+ plans: phaseInfo?.plans || [],
51
+ summaries: phaseInfo?.summaries || [],
52
+ incomplete_plans: phaseInfo?.incomplete_plans || [],
53
+ plan_count: phaseInfo?.plans?.length || 0,
54
+ incomplete_count: phaseInfo?.incomplete_plans?.length || 0,
55
+
56
+ // Branch name (pre-computed)
57
+ branch_name: config.branching_strategy === 'phase' && phaseInfo
58
+ ? config.phase_branch_template
59
+ .replace('{phase}', phaseInfo.phase_number)
60
+ .replace('{slug}', phaseInfo.phase_slug || 'phase')
61
+ : config.branching_strategy === 'milestone'
62
+ ? config.milestone_branch_template
63
+ .replace('{milestone}', milestone.version)
64
+ .replace('{slug}', generateSlugInternal(milestone.name) || 'milestone')
65
+ : null,
66
+
67
+ // Milestone info
68
+ milestone_version: milestone.version,
69
+ milestone_name: milestone.name,
70
+ milestone_slug: generateSlugInternal(milestone.name),
71
+
72
+ // File existence
73
+ state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
74
+ roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
75
+ config_exists: pathExistsInternal(cwd, '.planning/config.json'),
76
+ // File paths
77
+ state_path: '.planning/STATE.md',
78
+ roadmap_path: '.planning/ROADMAP.md',
79
+ config_path: '.planning/config.json',
80
+ };
81
+
82
+ output(result, raw);
83
+ }
84
+
85
+ function cmdInitPlanPhase(cwd, phase, raw) {
86
+ if (!phase) {
87
+ error('phase required for init plan-phase');
88
+ }
89
+
90
+ const config = loadConfig(cwd);
91
+ const phaseInfo = findPhaseInternal(cwd, phase);
92
+
93
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
94
+ const reqMatch = roadmapPhase?.section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
95
+ const reqExtracted = reqMatch
96
+ ? reqMatch[1].replace(/[\[\]]/g, '').split(',').map(s => s.trim()).filter(Boolean).join(', ')
97
+ : null;
98
+ const phase_req_ids = (reqExtracted && reqExtracted !== 'TBD') ? reqExtracted : null;
99
+
100
+ const result = {
101
+ // Models
102
+ researcher_model: resolveModelInternal(cwd, 'ez-phase-researcher'),
103
+ planner_model: resolveModelInternal(cwd, 'ez-planner'),
104
+ checker_model: resolveModelInternal(cwd, 'ez-plan-checker'),
105
+
106
+ // Workflow flags
107
+ research_enabled: config.research,
108
+ plan_checker_enabled: config.plan_checker,
109
+ nyquist_validation_enabled: config.nyquist_validation,
110
+ commit_docs: config.commit_docs,
111
+
112
+ // Phase info
113
+ phase_found: !!phaseInfo,
114
+ phase_dir: phaseInfo?.directory || null,
115
+ phase_number: phaseInfo?.phase_number || null,
116
+ phase_name: phaseInfo?.phase_name || null,
117
+ phase_slug: phaseInfo?.phase_slug || null,
118
+ padded_phase: phaseInfo?.phase_number ? normalizePhaseName(phaseInfo.phase_number) : null,
119
+ phase_req_ids,
120
+
121
+ // Existing artifacts
122
+ has_research: phaseInfo?.has_research || false,
123
+ has_context: phaseInfo?.has_context || false,
124
+ has_plans: (phaseInfo?.plans?.length || 0) > 0,
125
+ plan_count: phaseInfo?.plans?.length || 0,
126
+
127
+ // Environment
128
+ planning_exists: pathExistsInternal(cwd, '.planning'),
129
+ roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
130
+
131
+ // File paths
132
+ state_path: '.planning/STATE.md',
133
+ roadmap_path: '.planning/ROADMAP.md',
134
+ requirements_path: '.planning/REQUIREMENTS.md',
135
+ };
136
+
137
+ if (phaseInfo?.directory) {
138
+ // Find *-CONTEXT.md in phase directory
139
+ const phaseDirFull = path.join(cwd, phaseInfo.directory);
140
+ try {
141
+ const files = fs.readdirSync(phaseDirFull);
142
+ const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
143
+ if (contextFile) {
144
+ result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
145
+ }
146
+ const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
147
+ if (researchFile) {
148
+ result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
149
+ }
150
+ const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
151
+ if (verificationFile) {
152
+ result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
153
+ }
154
+ const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
155
+ if (uatFile) {
156
+ result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
157
+ }
158
+ } catch (err) {
159
+ logger.warn('Failed to inspect phase artifacts in cmdInitPlanPhase', { phaseDirFull, error: err.message });
160
+ }
161
+ }
162
+
163
+ output(result, raw);
164
+ }
165
+
166
+ async function cmdInitNewProject(cwd, raw) {
167
+ const config = loadConfig(cwd);
168
+
169
+ // Detect Brave Search API key availability (prefer ~/.ez)
170
+ const homedir = require('os').homedir();
171
+ const braveKeyCandidates = [
172
+ path.join(homedir, '.ez', 'brave_api_key'),
173
+ ];
174
+ const hasBraveSearch = !!(process.env.BRAVE_API_KEY || braveKeyCandidates.some(p => fs.existsSync(p)));
175
+
176
+ // Detect existing code
177
+ let hasCode = false;
178
+ let hasPackageFile = false;
179
+ try {
180
+ const codeFiles = findFiles(cwd, [
181
+ /\.ts$/,
182
+ /\.js$/,
183
+ /\.py$/,
184
+ /\.go$/,
185
+ /\.rs$/,
186
+ /\.swift$/,
187
+ /\.java$/,
188
+ ], {
189
+ maxDepth: 3,
190
+ exclude: ['node_modules', '.git'],
191
+ });
192
+ hasCode = codeFiles.length > 0;
193
+ } catch (err) {
194
+ logger.warn('Failed to detect existing source files in cmdInitNewProject', { cwd, error: err.message });
195
+ }
196
+
197
+ hasPackageFile = pathExistsInternal(cwd, 'package.json') ||
198
+ pathExistsInternal(cwd, 'requirements.txt') ||
199
+ pathExistsInternal(cwd, 'Cargo.toml') ||
200
+ pathExistsInternal(cwd, 'go.mod') ||
201
+ pathExistsInternal(cwd, 'Package.swift');
202
+
203
+ let hasGit = pathExistsInternal(cwd, '.git');
204
+ try {
205
+ const gitProbe = await execWithTimeout('git', ['rev-parse', '--is-inside-work-tree'], { timeout: 5000, fallback: '' });
206
+ if (gitProbe === '') {
207
+ logger.info('Fallback activated during init new-project git probe', { command: 'git rev-parse --is-inside-work-tree' });
208
+ } else {
209
+ hasGit = gitProbe.trim() === 'true' || hasGit;
210
+ }
211
+ } catch (err) {
212
+ logger.warn('Init new-project git probe failed without fallback', { error: err.message });
213
+ }
214
+
215
+ const result = {
216
+ // Models
217
+ researcher_model: resolveModelInternal(cwd, 'ez-project-researcher'),
218
+ synthesizer_model: resolveModelInternal(cwd, 'ez-research-synthesizer'),
219
+ roadmapper_model: resolveModelInternal(cwd, 'ez-roadmapper'),
220
+
221
+ // Config
222
+ commit_docs: config.commit_docs,
223
+
224
+ // Existing state
225
+ project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
226
+ has_codebase_map: pathExistsInternal(cwd, '.planning/codebase'),
227
+ planning_exists: pathExistsInternal(cwd, '.planning'),
228
+
229
+ // Brownfield detection
230
+ has_existing_code: hasCode,
231
+ has_package_file: hasPackageFile,
232
+ is_brownfield: hasCode || hasPackageFile,
233
+ needs_codebase_map: (hasCode || hasPackageFile) && !pathExistsInternal(cwd, '.planning/codebase'),
234
+
235
+ // Git state
236
+ has_git: hasGit,
237
+
238
+ // Enhanced search
239
+ brave_search_available: hasBraveSearch,
240
+
241
+ // File paths
242
+ project_path: '.planning/PROJECT.md',
243
+ };
244
+
245
+ output(result, raw);
246
+ }
247
+
248
+ function cmdInitNewMilestone(cwd, raw) {
249
+ const config = loadConfig(cwd);
250
+ const milestone = getMilestoneInfo(cwd);
251
+
252
+ const result = {
253
+ // Models
254
+ researcher_model: resolveModelInternal(cwd, 'ez-project-researcher'),
255
+ synthesizer_model: resolveModelInternal(cwd, 'ez-research-synthesizer'),
256
+ roadmapper_model: resolveModelInternal(cwd, 'ez-roadmapper'),
257
+
258
+ // Config
259
+ commit_docs: config.commit_docs,
260
+ research_enabled: config.research,
261
+
262
+ // Current milestone
263
+ current_milestone: milestone.version,
264
+ current_milestone_name: milestone.name,
265
+
266
+ // File existence
267
+ project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
268
+ roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
269
+ state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
270
+
271
+ // File paths
272
+ project_path: '.planning/PROJECT.md',
273
+ roadmap_path: '.planning/ROADMAP.md',
274
+ state_path: '.planning/STATE.md',
275
+ };
276
+
277
+ output(result, raw);
278
+ }
279
+
280
+ function cmdInitQuick(cwd, description, raw) {
281
+ const config = loadConfig(cwd);
282
+ const now = new Date();
283
+ const slug = description ? generateSlugInternal(description)?.substring(0, 40) : null;
284
+
285
+ // Generate collision-resistant quick task ID: YYMMDD-xxx
286
+ // xxx = 2-second precision blocks since midnight, encoded as 3-char Base36 (lowercase)
287
+ // Range: 000 (00:00:00) to xbz (23:59:58), guaranteed 3 chars for any time of day.
288
+ // Provides ~2s uniqueness window per user — practically collision-free across a team.
289
+ const yy = String(now.getFullYear()).slice(-2);
290
+ const mm = String(now.getMonth() + 1).padStart(2, '0');
291
+ const dd = String(now.getDate()).padStart(2, '0');
292
+ const dateStr = yy + mm + dd;
293
+ const secondsSinceMidnight = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds();
294
+ const timeBlocks = Math.floor(secondsSinceMidnight / 2);
295
+ const timeEncoded = timeBlocks.toString(36).padStart(3, '0');
296
+ const quickId = dateStr + '-' + timeEncoded;
297
+
298
+ const result = {
299
+ // Models
300
+ planner_model: resolveModelInternal(cwd, 'ez-planner'),
301
+ executor_model: resolveModelInternal(cwd, 'ez-executor'),
302
+ checker_model: resolveModelInternal(cwd, 'ez-plan-checker'),
303
+ verifier_model: resolveModelInternal(cwd, 'ez-verifier'),
304
+
305
+ // Config
306
+ commit_docs: config.commit_docs,
307
+
308
+ // Quick task info
309
+ quick_id: quickId,
310
+ slug: slug,
311
+ description: description || null,
312
+
313
+ // Timestamps
314
+ date: now.toISOString().split('T')[0],
315
+ timestamp: now.toISOString(),
316
+
317
+ // Paths
318
+ quick_dir: '.planning/quick',
319
+ task_dir: slug ? `.planning/quick/${quickId}-${slug}` : null,
320
+
321
+ // File existence
322
+ roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
323
+ planning_exists: pathExistsInternal(cwd, '.planning'),
324
+
325
+ };
326
+
327
+ output(result, raw);
328
+ }
329
+
330
+ function cmdInitResume(cwd, raw) {
331
+ const config = loadConfig(cwd);
332
+
333
+ // Check for interrupted agent
334
+ let interruptedAgentId = null;
335
+ try {
336
+ interruptedAgentId = fs.readFileSync(path.join(cwd, '.planning', 'current-agent-id.txt'), 'utf-8').trim();
337
+ } catch (err) {
338
+ logger.warn('Failed to read current-agent-id marker in cmdInitResume', { cwd, error: err.message });
339
+ }
340
+
341
+ const result = {
342
+ // File existence
343
+ state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
344
+ roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
345
+ project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
346
+ planning_exists: pathExistsInternal(cwd, '.planning'),
347
+
348
+ // File paths
349
+ state_path: '.planning/STATE.md',
350
+ roadmap_path: '.planning/ROADMAP.md',
351
+ project_path: '.planning/PROJECT.md',
352
+
353
+ // Agent state
354
+ has_interrupted_agent: !!interruptedAgentId,
355
+ interrupted_agent_id: interruptedAgentId,
356
+
357
+ // Config
358
+ commit_docs: config.commit_docs,
359
+ };
360
+
361
+ output(result, raw);
362
+ }
363
+
364
+ function cmdInitVerifyWork(cwd, phase, raw) {
365
+ if (!phase) {
366
+ error('phase required for init verify-work');
367
+ }
368
+
369
+ const config = loadConfig(cwd);
370
+ const phaseInfo = findPhaseInternal(cwd, phase);
371
+
372
+ const result = {
373
+ // Models
374
+ planner_model: resolveModelInternal(cwd, 'ez-planner'),
375
+ checker_model: resolveModelInternal(cwd, 'ez-plan-checker'),
376
+
377
+ // Config
378
+ commit_docs: config.commit_docs,
379
+
380
+ // Phase info
381
+ phase_found: !!phaseInfo,
382
+ phase_dir: phaseInfo?.directory || null,
383
+ phase_number: phaseInfo?.phase_number || null,
384
+ phase_name: phaseInfo?.phase_name || null,
385
+
386
+ // Existing artifacts
387
+ has_verification: phaseInfo?.has_verification || false,
388
+ };
389
+
390
+ output(result, raw);
391
+ }
392
+
393
+ function cmdInitPhaseOp(cwd, phase, raw) {
394
+ const config = loadConfig(cwd);
395
+ let phaseInfo = findPhaseInternal(cwd, phase);
396
+
397
+ // Fallback to ROADMAP.md if no directory exists (e.g., Plans: TBD)
398
+ if (!phaseInfo) {
399
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
400
+ if (roadmapPhase?.found) {
401
+ const phaseName = roadmapPhase.phase_name;
402
+ phaseInfo = {
403
+ found: true,
404
+ directory: null,
405
+ phase_number: roadmapPhase.phase_number,
406
+ phase_name: phaseName,
407
+ phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
408
+ plans: [],
409
+ summaries: [],
410
+ incomplete_plans: [],
411
+ has_research: false,
412
+ has_context: false,
413
+ has_verification: false,
414
+ };
415
+ }
416
+ }
417
+
418
+ const result = {
419
+ // Config
420
+ commit_docs: config.commit_docs,
421
+ brave_search: config.brave_search,
422
+
423
+ // Phase info
424
+ phase_found: !!phaseInfo,
425
+ phase_dir: phaseInfo?.directory || null,
426
+ phase_number: phaseInfo?.phase_number || null,
427
+ phase_name: phaseInfo?.phase_name || null,
428
+ phase_slug: phaseInfo?.phase_slug || null,
429
+ padded_phase: phaseInfo?.phase_number ? normalizePhaseName(phaseInfo.phase_number) : null,
430
+
431
+ // Existing artifacts
432
+ has_research: phaseInfo?.has_research || false,
433
+ has_context: phaseInfo?.has_context || false,
434
+ has_plans: (phaseInfo?.plans?.length || 0) > 0,
435
+ has_verification: phaseInfo?.has_verification || false,
436
+ plan_count: phaseInfo?.plans?.length || 0,
437
+
438
+ // File existence
439
+ roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
440
+ planning_exists: pathExistsInternal(cwd, '.planning'),
441
+
442
+ // File paths
443
+ state_path: '.planning/STATE.md',
444
+ roadmap_path: '.planning/ROADMAP.md',
445
+ requirements_path: '.planning/REQUIREMENTS.md',
446
+ };
447
+
448
+ if (phaseInfo?.directory) {
449
+ const phaseDirFull = path.join(cwd, phaseInfo.directory);
450
+ try {
451
+ const files = fs.readdirSync(phaseDirFull);
452
+ const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
453
+ if (contextFile) {
454
+ result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
455
+ }
456
+ const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
457
+ if (researchFile) {
458
+ result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
459
+ }
460
+ const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
461
+ if (verificationFile) {
462
+ result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
463
+ }
464
+ const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
465
+ if (uatFile) {
466
+ result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
467
+ }
468
+ } catch (err) {
469
+ logger.warn('Failed to inspect phase artifacts in cmdInitPhaseOp', { phaseDirFull, error: err.message });
470
+ }
471
+ }
472
+
473
+ output(result, raw);
474
+ }
475
+
476
+ function cmdInitTodos(cwd, area, raw) {
477
+ const config = loadConfig(cwd);
478
+ const now = new Date();
479
+
480
+ // List todos (reuse existing logic)
481
+ const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
482
+ let count = 0;
483
+ const todos = [];
484
+
485
+ try {
486
+ const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
487
+ for (const file of files) {
488
+ try {
489
+ const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
490
+ const createdMatch = content.match(/^created:\s*(.+)$/m);
491
+ const titleMatch = content.match(/^title:\s*(.+)$/m);
492
+ const areaMatch = content.match(/^area:\s*(.+)$/m);
493
+ const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
494
+
495
+ if (area && todoArea !== area) continue;
496
+
497
+ count++;
498
+ todos.push({
499
+ file,
500
+ created: createdMatch ? createdMatch[1].trim() : 'unknown',
501
+ title: titleMatch ? titleMatch[1].trim() : 'Untitled',
502
+ area: todoArea,
503
+ path: '.planning/todos/pending/' + file,
504
+ });
505
+ } catch (err) {
506
+ logger.warn('Failed to parse todo file in cmdInitTodos', { file, error: err.message });
507
+ }
508
+ }
509
+ } catch (err) {
510
+ logger.warn('Failed to list pending todos in cmdInitTodos', { pendingDir, error: err.message });
511
+ }
512
+
513
+ const result = {
514
+ // Config
515
+ commit_docs: config.commit_docs,
516
+
517
+ // Timestamps
518
+ date: now.toISOString().split('T')[0],
519
+ timestamp: now.toISOString(),
520
+
521
+ // Todo inventory
522
+ todo_count: count,
523
+ todos,
524
+ area_filter: area || null,
525
+
526
+ // Paths
527
+ pending_dir: '.planning/todos/pending',
528
+ completed_dir: '.planning/todos/completed',
529
+
530
+ // File existence
531
+ planning_exists: pathExistsInternal(cwd, '.planning'),
532
+ todos_dir_exists: pathExistsInternal(cwd, '.planning/todos'),
533
+ pending_dir_exists: pathExistsInternal(cwd, '.planning/todos/pending'),
534
+ };
535
+
536
+ output(result, raw);
537
+ }
538
+
539
+ function cmdInitMilestoneOp(cwd, raw) {
540
+ const config = loadConfig(cwd);
541
+ const milestone = getMilestoneInfo(cwd);
542
+
543
+ // Count phases
544
+ let phaseCount = 0;
545
+ let completedPhases = 0;
546
+ const phasesDir = path.join(cwd, '.planning', 'phases');
547
+ try {
548
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
549
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
550
+ phaseCount = dirs.length;
551
+
552
+ // Count phases with summaries (completed)
553
+ for (const dir of dirs) {
554
+ try {
555
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
556
+ const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
557
+ if (hasSummary) completedPhases++;
558
+ } catch (err) {
559
+ logger.warn('Failed to inspect phase directory in cmdInitMilestoneOp', { dir, error: err.message });
560
+ }
561
+ }
562
+ } catch (err) {
563
+ logger.warn('Failed to list phase directories in cmdInitMilestoneOp', { phasesDir, error: err.message });
564
+ }
565
+
566
+ // Check archive
567
+ const archiveDir = path.join(cwd, '.planning', 'archive');
568
+ let archivedMilestones = [];
569
+ try {
570
+ archivedMilestones = fs.readdirSync(archiveDir, { withFileTypes: true })
571
+ .filter(e => e.isDirectory())
572
+ .map(e => e.name);
573
+ } catch (err) {
574
+ logger.warn('Failed to list archived milestones in cmdInitMilestoneOp', { archiveDir, error: err.message });
575
+ }
576
+
577
+ const result = {
578
+ // Config
579
+ commit_docs: config.commit_docs,
580
+
581
+ // Current milestone
582
+ milestone_version: milestone.version,
583
+ milestone_name: milestone.name,
584
+ milestone_slug: generateSlugInternal(milestone.name),
585
+
586
+ // Phase counts
587
+ phase_count: phaseCount,
588
+ completed_phases: completedPhases,
589
+ all_phases_complete: phaseCount > 0 && phaseCount === completedPhases,
590
+
591
+ // Archive
592
+ archived_milestones: archivedMilestones,
593
+ archive_count: archivedMilestones.length,
594
+
595
+ // File existence
596
+ project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
597
+ roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
598
+ state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
599
+ archive_exists: pathExistsInternal(cwd, '.planning/archive'),
600
+ phases_dir_exists: pathExistsInternal(cwd, '.planning/phases'),
601
+ };
602
+
603
+ output(result, raw);
604
+ }
605
+
606
+ function cmdInitMapCodebase(cwd, raw) {
607
+ const config = loadConfig(cwd);
608
+
609
+ // Check for existing codebase maps
610
+ const codebaseDir = path.join(cwd, '.planning', 'codebase');
611
+ let existingMaps = [];
612
+ try {
613
+ existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
614
+ } catch (err) {
615
+ logger.warn('Failed to list codebase map files in cmdInitMapCodebase', { codebaseDir, error: err.message });
616
+ }
617
+
618
+ const result = {
619
+ // Models
620
+ mapper_model: resolveModelInternal(cwd, 'ez-codebase-mapper'),
621
+
622
+ // Config
623
+ commit_docs: config.commit_docs,
624
+ search_gitignored: config.search_gitignored,
625
+ parallelization: config.parallelization,
626
+
627
+ // Paths
628
+ codebase_dir: '.planning/codebase',
629
+
630
+ // Existing maps
631
+ existing_maps: existingMaps,
632
+ has_maps: existingMaps.length > 0,
633
+
634
+ // File existence
635
+ planning_exists: pathExistsInternal(cwd, '.planning'),
636
+ codebase_dir_exists: pathExistsInternal(cwd, '.planning/codebase'),
637
+ };
638
+
639
+ output(result, raw);
640
+ }
641
+
642
+ function cmdInitProgress(cwd, raw) {
643
+ const config = loadConfig(cwd);
644
+ const milestone = getMilestoneInfo(cwd);
645
+
646
+ // Analyze phases
647
+ const phasesDir = path.join(cwd, '.planning', 'phases');
648
+ const phases = [];
649
+ let currentPhase = null;
650
+ let nextPhase = null;
651
+
652
+ try {
653
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
654
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name).sort();
655
+
656
+ for (const dir of dirs) {
657
+ const match = dir.match(/^(\d+(?:\.\d+)*)-?(.*)/);
658
+ const phaseNumber = match ? match[1] : dir;
659
+ const phaseName = match && match[2] ? match[2] : null;
660
+
661
+ const phasePath = path.join(phasesDir, dir);
662
+ const phaseFiles = fs.readdirSync(phasePath);
663
+
664
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
665
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
666
+ const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
667
+
668
+ const status = summaries.length >= plans.length && plans.length > 0 ? 'complete' :
669
+ plans.length > 0 ? 'in_progress' :
670
+ hasResearch ? 'researched' : 'pending';
671
+
672
+ const phaseInfo = {
673
+ number: phaseNumber,
674
+ name: phaseName,
675
+ directory: '.planning/phases/' + dir,
676
+ status,
677
+ plan_count: plans.length,
678
+ summary_count: summaries.length,
679
+ has_research: hasResearch,
680
+ };
681
+
682
+ phases.push(phaseInfo);
683
+
684
+ // Find current (first incomplete with plans) and next (first pending)
685
+ if (!currentPhase && (status === 'in_progress' || status === 'researched')) {
686
+ currentPhase = phaseInfo;
687
+ }
688
+ if (!nextPhase && status === 'pending') {
689
+ nextPhase = phaseInfo;
690
+ }
691
+ }
692
+ } catch (err) {
693
+ logger.warn('Failed to analyze phase progress in cmdInitProgress', { phasesDir, error: err.message });
694
+ }
695
+
696
+ // Check for paused work
697
+ let pausedAt = null;
698
+ try {
699
+ const state = fs.readFileSync(path.join(cwd, '.planning', 'STATE.md'), 'utf-8');
700
+ const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/);
701
+ if (pauseMatch) pausedAt = pauseMatch[1].trim();
702
+ } catch (err) {
703
+ logger.warn('Failed to read paused state in cmdInitProgress', { cwd, error: err.message });
704
+ }
705
+
706
+ const result = {
707
+ // Models
708
+ executor_model: resolveModelInternal(cwd, 'ez-executor'),
709
+ planner_model: resolveModelInternal(cwd, 'ez-planner'),
710
+
711
+ // Config
712
+ commit_docs: config.commit_docs,
713
+
714
+ // Milestone
715
+ milestone_version: milestone.version,
716
+ milestone_name: milestone.name,
717
+
718
+ // Phase overview
719
+ phases,
720
+ phase_count: phases.length,
721
+ completed_count: phases.filter(p => p.status === 'complete').length,
722
+ in_progress_count: phases.filter(p => p.status === 'in_progress').length,
723
+
724
+ // Current state
725
+ current_phase: currentPhase,
726
+ next_phase: nextPhase,
727
+ paused_at: pausedAt,
728
+ has_work_in_progress: !!currentPhase,
729
+
730
+ // File existence
731
+ project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
732
+ roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
733
+ state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
734
+ // File paths
735
+ state_path: '.planning/STATE.md',
736
+ roadmap_path: '.planning/ROADMAP.md',
737
+ project_path: '.planning/PROJECT.md',
738
+ config_path: '.planning/config.json',
739
+ };
740
+
741
+ output(result, raw);
742
+ }
743
+
744
+ module.exports = {
745
+ cmdInitExecutePhase,
746
+ cmdInitPlanPhase,
747
+ cmdInitNewProject,
748
+ cmdInitNewMilestone,
749
+ cmdInitQuick,
750
+ cmdInitResume,
751
+ cmdInitVerifyWork,
752
+ cmdInitPhaseOp,
753
+ cmdInitTodos,
754
+ cmdInitMilestoneOp,
755
+ cmdInitMapCodebase,
756
+ cmdInitProgress,
757
+ };