qaa-agent 1.0.0

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 (56) hide show
  1. package/.claude/commands/create-test.md +40 -0
  2. package/.claude/commands/qa-analyze.md +60 -0
  3. package/.claude/commands/qa-audit.md +37 -0
  4. package/.claude/commands/qa-blueprint.md +54 -0
  5. package/.claude/commands/qa-fix.md +36 -0
  6. package/.claude/commands/qa-from-ticket.md +88 -0
  7. package/.claude/commands/qa-gap.md +54 -0
  8. package/.claude/commands/qa-pom.md +36 -0
  9. package/.claude/commands/qa-pyramid.md +37 -0
  10. package/.claude/commands/qa-report.md +38 -0
  11. package/.claude/commands/qa-start.md +33 -0
  12. package/.claude/commands/qa-testid.md +54 -0
  13. package/.claude/commands/qa-validate.md +54 -0
  14. package/.claude/commands/update-test.md +58 -0
  15. package/.claude/settings.json +19 -0
  16. package/.claude/skills/qa-bug-detective/SKILL.md +122 -0
  17. package/.claude/skills/qa-repo-analyzer/SKILL.md +88 -0
  18. package/.claude/skills/qa-self-validator/SKILL.md +109 -0
  19. package/.claude/skills/qa-template-engine/SKILL.md +113 -0
  20. package/.claude/skills/qa-testid-injector/SKILL.md +93 -0
  21. package/.claude/skills/qa-workflow-documenter/SKILL.md +87 -0
  22. package/CLAUDE.md +543 -0
  23. package/README.md +418 -0
  24. package/agents/qa-pipeline-orchestrator.md +1217 -0
  25. package/agents/qaa-analyzer.md +508 -0
  26. package/agents/qaa-bug-detective.md +444 -0
  27. package/agents/qaa-executor.md +618 -0
  28. package/agents/qaa-planner.md +374 -0
  29. package/agents/qaa-scanner.md +422 -0
  30. package/agents/qaa-testid-injector.md +583 -0
  31. package/agents/qaa-validator.md +450 -0
  32. package/bin/install.cjs +176 -0
  33. package/bin/lib/commands.cjs +709 -0
  34. package/bin/lib/config.cjs +307 -0
  35. package/bin/lib/core.cjs +497 -0
  36. package/bin/lib/frontmatter.cjs +299 -0
  37. package/bin/lib/init.cjs +989 -0
  38. package/bin/lib/milestone.cjs +241 -0
  39. package/bin/lib/model-profiles.cjs +60 -0
  40. package/bin/lib/phase.cjs +911 -0
  41. package/bin/lib/roadmap.cjs +306 -0
  42. package/bin/lib/state.cjs +748 -0
  43. package/bin/lib/template.cjs +222 -0
  44. package/bin/lib/verify.cjs +842 -0
  45. package/bin/qaa-tools.cjs +607 -0
  46. package/package.json +34 -0
  47. package/templates/failure-classification.md +391 -0
  48. package/templates/gap-analysis.md +409 -0
  49. package/templates/pr-template.md +48 -0
  50. package/templates/qa-analysis.md +381 -0
  51. package/templates/qa-audit-report.md +465 -0
  52. package/templates/qa-repo-blueprint.md +636 -0
  53. package/templates/scan-manifest.md +312 -0
  54. package/templates/test-inventory.md +582 -0
  55. package/templates/testid-audit-report.md +354 -0
  56. package/templates/validation-report.md +243 -0
