@paths.design/caws-cli 4.0.0 → 4.1.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 (76) hide show
  1. package/dist/commands/archive.d.ts +50 -0
  2. package/dist/commands/archive.d.ts.map +1 -0
  3. package/dist/commands/archive.js +353 -0
  4. package/dist/commands/iterate.d.ts.map +1 -1
  5. package/dist/commands/iterate.js +12 -13
  6. package/dist/commands/mode.d.ts +24 -0
  7. package/dist/commands/mode.d.ts.map +1 -0
  8. package/dist/commands/mode.js +259 -0
  9. package/dist/commands/plan.d.ts +49 -0
  10. package/dist/commands/plan.d.ts.map +1 -0
  11. package/dist/commands/plan.js +448 -0
  12. package/dist/commands/quality-gates.d.ts +52 -0
  13. package/dist/commands/quality-gates.d.ts.map +1 -0
  14. package/dist/commands/quality-gates.js +490 -0
  15. package/dist/commands/specs.d.ts +71 -0
  16. package/dist/commands/specs.d.ts.map +1 -0
  17. package/dist/commands/specs.js +735 -0
  18. package/dist/commands/status.d.ts +4 -3
  19. package/dist/commands/status.d.ts.map +1 -1
  20. package/dist/commands/status.js +552 -22
  21. package/dist/commands/tutorial.d.ts +55 -0
  22. package/dist/commands/tutorial.d.ts.map +1 -0
  23. package/dist/commands/tutorial.js +481 -0
  24. package/dist/commands/validate.d.ts +10 -3
  25. package/dist/commands/validate.d.ts.map +1 -1
  26. package/dist/commands/validate.js +137 -54
  27. package/dist/config/modes.d.ts +225 -0
  28. package/dist/config/modes.d.ts.map +1 -0
  29. package/dist/config/modes.js +321 -0
  30. package/dist/constants/spec-types.d.ts +41 -0
  31. package/dist/constants/spec-types.d.ts.map +1 -0
  32. package/dist/constants/spec-types.js +42 -0
  33. package/dist/index-new.d.ts +5 -0
  34. package/dist/index-new.d.ts.map +1 -0
  35. package/dist/index-new.js +317 -0
  36. package/dist/index.js +225 -10
  37. package/dist/index.js.backup +4711 -0
  38. package/dist/scaffold/git-hooks.d.ts.map +1 -1
  39. package/dist/scaffold/git-hooks.js +32 -44
  40. package/dist/scaffold/index.d.ts.map +1 -1
  41. package/dist/scaffold/index.js +19 -0
  42. package/dist/utils/quality-gates-errors.js +520 -0
  43. package/dist/utils/quality-gates.d.ts +49 -0
  44. package/dist/utils/quality-gates.d.ts.map +1 -0
  45. package/dist/utils/quality-gates.js +361 -0
  46. package/dist/utils/spec-resolver.d.ts +88 -0
  47. package/dist/utils/spec-resolver.d.ts.map +1 -0
  48. package/dist/utils/spec-resolver.js +602 -0
  49. package/package.json +6 -5
  50. package/templates/.cursor/hooks/caws-scope-guard.sh +64 -8
  51. package/templates/.cursor/hooks/validate-spec.sh +22 -12
  52. package/templates/.cursor/rules/{01-claims-verification.mdc → 00-claims-verification.mdc} +1 -1
  53. package/templates/.cursor/rules/01-working-style.mdc +50 -0
  54. package/templates/.cursor/rules/{02-testing-standards.mdc → 02-quality-gates.mdc} +84 -29
  55. package/templates/.cursor/rules/03-naming-and-refactor.mdc +33 -0
  56. package/templates/.cursor/rules/04-logging-language-style.mdc +23 -0
  57. package/templates/.cursor/rules/05-safe-defaults-guards.mdc +23 -0
  58. package/templates/.cursor/rules/06-typescript-conventions.mdc +36 -0
  59. package/templates/.cursor/rules/07-process-ops.mdc +20 -0
  60. package/templates/.cursor/rules/08-solid-and-architecture.mdc +16 -0
  61. package/templates/.cursor/rules/09-docstrings.mdc +89 -0
  62. package/templates/.cursor/rules/10-authorship-and-attribution.mdc +15 -0
  63. package/templates/.cursor/rules/11-documentation-quality-standards.mdc +390 -0
  64. package/templates/.cursor/rules/12-scope-management-waivers.mdc +385 -0
  65. package/templates/.cursor/rules/13-implementation-completeness.mdc +516 -0
  66. package/templates/.cursor/rules/14-language-agnostic-standards.mdc +588 -0
  67. package/templates/.cursor/rules/15-sophisticated-todo-detection.mdc +425 -0
  68. package/templates/.cursor/rules/README.md +93 -7
  69. package/templates/apps/tools/caws/prompt-lint.js.backup +274 -0
  70. package/templates/apps/tools/caws/provenance.js.backup +73 -0
  71. package/templates/scripts/quality-gates/check-god-objects.js +146 -0
  72. package/templates/scripts/quality-gates/run-quality-gates.js +50 -0
  73. package/templates/scripts/v3/analysis/todo_analyzer.py +1950 -0
  74. package/templates/.cursor/rules/03-infrastructure-standards.mdc +0 -251
  75. package/templates/.cursor/rules/04-documentation-integrity.mdc +0 -291
  76. package/templates/.cursor/rules/05-production-readiness-checklist.mdc +0 -214
