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