@@ -0,0 +1,989 @@
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, getMilestonePhaseFilter, stripShippedMilestones, 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, 'qaa-executor'),
29
+ verifier_model: resolveModelInternal(cwd, 'qaa-validator'),
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, 'qaa-analyzer'),
101
+ planner_model: resolveModelInternal(cwd, 'qaa-planner'),
102
+ checker_model: resolveModelInternal(cwd, 'qaa-validator'),
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, '.qaa', '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, 'qaa-scanner'),
191
+ synthesizer_model: resolveModelInternal(cwd, 'qaa-analyzer'),
192
+ roadmapper_model: resolveModelInternal(cwd, 'qaa-planner'),
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, 'qaa-scanner'),
228
+ synthesizer_model: resolveModelInternal(cwd, 'qaa-analyzer'),
229
+ roadmapper_model: resolveModelInternal(cwd, 'qaa-planner'),
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, 'qaa-planner'),
274
+ executor_model: resolveModelInternal(cwd, 'qaa-executor'),
275
+ checker_model: resolveModelInternal(cwd, 'qaa-validator'),
276
+ verifier_model: resolveModelInternal(cwd, 'qaa-validator'),
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, 'qaa-planner'),
346
+ checker_model: resolveModelInternal(cwd, 'qaa-validator'),
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
+ // If the only disk match comes from an archived milestone, prefer the
369
+ // current milestone's ROADMAP entry so discuss-phase and similar flows
370
+ // don't attach to shipped work that reused the same phase number.
371
+ if (phaseInfo?.archived) {
372
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
373
+ if (roadmapPhase?.found) {
374
+ const phaseName = roadmapPhase.phase_name;
375
+ phaseInfo = {
376
+ found: true,
377
+ directory: null,
378
+ phase_number: roadmapPhase.phase_number,
379
+ phase_name: phaseName,
380
+ phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
381
+ plans: [],
382
+ summaries: [],
383
+ incomplete_plans: [],
384
+ has_research: false,
385
+ has_context: false,
386
+ has_verification: false,
387
+ };
388
+ }
389
+ }
390
+
391
+ // Fallback to ROADMAP.md if no directory exists (e.g., Plans: TBD)
392
+ if (!phaseInfo) {
393
+ const roadmapPhase = getRoadmapPhaseInternal(cwd, phase);
394
+ if (roadmapPhase?.found) {
395
+ const phaseName = roadmapPhase.phase_name;
396
+ phaseInfo = {
397
+ found: true,
398
+ directory: null,
399
+ phase_number: roadmapPhase.phase_number,
400
+ phase_name: phaseName,
401
+ phase_slug: phaseName ? phaseName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') : null,
402
+ plans: [],
403
+ summaries: [],
404
+ incomplete_plans: [],
405
+ has_research: false,
406
+ has_context: false,
407
+ has_verification: false,
408
+ };
409
+ }
410
+ }
411
+
412
+ const result = {
413
+ // Config
414
+ commit_docs: config.commit_docs,
415
+ brave_search: config.brave_search,
416
+
417
+ // Phase info
418
+ phase_found: !!phaseInfo,
419
+ phase_dir: phaseInfo?.directory || null,
420
+ phase_number: phaseInfo?.phase_number || null,
421
+ phase_name: phaseInfo?.phase_name || null,
422
+ phase_slug: phaseInfo?.phase_slug || null,
423
+ padded_phase: phaseInfo?.phase_number ? normalizePhaseName(phaseInfo.phase_number) : null,
424
+
425
+ // Existing artifacts
426
+ has_research: phaseInfo?.has_research || false,
427
+ has_context: phaseInfo?.has_context || false,
428
+ has_plans: (phaseInfo?.plans?.length || 0) > 0,
429
+ has_verification: phaseInfo?.has_verification || false,
430
+ plan_count: phaseInfo?.plans?.length || 0,
431
+
432
+ // File existence
433
+ roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
434
+ planning_exists: pathExistsInternal(cwd, '.planning'),
435
+
436
+ // File paths
437
+ state_path: '.planning/STATE.md',
438
+ roadmap_path: '.planning/ROADMAP.md',
439
+ requirements_path: '.planning/REQUIREMENTS.md',
440
+ };
441
+
442
+ if (phaseInfo?.directory) {
443
+ const phaseDirFull = path.join(cwd, phaseInfo.directory);
444
+ try {
445
+ const files = fs.readdirSync(phaseDirFull);
446
+ const contextFile = files.find(f => f.endsWith('-CONTEXT.md') || f === 'CONTEXT.md');
447
+ if (contextFile) {
448
+ result.context_path = toPosixPath(path.join(phaseInfo.directory, contextFile));
449
+ }
450
+ const researchFile = files.find(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
451
+ if (researchFile) {
452
+ result.research_path = toPosixPath(path.join(phaseInfo.directory, researchFile));
453
+ }
454
+ const verificationFile = files.find(f => f.endsWith('-VERIFICATION.md') || f === 'VERIFICATION.md');
455
+ if (verificationFile) {
456
+ result.verification_path = toPosixPath(path.join(phaseInfo.directory, verificationFile));
457
+ }
458
+ const uatFile = files.find(f => f.endsWith('-UAT.md') || f === 'UAT.md');
459
+ if (uatFile) {
460
+ result.uat_path = toPosixPath(path.join(phaseInfo.directory, uatFile));
461
+ }
462
+ } catch {}
463
+ }
464
+
465
+ output(result, raw);
466
+ }
467
+
468
+ function cmdInitTodos(cwd, area, raw) {
469
+ const config = loadConfig(cwd);
470
+ const now = new Date();
471
+
472
+ // List todos (reuse existing logic)
473
+ const pendingDir = path.join(cwd, '.planning', 'todos', 'pending');
474
+ let count = 0;
475
+ const todos = [];
476
+
477
+ try {
478
+ const files = fs.readdirSync(pendingDir).filter(f => f.endsWith('.md'));
479
+ for (const file of files) {
480
+ try {
481
+ const content = fs.readFileSync(path.join(pendingDir, file), 'utf-8');
482
+ const createdMatch = content.match(/^created:\s*(.+)$/m);
483
+ const titleMatch = content.match(/^title:\s*(.+)$/m);
484
+ const areaMatch = content.match(/^area:\s*(.+)$/m);
485
+ const todoArea = areaMatch ? areaMatch[1].trim() : 'general';
486
+
487
+ if (area && todoArea !== area) continue;
488
+
489
+ count++;
490
+ todos.push({
491
+ file,
492
+ created: createdMatch ? createdMatch[1].trim() : 'unknown',
493
+ title: titleMatch ? titleMatch[1].trim() : 'Untitled',
494
+ area: todoArea,
495
+ path: '.planning/todos/pending/' + file,
496
+ });
497
+ } catch {}
498
+ }
499
+ } catch {}
500
+
501
+ const result = {
502
+ // Config
503
+ commit_docs: config.commit_docs,
504
+
505
+ // Timestamps
506
+ date: now.toISOString().split('T')[0],
507
+ timestamp: now.toISOString(),
508
+
509
+ // Todo inventory
510
+ todo_count: count,
511
+ todos,
512
+ area_filter: area || null,
513
+
514
+ // Paths
515
+ pending_dir: '.planning/todos/pending',
516
+ completed_dir: '.planning/todos/completed',
517
+
518
+ // File existence
519
+ planning_exists: pathExistsInternal(cwd, '.planning'),
520
+ todos_dir_exists: pathExistsInternal(cwd, '.planning/todos'),
521
+ pending_dir_exists: pathExistsInternal(cwd, '.planning/todos/pending'),
522
+ };
523
+
524
+ output(result, raw);
525
+ }
526
+
527
+ function cmdInitMilestoneOp(cwd, raw) {
528
+ const config = loadConfig(cwd);
529
+ const milestone = getMilestoneInfo(cwd);
530
+
531
+ // Count phases
532
+ let phaseCount = 0;
533
+ let completedPhases = 0;
534
+ const phasesDir = path.join(cwd, '.planning', 'phases');
535
+ try {
536
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
537
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name);
538
+ phaseCount = dirs.length;
539
+
540
+ // Count phases with summaries (completed)
541
+ for (const dir of dirs) {
542
+ try {
543
+ const phaseFiles = fs.readdirSync(path.join(phasesDir, dir));
544
+ const hasSummary = phaseFiles.some(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
545
+ if (hasSummary) completedPhases++;
546
+ } catch {}
547
+ }
548
+ } catch {}
549
+
550
+ // Check archive
551
+ const archiveDir = path.join(cwd, '.planning', 'archive');
552
+ let archivedMilestones = [];
553
+ try {
554
+ archivedMilestones = fs.readdirSync(archiveDir, { withFileTypes: true })
555
+ .filter(e => e.isDirectory())
556
+ .map(e => e.name);
557
+ } catch {}
558
+
559
+ const result = {
560
+ // Config
561
+ commit_docs: config.commit_docs,
562
+
563
+ // Current milestone
564
+ milestone_version: milestone.version,
565
+ milestone_name: milestone.name,
566
+ milestone_slug: generateSlugInternal(milestone.name),
567
+
568
+ // Phase counts
569
+ phase_count: phaseCount,
570
+ completed_phases: completedPhases,
571
+ all_phases_complete: phaseCount > 0 && phaseCount === completedPhases,
572
+
573
+ // Archive
574
+ archived_milestones: archivedMilestones,
575
+ archive_count: archivedMilestones.length,
576
+
577
+ // File existence
578
+ project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
579
+ roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
580
+ state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
581
+ archive_exists: pathExistsInternal(cwd, '.planning/archive'),
582
+ phases_dir_exists: pathExistsInternal(cwd, '.planning/phases'),
583
+ };
584
+
585
+ output(result, raw);
586
+ }
587
+
588
+ function cmdInitMapCodebase(cwd, raw) {
589
+ const config = loadConfig(cwd);
590
+
591
+ // Check for existing codebase maps
592
+ const codebaseDir = path.join(cwd, '.planning', 'codebase');
593
+ let existingMaps = [];
594
+ try {
595
+ existingMaps = fs.readdirSync(codebaseDir).filter(f => f.endsWith('.md'));
596
+ } catch {}
597
+
598
+ const result = {
599
+ // Models
600
+ mapper_model: resolveModelInternal(cwd, 'qaa-scanner'),
601
+
602
+ // Config
603
+ commit_docs: config.commit_docs,
604
+ search_gitignored: config.search_gitignored,
605
+ parallelization: config.parallelization,
606
+
607
+ // Paths
608
+ codebase_dir: '.planning/codebase',
609
+
610
+ // Existing maps
611
+ existing_maps: existingMaps,
612
+ has_maps: existingMaps.length > 0,
613
+
614
+ // File existence
615
+ planning_exists: pathExistsInternal(cwd, '.planning'),
616
+ codebase_dir_exists: pathExistsInternal(cwd, '.planning/codebase'),
617
+ };
618
+
619
+ output(result, raw);
620
+ }
621
+
622
+ function cmdInitQaStart(cwd, raw) {
623
+ const config = loadConfig(cwd);
624
+
625
+ // Parse --dev-repo and --qa-repo from process.argv
626
+ const argv = process.argv;
627
+ let devRepoPath = cwd;
628
+ let qaRepoPath = null;
629
+
630
+ const devRepoIdx = argv.indexOf('--dev-repo');
631
+ if (devRepoIdx !== -1 && argv[devRepoIdx + 1]) {
632
+ devRepoPath = path.resolve(argv[devRepoIdx + 1]);
633
+ }
634
+ const qaRepoIdx = argv.indexOf('--qa-repo');
635
+ if (qaRepoIdx !== -1 && argv[qaRepoIdx + 1]) {
636
+ qaRepoPath = path.resolve(argv[qaRepoIdx + 1]);
637
+ }
638
+
639
+ // --- Recursive file finder (max 3 levels deep) ---
640
+ function findFilesRecursive(dir, pattern, maxDepth) {
641
+ if (maxDepth === undefined) maxDepth = 3;
642
+ const results = [];
643
+ try {
644
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
645
+ for (const entry of entries) {
646
+ const fullPath = path.join(dir, entry.name);
647
+ if (entry.isFile() && pattern.test(entry.name)) {
648
+ results.push(fullPath);
649
+ } else if (entry.isDirectory() && maxDepth > 0 && entry.name !== 'node_modules' && entry.name !== '.git') {
650
+ const sub = findFilesRecursive(fullPath, pattern, maxDepth - 1);
651
+ for (const s of sub) results.push(s);
652
+ }
653
+ }
654
+ } catch {}
655
+ return results;
656
+ }
657
+
658
+ // --- Maturity scoring (when qaRepoPath is provided) ---
659
+ let maturityScore = null;
660
+ let maturityNote = null;
661
+
662
+ if (qaRepoPath) {
663
+ let score = 0;
664
+
665
+ try {
666
+ // 1. POM usage (25 pts)
667
+ const hasPagesDir = fs.existsSync(path.join(qaRepoPath, 'pages')) ||
668
+ fs.existsSync(path.join(qaRepoPath, 'page-objects'));
669
+ if (hasPagesDir) {
670
+ const basePageFiles = findFilesRecursive(qaRepoPath, /^BasePage\./);
671
+ if (basePageFiles.length > 0) {
672
+ score += 25;
673
+ } else {
674
+ score += 12;
675
+ }
676
+ }
677
+
678
+ // 2. Assertion quality (25 pts)
679
+ const testFilePattern = /\.(spec|test)\.(ts|js)$/;
680
+ const allTestFiles = findFilesRecursive(qaRepoPath, testFilePattern);
681
+ const sampledTestFiles = allTestFiles.slice(0, 10);
682
+ let concrete = 0;
683
+ let vague = 0;
684
+ const concretePattern = /toBe\(|toEqual\(|toHaveText\(|toContain\(|should\('have\.|should\('eq'|should\('equal'/;
685
+ const vaguePattern = /toBeTruthy|toBeDefined|toBeFalsy|should\('exist'\)|should\('be\.visible'\)/;
686
+ for (const tf of sampledTestFiles) {
687
+ try {
688
+ const content = fs.readFileSync(tf, 'utf-8');
689
+ const lines = content.split('\n');
690
+ for (const line of lines) {
691
+ if (concretePattern.test(line)) concrete++;
692
+ if (vaguePattern.test(line)) vague++;
693
+ }
694
+ } catch {}
695
+ }
696
+ score += Math.round((concrete / Math.max(concrete + vague, 1)) * 25);
697
+
698
+ // 3. CI/CD integration (20 pts)
699
+ const ciPaths = [
700
+ path.join(qaRepoPath, '.github', 'workflows'),
701
+ path.join(qaRepoPath, 'Jenkinsfile'),
702
+ path.join(qaRepoPath, '.gitlab-ci.yml'),
703
+ path.join(qaRepoPath, 'azure-pipelines.yml'),
704
+ ];
705
+ let ciConfigFound = null;
706
+ for (const ciPath of ciPaths) {
707
+ if (fs.existsSync(ciPath)) {
708
+ ciConfigFound = ciPath;
709
+ break;
710
+ }
711
+ }
712
+ if (ciConfigFound) {
713
+ let hasTestCommand = false;
714
+ try {
715
+ let ciContent = '';
716
+ const stat = fs.statSync(ciConfigFound);
717
+ if (stat.isDirectory()) {
718
+ // .github/workflows/ -- read first file in directory
719
+ const files = fs.readdirSync(ciConfigFound);
720
+ if (files.length > 0) {
721
+ ciContent = fs.readFileSync(path.join(ciConfigFound, files[0]), 'utf-8').substring(0, 500);
722
+ }
723
+ } else {
724
+ ciContent = fs.readFileSync(ciConfigFound, 'utf-8').substring(0, 500);
725
+ }
726
+ if (/test|jest|playwright|cypress/i.test(ciContent)) {
727
+ hasTestCommand = true;
728
+ }
729
+ } catch {}
730
+ score += hasTestCommand ? 20 : 10;
731
+ }
732
+
733
+ // 4. Fixture management (15 pts)
734
+ const fixturesDir = path.join(qaRepoPath, 'fixtures');
735
+ if (fs.existsSync(fixturesDir)) {
736
+ try {
737
+ const fixtureFiles = fs.readdirSync(fixturesDir).filter(f => !f.startsWith('.'));
738
+ score += fixtureFiles.length >= 2 ? 15 : 7;
739
+ } catch {
740
+ score += 7;
741
+ }
742
+ }
743
+
744
+ // 5. Naming convention (15 pts)
745
+ const namingPattern = /\.(e2e\.spec|api\.spec|unit\.spec|e2e\.cy)\./;
746
+ const totalTestFiles = sampledTestFiles.length;
747
+ let matchCount = 0;
748
+ for (const tf of sampledTestFiles) {
749
+ if (namingPattern.test(path.basename(tf))) {
750
+ matchCount++;
751
+ }
752
+ }
753
+ score += Math.round((matchCount / Math.max(totalTestFiles, 1)) * 15);
754
+
755
+ maturityScore = Math.min(100, score);
756
+ } catch {
757
+ maturityScore = 0;
758
+ }
759
+
760
+ // Handle score=0 fallback
761
+ if (maturityScore === 0) {
762
+ maturityNote = 'QA repo at ' + qaRepoPath + ' is empty (score 0). Running Option 1 (full pipeline from scratch).';
763
+ }
764
+ }
765
+
766
+ // --- Determine workflow option ---
767
+ let option = 1;
768
+ if (qaRepoPath === null) {
769
+ option = 1;
770
+ } else if (maturityScore === 0) {
771
+ option = 1;
772
+ } else if (maturityScore < 70) {
773
+ option = 2;
774
+ } else {
775
+ option = 3;
776
+ }
777
+
778
+ // --- Build result object ---
779
+ const result = {
780
+ // Models for each agent
781
+ scanner_model: resolveModelInternal(cwd, 'qaa-scanner'),
782
+ analyzer_model: resolveModelInternal(cwd, 'qaa-analyzer'),
783
+ planner_model: resolveModelInternal(cwd, 'qaa-planner'),
784
+ executor_model: resolveModelInternal(cwd, 'qaa-executor'),
785
+ validator_model: resolveModelInternal(cwd, 'qaa-validator'),
786
+ detective_model: resolveModelInternal(cwd, 'qaa-validator'),
787
+ injector_model: resolveModelInternal(cwd, 'qaa-scanner'),
788
+
789
+ // Workflow routing
790
+ option,
791
+ maturity_score: maturityScore,
792
+ maturity_note: maturityNote,
793
+
794
+ // Repo paths
795
+ dev_repo_path: devRepoPath,
796
+ qa_repo_path: qaRepoPath,
797
+
798
+ // Config flags
799
+ auto_advance: config.workflow?.auto_advance || false,
800
+ auto_chain_active: config.workflow?._auto_chain_active || false,
801
+ commit_docs: config.commit_docs,
802
+ parallelization: config.parallelization,
803
+
804
+ // Pipeline state
805
+ pipeline: {
806
+ scan_status: 'pending',
807
+ analyze_status: 'pending',
808
+ generate_status: 'pending',
809
+ validate_status: 'pending',
810
+ deliver_status: 'pending',
811
+ },
812
+
813
+ // File existence
814
+ state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
815
+ config_exists: pathExistsInternal(cwd, '.planning/config.json'),
816
+
817
+ // Output paths
818
+ output_dir: '.qa-output',
819
+
820
+ // Timestamps
821
+ date: new Date().toISOString().split('T')[0],
822
+ timestamp: new Date().toISOString(),
823
+ };
824
+
825
+ output(result, raw);
826
+ }
827
+
828
+ function cmdInitProgress(cwd, raw) {
829
+ const config = loadConfig(cwd);
830
+ const milestone = getMilestoneInfo(cwd);
831
+
832
+ // Analyze phases — filter to current milestone and include ROADMAP-only phases
833
+ const phasesDir = path.join(cwd, '.planning', 'phases');
834
+ const phases = [];
835
+ let currentPhase = null;
836
+ let nextPhase = null;
837
+
838
+ // Build set of phases defined in ROADMAP for the current milestone
839
+ const roadmapPhaseNums = new Set();
840
+ const roadmapPhaseNames = new Map();
841
+ try {
842
+ const roadmapContent = stripShippedMilestones(
843
+ fs.readFileSync(path.join(cwd, '.planning', 'ROADMAP.md'), 'utf-8')
844
+ );
845
+ const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
846
+ let hm;
847
+ while ((hm = headingPattern.exec(roadmapContent)) !== null) {
848
+ roadmapPhaseNums.add(hm[1]);
849
+ roadmapPhaseNames.set(hm[1], hm[2].replace(/\(INSERTED\)/i, '').trim());
850
+ }
851
+ } catch {}
852
+
853
+ const isDirInMilestone = getMilestonePhaseFilter(cwd);
854
+ const seenPhaseNums = new Set();
855
+
856
+ try {
857
+ const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
858
+ const dirs = entries.filter(e => e.isDirectory()).map(e => e.name)
859
+ .filter(isDirInMilestone)
860
+ .sort((a, b) => {
861
+ const pa = a.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
862
+ const pb = b.match(/^(\d+[A-Z]?(?:\.\d+)*)/i);
863
+ if (!pa || !pb) return a.localeCompare(b);
864
+ return parseInt(pa[1], 10) - parseInt(pb[1], 10);
865
+ });
866
+
867
+ for (const dir of dirs) {
868
+ const match = dir.match(/^(\d+[A-Z]?(?:\.\d+)*)-?(.*)/i);
869
+ const phaseNumber = match ? match[1] : dir;
870
+ const phaseName = match && match[2] ? match[2] : null;
871
+ seenPhaseNums.add(phaseNumber.replace(/^0+/, '') || '0');
872
+
873
+ const phasePath = path.join(phasesDir, dir);
874
+ const phaseFiles = fs.readdirSync(phasePath);
875
+
876
+ const plans = phaseFiles.filter(f => f.endsWith('-PLAN.md') || f === 'PLAN.md');
877
+ const summaries = phaseFiles.filter(f => f.endsWith('-SUMMARY.md') || f === 'SUMMARY.md');
878
+ const hasResearch = phaseFiles.some(f => f.endsWith('-RESEARCH.md') || f === 'RESEARCH.md');
879
+
880
+ const status = summaries.length >= plans.length && plans.length > 0 ? 'complete' :
881
+ plans.length > 0 ? 'in_progress' :
882
+ hasResearch ? 'researched' : 'pending';
883
+
884
+ const phaseInfo = {
885
+ number: phaseNumber,
886
+ name: phaseName,
887
+ directory: '.planning/phases/' + dir,
888
+ status,
889
+ plan_count: plans.length,
890
+ summary_count: summaries.length,
891
+ has_research: hasResearch,
892
+ };
893
+
894
+ phases.push(phaseInfo);
895
+
896
+ // Find current (first incomplete with plans) and next (first pending)
897
+ if (!currentPhase && (status === 'in_progress' || status === 'researched')) {
898
+ currentPhase = phaseInfo;
899
+ }
900
+ if (!nextPhase && status === 'pending') {
901
+ nextPhase = phaseInfo;
902
+ }
903
+ }
904
+ } catch {}
905
+
906
+ // Add phases defined in ROADMAP but not yet scaffolded to disk
907
+ for (const [num, name] of roadmapPhaseNames) {
908
+ const stripped = num.replace(/^0+/, '') || '0';
909
+ if (!seenPhaseNums.has(stripped)) {
910
+ const phaseInfo = {
911
+ number: num,
912
+ name: name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, ''),
913
+ directory: null,
914
+ status: 'not_started',
915
+ plan_count: 0,
916
+ summary_count: 0,
917
+ has_research: false,
918
+ };
919
+ phases.push(phaseInfo);
920
+ if (!nextPhase && !currentPhase) {
921
+ nextPhase = phaseInfo;
922
+ }
923
+ }
924
+ }
925
+
926
+ // Re-sort phases by number after adding ROADMAP-only phases
927
+ phases.sort((a, b) => parseInt(a.number, 10) - parseInt(b.number, 10));
928
+
929
+ // Check for paused work
930
+ let pausedAt = null;
931
+ try {
932
+ const state = fs.readFileSync(path.join(cwd, '.planning', 'STATE.md'), 'utf-8');
933
+ const pauseMatch = state.match(/\*\*Paused At:\*\*\s*(.+)/);
934
+ if (pauseMatch) pausedAt = pauseMatch[1].trim();
935
+ } catch {}
936
+
937
+ const result = {
938
+ // Models
939
+ executor_model: resolveModelInternal(cwd, 'qaa-executor'),
940
+ planner_model: resolveModelInternal(cwd, 'qaa-planner'),
941
+
942
+ // Config
943
+ commit_docs: config.commit_docs,
944
+
945
+ // Milestone
946
+ milestone_version: milestone.version,
947
+ milestone_name: milestone.name,
948
+
949
+ // Phase overview
950
+ phases,
951
+ phase_count: phases.length,
952
+ completed_count: phases.filter(p => p.status === 'complete').length,
953
+ in_progress_count: phases.filter(p => p.status === 'in_progress').length,
954
+
955
+ // Current state
956
+ current_phase: currentPhase,
957
+ next_phase: nextPhase,
958
+ paused_at: pausedAt,
959
+ has_work_in_progress: !!currentPhase,
960
+
961
+ // File existence
962
+ project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
963
+ roadmap_exists: pathExistsInternal(cwd, '.planning/ROADMAP.md'),
964
+ state_exists: pathExistsInternal(cwd, '.planning/STATE.md'),
965
+ // File paths
966
+ state_path: '.planning/STATE.md',
967
+ roadmap_path: '.planning/ROADMAP.md',
968
+ project_path: '.planning/PROJECT.md',
969
+ config_path: '.planning/config.json',
970
+ };
971
+
972
+ output(result, raw);
973
+ }
974
+
975
+ module.exports = {
976
+ cmdInitExecutePhase,
977
+ cmdInitPlanPhase,
978
+ cmdInitNewProject,
979
+ cmdInitNewMilestone,
980
+ cmdInitQuick,
981
+ cmdInitResume,
982
+ cmdInitVerifyWork,
983
+ cmdInitPhaseOp,
984
+ cmdInitTodos,
985
+ cmdInitMilestoneOp,
986
+ cmdInitMapCodebase,
987
+ cmdInitProgress,
988
+ cmdInitQaStart,
989
+ };