@@ -0,0 +1,602 @@
1
+ /**
2
+ * @fileoverview Spec Resolution System
3
+ * Resolves spec files with priority: feature-specific > working-spec.yaml
4
+ * Enables multi-agent workflows where each agent works on their own spec
5
+ * @author @darianrosebrook
6
+ */
7
+
8
+ const fs = require('fs-extra');
9
+ const path = require('path');
10
+ const yaml = require('js-yaml');
11
+ const chalk = require('chalk');
12
+
13
+ // Import SPEC_TYPES from constants for consistent display
14
+ const { SPEC_TYPES } = require('../constants/spec-types');
15
+
16
+ /**
17
+ * Spec resolution priority:
18
+ * 1. .caws/specs/<spec-id>.yaml (feature-specific, multi-agent safe)
19
+ * 2. .caws/working-spec.yaml (legacy, single-agent only)
20
+ */
21
+ const SPECS_DIR = '.caws/specs';
22
+ const LEGACY_SPEC = '.caws/working-spec.yaml';
23
+ const SPECS_REGISTRY = '.caws/specs/registry.json';
24
+
25
+ /**
26
+ * Resolve spec file path based on priority
27
+ * @param {Object} options - Resolution options
28
+ * @param {string} [options.specId] - Feature-specific spec ID (e.g., 'user-auth', 'FEAT-001')
29
+ * @param {string} [options.specFile] - Explicit file path override
30
+ * @param {boolean} [options.warnLegacy=true] - Warn when falling back to legacy spec
31
+ * @param {boolean} [options.interactive=false] - Use interactive spec selection for multiple specs
32
+ * @returns {Promise<{path: string, type: 'feature' | 'legacy', spec: Object}>}
33
+ */
34
+ async function resolveSpec(options = {}) {
35
+ const { specId, specFile, warnLegacy = true, interactive = false } = options;
36
+
37
+ // 1. Explicit file path takes highest priority
38
+ if (specFile) {
39
+ const explicitPath = path.isAbsolute(specFile) ? specFile : path.join(process.cwd(), specFile);
40
+
41
+ if (await fs.pathExists(explicitPath)) {
42
+ const yaml = require('js-yaml');
43
+ const content = await fs.readFile(explicitPath, 'utf8');
44
+ const spec = yaml.load(content);
45
+
46
+ return {
47
+ path: explicitPath,
48
+ type: explicitPath.includes('/specs/') ? 'feature' : 'legacy',
49
+ spec,
50
+ };
51
+ }
52
+
53
+ throw new Error(`Spec file not found: ${explicitPath}`);
54
+ }
55
+
56
+ // 2. Feature-specific spec (preferred for multi-agent)
57
+ if (specId) {
58
+ const featurePath = path.join(process.cwd(), SPECS_DIR, `${specId}.yaml`);
59
+
60
+ if (await fs.pathExists(featurePath)) {
61
+ const yaml = require('js-yaml');
62
+ const content = await fs.readFile(featurePath, 'utf8');
63
+ const spec = yaml.load(content);
64
+
65
+ console.log(chalk.green(`✅ Using feature-specific spec: ${specId}`));
66
+
67
+ return {
68
+ path: featurePath,
69
+ type: 'feature',
70
+ spec,
71
+ };
72
+ }
73
+
74
+ throw new Error(
75
+ `Feature spec '${specId}' not found. Create it with: caws specs create ${specId}`
76
+ );
77
+ }
78
+
79
+ // 3. Auto-detect from registry or list specs
80
+ const registry = await loadSpecsRegistry();
81
+ const specIds = Object.keys(registry.specs ?? {});
82
+
83
+ if (specIds.length === 1) {
84
+ // Single spec - use it automatically
85
+ const singleSpecId = specIds[0];
86
+ const singleSpecPath = path.join(process.cwd(), SPECS_DIR, registry.specs[singleSpecId].path);
87
+
88
+ if (await fs.pathExists(singleSpecPath)) {
89
+ const yaml = require('js-yaml');
90
+ const content = await fs.readFile(singleSpecPath, 'utf8');
91
+ const spec = yaml.load(content);
92
+
93
+ console.log(chalk.blue(`📋 Auto-detected single spec: ${singleSpecId}`));
94
+
95
+ return {
96
+ path: singleSpecPath,
97
+ type: 'feature',
98
+ spec,
99
+ };
100
+ }
101
+ } else if (specIds.length > 1) {
102
+ // Multiple specs - require explicit selection with enhanced guidance
103
+ console.error(chalk.red('❌ Multiple specs detected. Please specify which one:'));
104
+
105
+ // Show specs with details
106
+ const specsInfo = [];
107
+ for (const id of specIds) {
108
+ const specPath = path.join(SPECS_DIR, registry.specs[id].path);
109
+ try {
110
+ const content = await fs.readFile(specPath, 'utf8');
111
+ const spec = yaml.load(content);
112
+ const status = spec.status || 'draft';
113
+ const type = spec.type || 'feature';
114
+ const statusColor =
115
+ status === 'active' ? chalk.green : status === 'completed' ? chalk.blue : chalk.yellow;
116
+ const typeColor = SPEC_TYPES[type] ? SPEC_TYPES[type].color : chalk.white;
117
+
118
+ console.log(
119
+ chalk.yellow(
120
+ ` - ${id} ${typeColor(`(${type})`)} ${statusColor(`[${status}]`)} - ${spec.title || 'Untitled'}`
121
+ )
122
+ );
123
+ specsInfo.push({ id, type, status, title: spec.title || 'Untitled' });
124
+ } catch (error) {
125
+ console.log(chalk.yellow(` - ${id} (error loading details)`));
126
+ specsInfo.push({ id, type: 'unknown', status: 'unknown', title: 'Error loading' });
127
+ }
128
+ }
129
+
130
+ // Interactive mode
131
+ if (interactive) {
132
+ try {
133
+ const selectedSpecId = await interactiveSpecSelection(specIds);
134
+
135
+ // Recursively resolve with the selected spec ID
136
+ return await resolveSpec({
137
+ specId: selectedSpecId,
138
+ warnLegacy,
139
+ interactive: false, // Prevent infinite recursion
140
+ });
141
+ } catch (error) {
142
+ throw new Error(`Interactive selection failed: ${error.message}`);
143
+ }
144
+ }
145
+
146
+ console.log(chalk.blue('\n Usage: caws <command> --spec-id <spec-id>'));
147
+ console.log(chalk.gray(` Example: caws validate --spec-id ${specIds[0]}`));
148
+
149
+ // Suggest most likely spec (active first, then by type priority)
150
+ const priorityOrder = { active: 0, draft: 1, completed: 2 };
151
+ const sortedSpecs = specIds.sort((a, b) => {
152
+ const aSpec = specsInfo.find((s) => s.id === a);
153
+ const bSpec = specsInfo.find((s) => s.id === b);
154
+ const aPriority = priorityOrder[aSpec?.status] || 999;
155
+ const bPriority = priorityOrder[bSpec?.status] || 999;
156
+ if (aPriority !== bPriority) return aPriority - bPriority;
157
+
158
+ // Then by type (feature > fix > refactor > etc.)
159
+ const typePriority = { feature: 0, fix: 1, refactor: 2, chore: 3, docs: 4 };
160
+ const aTypePriority = typePriority[aSpec?.type] || 999;
161
+ const bTypePriority = typePriority[bSpec?.type] || 999;
162
+ return aTypePriority - bTypePriority;
163
+ });
164
+
165
+ console.log(chalk.green('\n💡 Quick suggestion:'));
166
+ console.log(chalk.gray(` Try: caws <command> --spec-id ${sortedSpecs[0]}`));
167
+
168
+ // Interactive mode suggestion
169
+ console.log(chalk.blue('\n Interactive mode: caws <command> --interactive-spec-selection'));
170
+
171
+ throw new Error('Spec ID required when multiple specs exist');
172
+ }
173
+
174
+ // 4. Fall back to legacy working-spec.yaml (with warning)
175
+ const legacyPath = path.join(process.cwd(), LEGACY_SPEC);
176
+
177
+ if (await fs.pathExists(legacyPath)) {
178
+ const yaml = require('js-yaml');
179
+ const content = await fs.readFile(legacyPath, 'utf8');
180
+ const spec = yaml.load(content);
181
+
182
+ if (warnLegacy) {
183
+ console.log(chalk.yellow('⚠️ Using legacy working-spec.yaml'));
184
+ console.log(chalk.gray(' For multi-agent workflows, use feature-specific specs:'));
185
+ console.log(chalk.blue(' caws specs create <feature-id>'));
186
+ console.log('');
187
+ }
188
+
189
+ return {
190
+ path: legacyPath,
191
+ type: 'legacy',
192
+ spec,
193
+ };
194
+ }
195
+
196
+ // 5. No specs found
197
+ throw new Error(
198
+ 'No CAWS spec found. Initialize with: caws init or create a feature spec: caws specs create <id>'
199
+ );
200
+ }
201
+
202
+ /**
203
+ * Load specs registry
204
+ * @returns {Promise<Object>} Registry data
205
+ */
206
+ async function loadSpecsRegistry() {
207
+ const registryPath = path.join(process.cwd(), SPECS_REGISTRY);
208
+
209
+ if (!(await fs.pathExists(registryPath))) {
210
+ return {
211
+ version: '1.0.0',
212
+ specs: {},
213
+ lastUpdated: new Date().toISOString(),
214
+ };
215
+ }
216
+
217
+ try {
218
+ const registry = await fs.readJson(registryPath);
219
+ return registry;
220
+ } catch (error) {
221
+ return {
222
+ version: '1.0.0',
223
+ specs: {},
224
+ lastUpdated: new Date().toISOString(),
225
+ };
226
+ }
227
+ }
228
+
229
+ /**
230
+ * List all available specs
231
+ * @returns {Promise<Array<{id: string, path: string, type: string}>>}
232
+ */
233
+ async function listAvailableSpecs() {
234
+ const specs = [];
235
+
236
+ // Check feature-specific specs
237
+ const specsDir = path.join(process.cwd(), SPECS_DIR);
238
+ if (await fs.pathExists(specsDir)) {
239
+ const files = await fs.readdir(specsDir);
240
+ const yamlFiles = files.filter((f) => f.endsWith('.yaml') || f.endsWith('.yml'));
241
+
242
+ for (const file of yamlFiles) {
243
+ if (file === 'registry.json') continue;
244
+
245
+ const specPath = path.join(specsDir, file);
246
+ try {
247
+ const yaml = require('js-yaml');
248
+ const content = await fs.readFile(specPath, 'utf8');
249
+ const spec = yaml.load(content);
250
+
251
+ specs.push({
252
+ id: spec.id || path.basename(file, path.extname(file)),
253
+ path: path.relative(process.cwd(), specPath),
254
+ type: 'feature',
255
+ title: spec.title || 'Untitled',
256
+ });
257
+ } catch (error) {
258
+ // Skip invalid specs
259
+ }
260
+ }
261
+ }
262
+
263
+ // Check legacy working-spec.yaml
264
+ const legacyPath = path.join(process.cwd(), LEGACY_SPEC);
265
+ if (await fs.pathExists(legacyPath)) {
266
+ try {
267
+ const yaml = require('js-yaml');
268
+ const content = await fs.readFile(legacyPath, 'utf8');
269
+ const spec = yaml.load(content);
270
+
271
+ specs.push({
272
+ id: spec.id || 'working-spec',
273
+ path: LEGACY_SPEC,
274
+ type: 'legacy',
275
+ title: spec.title || 'Legacy Working Spec',
276
+ });
277
+ } catch (error) {
278
+ // Skip invalid spec
279
+ }
280
+ }
281
+
282
+ return specs;
283
+ }
284
+
285
+ /**
286
+ * Interactive spec selection using readline
287
+ * @param {string[]} specIds - Available spec IDs
288
+ * @returns {Promise<string>} Selected spec ID
289
+ */
290
+ async function interactiveSpecSelection(specIds) {
291
+ return new Promise((resolve, reject) => {
292
+ const readline = require('readline');
293
+
294
+ console.log(chalk.blue('\n📋 Interactive Spec Selection'));
295
+ console.log(chalk.gray('Select which spec to use:\n'));
296
+
297
+ specIds.forEach((id, index) => {
298
+ console.log(chalk.yellow(`${index + 1}. ${id}`));
299
+ });
300
+
301
+ console.log(chalk.gray('\nEnter number (1-' + specIds.length + ') or spec ID directly: '));
302
+
303
+ const rl = readline.createInterface({
304
+ input: process.stdin,
305
+ output: process.stdout,
306
+ });
307
+
308
+ rl.question('> ', (answer) => {
309
+ rl.close();
310
+
311
+ const trimmed = answer.trim();
312
+
313
+ // Check if it's a number
314
+ const num = parseInt(trimmed);
315
+ if (num >= 1 && num <= specIds.length) {
316
+ resolve(specIds[num - 1]);
317
+ return;
318
+ }
319
+
320
+ // Check if it's a direct spec ID
321
+ if (specIds.includes(trimmed)) {
322
+ resolve(trimmed);
323
+ return;
324
+ }
325
+
326
+ reject(new Error(`Invalid selection: ${trimmed}. Please choose a valid spec ID.`));
327
+ });
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Check if project is using multi-spec architecture
333
+ * @returns {Promise<{isMultiSpec: boolean, specCount: number, needsMigration: boolean}>}
334
+ */
335
+ async function checkMultiSpecStatus() {
336
+ const registry = await loadSpecsRegistry();
337
+ const hasFeatureSpecs = Object.keys(registry.specs ?? {}).length > 0;
338
+ const legacyPath = path.join(process.cwd(), LEGACY_SPEC);
339
+ const hasLegacySpec = await fs.pathExists(legacyPath);
340
+
341
+ return {
342
+ isMultiSpec: hasFeatureSpecs,
343
+ specCount: Object.keys(registry.specs ?? {}).length,
344
+ needsMigration: hasLegacySpec && !hasFeatureSpecs,
345
+ };
346
+ }
347
+
348
+ /**
349
+ * Check for scope conflicts between specs
350
+ * @param {string[]} specIds - Array of spec IDs to check
351
+ * @returns {Promise<Array<{spec1: string, spec2: string, conflicts: string[]}>>} Array of conflicts
352
+ */
353
+ async function checkScopeConflicts(specIds) {
354
+ const conflicts = [];
355
+ const specScopes = [];
356
+
357
+ // Load registry once
358
+ const registry = await loadSpecsRegistry();
359
+
360
+ // Load all specs and their scopes
361
+ for (const id of specIds) {
362
+ const specPath = path.join(SPECS_DIR, registry.specs[id].path);
363
+
364
+ try {
365
+ const content = await fs.readFile(specPath, 'utf8');
366
+ const spec = yaml.load(content);
367
+
368
+ specScopes.push({
369
+ id,
370
+ scope: spec.scope || { in: [], out: [] },
371
+ title: spec.title || id,
372
+ });
373
+ } catch (error) {
374
+ // Skip specs that can't be loaded
375
+ continue;
376
+ }
377
+ }
378
+
379
+ // Check for conflicts between each pair of specs
380
+ for (let i = 0; i < specScopes.length; i++) {
381
+ for (let j = i + 1; j < specScopes.length; j++) {
382
+ const spec1 = specScopes[i];
383
+ const spec2 = specScopes[j];
384
+
385
+ const spec1Paths = new Set(spec1.scope.in || []);
386
+ const spec2Paths = new Set(spec2.scope.in || []);
387
+
388
+ // Find overlapping paths
389
+ const overlappingPaths = [];
390
+ for (const path1 of spec1Paths) {
391
+ for (const path2 of spec2Paths) {
392
+ if (pathsOverlap(path1, path2)) {
393
+ overlappingPaths.push(`${path1} ↔ ${path2}`);
394
+ }
395
+ }
396
+ }
397
+
398
+ if (overlappingPaths.length > 0) {
399
+ conflicts.push({
400
+ spec1: spec1.id,
401
+ spec2: spec2.id,
402
+ conflicts: overlappingPaths,
403
+ severity: 'warning', // Could be 'error' for stricter enforcement
404
+ });
405
+ }
406
+ }
407
+ }
408
+
409
+ return conflicts;
410
+ }
411
+
412
+ /**
413
+ * Check if two paths overlap (simplified implementation)
414
+ * @param {string} path1 - First path
415
+ * @param {string} path2 - Second path
416
+ * @returns {boolean} True if paths overlap
417
+ */
418
+ function pathsOverlap(path1, path2) {
419
+ // Normalize paths (remove leading/trailing slashes)
420
+ const normalizePath = (p) => p.replace(/^\/+|\/+$/g, '');
421
+
422
+ const normalized1 = normalizePath(path1);
423
+ const normalized2 = normalizePath(path2);
424
+
425
+ // Check for exact match
426
+ if (normalized1 === normalized2) {
427
+ return true;
428
+ }
429
+
430
+ // Handle wildcard patterns
431
+ const hasWildcard = (p) => p.includes('*');
432
+
433
+ if (hasWildcard(normalized1) || hasWildcard(normalized2)) {
434
+ // Convert wildcards to regex patterns
435
+ const toRegex = (p) => {
436
+ // Escape dots first
437
+ let result = p.replace(/\./g, '\\.');
438
+
439
+ // Handle ** patterns (match any path including zero segments)
440
+ result = result.replace(/\*\*/g, '(?:.*/)?');
441
+
442
+ // Handle single * patterns (match any non-slash characters)
443
+ result = result.replace(/\*/g, '[^/]*');
444
+
445
+ // Fix patterns like src/auth/**/*.js to match src/auth/login.js
446
+ // The pattern (?:.*/)?[^/]* should become .*[^/]* for direct filename matching
447
+ result = result.replace(/(\?:.*\/)?[^/]*/g, '.*[^/]*');
448
+
449
+ // Also fix patterns like (?:.[^/]*/)?/[^/]* to match direct filenames
450
+ result = result.replace(/(?:\..*\/)?[^/]*/g, '.*[^/]*');
451
+
452
+ return result;
453
+ };
454
+
455
+ // Check if either path matches the other's pattern
456
+ if (hasWildcard(normalized1)) {
457
+ const regex1 = new RegExp('^' + toRegex(normalized1) + '$');
458
+ if (regex1.test(normalized2)) return true;
459
+ }
460
+
461
+ if (hasWildcard(normalized2)) {
462
+ const regex2 = new RegExp('^' + toRegex(normalized2) + '$');
463
+ if (regex2.test(normalized1)) return true;
464
+ }
465
+
466
+ return false;
467
+ }
468
+
469
+ // Simple substring check for non-wildcard paths
470
+ return normalized1.includes(normalized2) || normalized2.includes(normalized1);
471
+ }
472
+
473
+ /**
474
+ * Suggest migration from legacy to multi-spec
475
+ * @returns {Promise<void>}
476
+ */
477
+ async function suggestMigration() {
478
+ const status = await checkMultiSpecStatus();
479
+
480
+ if (status.needsMigration) {
481
+ console.log(chalk.yellow('\n⚠️ Migration Recommended: Single-Spec → Multi-Spec'));
482
+ console.log(chalk.gray(' Your project uses the legacy working-spec.yaml'));
483
+ console.log(chalk.gray(' For multi-agent workflows, migrate to feature-specific specs:\n'));
484
+ console.log(chalk.blue(' 1. caws specs create <feature-id>'));
485
+ console.log(chalk.blue(' 2. Copy relevant content from working-spec.yaml'));
486
+ console.log(chalk.blue(' 3. Update agents to use --spec-id <feature-id>'));
487
+ console.log(chalk.gray('\n See: docs/guides/multi-agent-migration.md\n'));
488
+ }
489
+ }
490
+
491
+ // Feature breakdown logic (moved from specs.js to avoid circular dependency)
492
+ function suggestFeatureBreakdown(legacySpec) {
493
+ const features = [];
494
+
495
+ if (legacySpec.acceptance && legacySpec.acceptance.length > 0) {
496
+ // Group acceptance criteria by logical features
497
+ const criteriaByFeature = {};
498
+
499
+ legacySpec.acceptance.forEach((criterion, index) => {
500
+ // Simple heuristic: extract feature from criterion description (check all fields)
501
+ const fullDescription = [
502
+ criterion.given || '',
503
+ criterion.when || '',
504
+ criterion.then || '',
505
+ criterion.description || '',
506
+ criterion.title || `A${index + 1}`,
507
+ ].join(' ');
508
+ const words = fullDescription.toLowerCase().split(' ');
509
+
510
+ // Look for common feature keywords
511
+ const featureKeywords = {
512
+ auth: 'Authentication',
513
+ login: 'Authentication',
514
+ payment: 'Payment System',
515
+ billing: 'Billing',
516
+ dashboard: 'Dashboard',
517
+ admin: 'Admin Panel',
518
+ api: 'API',
519
+ database: 'Data Layer',
520
+ ui: 'User Interface',
521
+ email: 'Email System',
522
+ notification: 'Notifications',
523
+ report: 'Reporting',
524
+ search: 'Search',
525
+ filter: 'Filtering',
526
+ user: 'User Management',
527
+ };
528
+
529
+ let featureKey = 'general';
530
+ let featureTitle = 'General Features';
531
+
532
+ for (const [keyword, title] of Object.entries(featureKeywords)) {
533
+ if (words.some((word) => word.includes(keyword))) {
534
+ featureKey = keyword;
535
+ featureTitle = title;
536
+ break;
537
+ }
538
+ }
539
+
540
+ if (!criteriaByFeature[featureKey]) {
541
+ criteriaByFeature[featureKey] = {
542
+ id: featureKey,
543
+ title: featureTitle,
544
+ criteria: [],
545
+ scope: { in: [], out: [] },
546
+ };
547
+ }
548
+
549
+ criteriaByFeature[featureKey].criteria.push(criterion);
550
+ });
551
+
552
+ // Convert to feature objects
553
+ Object.values(criteriaByFeature).forEach((feature) => {
554
+ // Suggest scope based on feature type
555
+ const scopeSuggestions = {
556
+ user: { in: ['src/users/', 'tests/users/'], out: ['src/payments/', 'src/admin/'] },
557
+ auth: { in: ['src/auth/', 'tests/auth/'], out: ['src/payments/', 'src/admin/'] },
558
+ payment: { in: ['src/payments/', 'tests/payments/'], out: ['src/users/', 'src/admin/'] },
559
+ dashboard: {
560
+ in: ['src/dashboard/', 'tests/dashboard/'],
561
+ out: ['src/payments/', 'src/users/'],
562
+ },
563
+ admin: { in: ['src/admin/', 'tests/admin/'], out: ['src/payments/', 'src/users/'] },
564
+ api: { in: ['src/api/', 'tests/api/'], out: ['src/dashboard/', 'src/admin/'] },
565
+ general: { in: ['src/', 'tests/'], out: [] },
566
+ };
567
+
568
+ const suggestion = scopeSuggestions[feature.id] || scopeSuggestions.general;
569
+ feature.scope = suggestion;
570
+
571
+ features.push(feature);
572
+ });
573
+ } else {
574
+ // Fallback: create a single feature
575
+ features.push({
576
+ id: 'main-feature',
577
+ title: legacySpec.title || 'Main Feature',
578
+ criteria: legacySpec.acceptance || [],
579
+ scope: {
580
+ in: ['src/', 'tests/'],
581
+ out: [],
582
+ },
583
+ });
584
+ }
585
+
586
+ return features;
587
+ }
588
+
589
+ module.exports = {
590
+ resolveSpec,
591
+ listAvailableSpecs,
592
+ checkMultiSpecStatus,
593
+ checkScopeConflicts,
594
+ suggestMigration,
595
+ interactiveSpecSelection,
596
+ loadSpecsRegistry,
597
+ suggestFeatureBreakdown,
598
+ pathsOverlap,
599
+ SPECS_DIR,
600
+ LEGACY_SPEC,
601
+ SPECS_REGISTRY,
602
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paths.design/caws-cli",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "CAWS CLI - Coding Agent Workflow System command line tools",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "templates"
14
14
  ],
15
15
  "scripts": {
16
- "build": "mkdir -p dist && cp -r src/* dist/ && npm run typecheck",
16
+ "build": "mkdir -p dist && cp -r src/* dist/",
17
17
  "dev": "mkdir -p dist && cp -r src/* dist/ && node dist/index.js",
18
18
  "typecheck": "tsc --emitDeclarationOnly --outDir dist",
19
19
  "start": "node dist/index.js",
@@ -51,12 +51,10 @@
51
51
  "directory": "packages/caws-cli"
52
52
  },
53
53
  "dependencies": {
54
- "ajv": "^8.12.0",
55
54
  "chalk": "4.1.2",
56
55
  "commander": "^11.0.0",
57
56
  "fs-extra": "^11.0.0",
58
- "inquirer": "8.2.7",
59
- "js-yaml": "4.1.0"
57
+ "inquirer": "8.2.7"
60
58
  },
61
59
  "devDependencies": {
62
60
  "@eslint/js": "^9.0.0",
@@ -68,10 +66,13 @@
68
66
  "@types/inquirer": "^8.2.6",
69
67
  "@types/js-yaml": "^4.0.0",
70
68
  "@types/node": "^20.0.0",
69
+ "ajv": "8.17.1",
71
70
  "esbuild": "0.25.10",
72
71
  "eslint": "^9.0.0",
73
72
  "jest": "30.1.3",
73
+ "js-yaml": "4.1.0",
74
74
  "lint-staged": "15.5.2",
75
+ "micromatch": "4.0.8",
75
76
  "prettier": "^3.0.0",
76
77
  "semantic-release": "25.0.0-beta.6",
77
78
  "typescript": "^5.0.0"