@paths.design/caws-cli 8.2.1 → 8.3.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 (51) hide show
  1. package/dist/budget-derivation.js +10 -10
  2. package/dist/commands/archive.js +22 -22
  3. package/dist/commands/burnup.js +7 -7
  4. package/dist/commands/diagnose.js +25 -25
  5. package/dist/commands/evaluate.js +20 -20
  6. package/dist/commands/init.js +71 -72
  7. package/dist/commands/iterate.js +21 -21
  8. package/dist/commands/mode.js +11 -11
  9. package/dist/commands/plan.js +5 -5
  10. package/dist/commands/provenance.js +86 -86
  11. package/dist/commands/quality-gates.js +3 -3
  12. package/dist/commands/quality-monitor.js +17 -17
  13. package/dist/commands/session.js +312 -0
  14. package/dist/commands/specs.js +44 -44
  15. package/dist/commands/status.js +43 -43
  16. package/dist/commands/templates.js +14 -14
  17. package/dist/commands/tool.js +1 -1
  18. package/dist/commands/troubleshoot.js +11 -11
  19. package/dist/commands/tutorial.js +119 -119
  20. package/dist/commands/validate.js +6 -6
  21. package/dist/commands/waivers.js +93 -60
  22. package/dist/commands/workflow.js +17 -17
  23. package/dist/commands/worktree.js +13 -13
  24. package/dist/config/index.js +5 -5
  25. package/dist/config/modes.js +7 -7
  26. package/dist/constants/spec-types.js +5 -5
  27. package/dist/error-handler.js +4 -4
  28. package/dist/generators/jest-config-generator.js +3 -3
  29. package/dist/generators/working-spec.js +4 -4
  30. package/dist/index.js +79 -27
  31. package/dist/minimal-cli.js +9 -9
  32. package/dist/policy/PolicyManager.js +1 -1
  33. package/dist/scaffold/claude-hooks.js +7 -7
  34. package/dist/scaffold/cursor-hooks.js +8 -8
  35. package/dist/scaffold/git-hooks.js +152 -152
  36. package/dist/scaffold/index.js +48 -48
  37. package/dist/session/session-manager.js +548 -0
  38. package/dist/test-analysis.js +20 -20
  39. package/dist/utils/command-wrapper.js +8 -8
  40. package/dist/utils/detection.js +7 -7
  41. package/dist/utils/finalization.js +21 -21
  42. package/dist/utils/git-lock.js +3 -3
  43. package/dist/utils/gitignore-updater.js +1 -1
  44. package/dist/utils/project-analysis.js +7 -7
  45. package/dist/utils/quality-gates-utils.js +35 -35
  46. package/dist/utils/spec-resolver.js +8 -8
  47. package/dist/utils/typescript-detector.js +5 -5
  48. package/dist/utils/yaml-validation.js +1 -1
  49. package/dist/validation/spec-validation.js +4 -4
  50. package/dist/worktree/worktree-manager.js +11 -5
  51. package/package.json +1 -1
@@ -0,0 +1,548 @@
1
+ /**
2
+ * @fileoverview CAWS Session Capsule Manager
3
+ * Manages session lifecycle and capsule persistence for multi-agent coordination.
4
+ * Each session produces a structured capsule that captures baseline state on entry
5
+ * and work summary + verification evidence on exit.
6
+ * @author @darianrosebrook
7
+ */
8
+
9
+ const { execFileSync } = require('child_process');
10
+ const fs = require('fs-extra');
11
+ const path = require('path');
12
+ const crypto = require('crypto');
13
+
14
+ const SESSIONS_DIR = '.caws/sessions';
15
+ const REGISTRY_FILE = '.caws/sessions.json';
16
+ const CAPSULE_SCHEMA_VERSION = 'caws.capsule.v1';
17
+
18
+ /**
19
+ * Get the git repository root
20
+ * @returns {string} Absolute path to repo root
21
+ */
22
+ function getRepoRoot() {
23
+ return execFileSync('git', ['rev-parse', '--show-toplevel'], {
24
+ encoding: 'utf8',
25
+ }).trim();
26
+ }
27
+
28
+ /**
29
+ * Get current HEAD revision (short hash)
30
+ * @param {string} cwd - Working directory
31
+ * @returns {string}
32
+ */
33
+ function getHeadRev(cwd) {
34
+ try {
35
+ return execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
36
+ encoding: 'utf8',
37
+ cwd,
38
+ }).trim();
39
+ } catch {
40
+ return 'unknown';
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Get current branch name
46
+ * @param {string} cwd - Working directory
47
+ * @returns {string}
48
+ */
49
+ function getCurrentBranch(cwd) {
50
+ try {
51
+ return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], {
52
+ encoding: 'utf8',
53
+ cwd,
54
+ }).trim();
55
+ } catch {
56
+ return 'detached';
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Get dirty files in working tree
62
+ * @param {string} cwd - Working directory
63
+ * @returns {{ paths: string[], dirty: boolean }}
64
+ */
65
+ function getWorkspaceFingerprint(cwd) {
66
+ try {
67
+ const output = execFileSync('git', ['status', '--porcelain'], {
68
+ encoding: 'utf8',
69
+ cwd,
70
+ });
71
+ const paths = output
72
+ .split('\n')
73
+ .filter(Boolean)
74
+ .map((line) => line.substring(3).trim());
75
+ return { paths_touched: paths, dirty: paths.length > 0 };
76
+ } catch {
77
+ return { paths_touched: [], dirty: false };
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Get project name from working spec or directory
83
+ * @param {string} root - Repository root
84
+ * @returns {string}
85
+ */
86
+ function getProjectName(root) {
87
+ try {
88
+ const yaml = require('js-yaml');
89
+ const specPath = path.join(root, '.caws/working-spec.yaml');
90
+ if (fs.existsSync(specPath)) {
91
+ const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
92
+ return spec.title || spec.id || path.basename(root);
93
+ }
94
+ } catch {
95
+ // Fall through
96
+ }
97
+ return path.basename(root);
98
+ }
99
+
100
+ /**
101
+ * Get skein ID from working spec
102
+ * @param {string} root - Repository root
103
+ * @returns {string}
104
+ */
105
+ function getSkeinId(root) {
106
+ try {
107
+ const yaml = require('js-yaml');
108
+ const specPath = path.join(root, '.caws/working-spec.yaml');
109
+ if (fs.existsSync(specPath)) {
110
+ const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
111
+ return spec.id || 'unknown';
112
+ }
113
+ } catch {
114
+ // Fall through
115
+ }
116
+ return 'unknown';
117
+ }
118
+
119
+ /**
120
+ * Load the session registry
121
+ * @param {string} root - Repository root
122
+ * @returns {Object} Registry object
123
+ */
124
+ function loadRegistry(root) {
125
+ const registryPath = path.join(root, REGISTRY_FILE);
126
+ try {
127
+ if (fs.existsSync(registryPath)) {
128
+ return JSON.parse(fs.readFileSync(registryPath, 'utf8'));
129
+ }
130
+ } catch {
131
+ // Corrupted registry, start fresh
132
+ }
133
+ return { version: 1, sessions: {} };
134
+ }
135
+
136
+ /**
137
+ * Save the session registry
138
+ * @param {string} root - Repository root
139
+ * @param {Object} registry - Registry object
140
+ */
141
+ function saveRegistry(root, registry) {
142
+ const registryPath = path.join(root, REGISTRY_FILE);
143
+ fs.ensureDirSync(path.dirname(registryPath));
144
+ fs.writeFileSync(registryPath, JSON.stringify(registry, null, 2));
145
+ }
146
+
147
+ /**
148
+ * Generate a deterministic session ID
149
+ * @returns {string}
150
+ */
151
+ function generateSessionId() {
152
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
153
+ const suffix = crypto.randomBytes(4).toString('hex');
154
+ return `${timestamp}__${suffix}`;
155
+ }
156
+
157
+ /**
158
+ * Start a new session, creating the initial capsule with baseline state
159
+ * @param {Object} options - Session options
160
+ * @param {string} [options.role] - Agent role (worker, integrator, qa)
161
+ * @param {string} [options.specId] - Associated feature spec ID
162
+ * @param {string[]} [options.allowedGlobs] - Allowed file patterns
163
+ * @param {string[]} [options.forbiddenGlobs] - Forbidden file patterns
164
+ * @param {string} [options.intent] - What this session intends to accomplish
165
+ * @returns {Object} Created capsule
166
+ */
167
+ function startSession(options = {}) {
168
+ const root = getRepoRoot();
169
+ const registry = loadRegistry(root);
170
+ const sessionId = generateSessionId();
171
+
172
+ const {
173
+ role = 'worker',
174
+ specId,
175
+ allowedGlobs = [],
176
+ forbiddenGlobs = [],
177
+ intent = '',
178
+ } = options;
179
+
180
+ // Build scope from spec if available and no explicit globs provided
181
+ let scope = {
182
+ allowed_globs: allowedGlobs,
183
+ forbidden_globs: forbiddenGlobs,
184
+ };
185
+
186
+ if (specId && allowedGlobs.length === 0) {
187
+ try {
188
+ const yaml = require('js-yaml');
189
+ const specPath = path.join(root, `.caws/specs/${specId}.yaml`);
190
+ if (fs.existsSync(specPath)) {
191
+ const spec = yaml.load(fs.readFileSync(specPath, 'utf8'));
192
+ if (spec.scope) {
193
+ scope.allowed_globs = spec.scope.in || [];
194
+ scope.forbidden_globs = spec.scope.out || [];
195
+ }
196
+ }
197
+ } catch {
198
+ // Non-fatal: scope stays as provided
199
+ }
200
+ }
201
+
202
+ const capsule = {
203
+ schema: CAPSULE_SCHEMA_VERSION,
204
+ project: getProjectName(root),
205
+ skein_id: getSkeinId(root),
206
+ session_id: sessionId,
207
+ role,
208
+ spec_id: specId || null,
209
+ scope,
210
+ base_state: {
211
+ head_rev: getHeadRev(root),
212
+ branch: getCurrentBranch(root),
213
+ workspace_fingerprint: getWorkspaceFingerprint(root),
214
+ },
215
+ started_at: new Date().toISOString(),
216
+ ended_at: null,
217
+ work_summary: {
218
+ intent: intent || '',
219
+ paths_touched: [],
220
+ artifacts_written: [],
221
+ commits: [],
222
+ },
223
+ verification: {
224
+ tests_run: [],
225
+ determinism_checks: [],
226
+ },
227
+ known_issues: [],
228
+ handoff: {
229
+ next_actions: [],
230
+ risk_notes: [],
231
+ },
232
+ };
233
+
234
+ // Persist capsule
235
+ const sessionsDir = path.join(root, SESSIONS_DIR);
236
+ fs.ensureDirSync(sessionsDir);
237
+ const capsulePath = path.join(sessionsDir, `${sessionId}.json`);
238
+ fs.writeFileSync(capsulePath, JSON.stringify(capsule, null, 2));
239
+
240
+ // Update registry
241
+ registry.sessions[sessionId] = {
242
+ path: `${sessionId}.json`,
243
+ role,
244
+ spec_id: specId || null,
245
+ status: 'active',
246
+ started_at: capsule.started_at,
247
+ ended_at: null,
248
+ head_rev: capsule.base_state.head_rev,
249
+ branch: capsule.base_state.branch,
250
+ };
251
+ saveRegistry(root, registry);
252
+
253
+ return capsule;
254
+ }
255
+
256
+ /**
257
+ * Add a checkpoint to the current (most recent active) session
258
+ * @param {Object} data - Checkpoint data
259
+ * @param {string} [data.sessionId] - Specific session ID (uses latest active if omitted)
260
+ * @param {string[]} [data.pathsTouched] - Files changed
261
+ * @param {string[]} [data.artifactsWritten] - Generated artifacts
262
+ * @param {Object[]} [data.testsRun] - Test results { name, status, evidence }
263
+ * @param {Object[]} [data.determinismChecks] - Determinism checks { name, status, total }
264
+ * @param {Object[]} [data.knownIssues] - Issues discovered { type, description }
265
+ * @param {string} [data.intent] - Updated intent description
266
+ * @returns {Object} Updated capsule
267
+ */
268
+ function checkpointSession(data = {}) {
269
+ const root = getRepoRoot();
270
+ const registry = loadRegistry(root);
271
+
272
+ // Find session
273
+ const sessionId = data.sessionId || findActiveSession(registry);
274
+ if (!sessionId) {
275
+ throw new Error('No active session found. Start one with: caws session start');
276
+ }
277
+
278
+ const capsulePath = path.join(root, SESSIONS_DIR, `${sessionId}.json`);
279
+ if (!fs.existsSync(capsulePath)) {
280
+ throw new Error(`Session capsule not found: ${sessionId}`);
281
+ }
282
+
283
+ const capsule = JSON.parse(fs.readFileSync(capsulePath, 'utf8'));
284
+
285
+ // Merge checkpoint data
286
+ if (data.intent) {
287
+ capsule.work_summary.intent = data.intent;
288
+ }
289
+ if (data.pathsTouched) {
290
+ const existing = new Set(capsule.work_summary.paths_touched);
291
+ for (const p of data.pathsTouched) existing.add(p);
292
+ capsule.work_summary.paths_touched = [...existing];
293
+ }
294
+ if (data.artifactsWritten) {
295
+ const existing = new Set(capsule.work_summary.artifacts_written);
296
+ for (const a of data.artifactsWritten) existing.add(a);
297
+ capsule.work_summary.artifacts_written = [...existing];
298
+ }
299
+ if (data.testsRun) {
300
+ capsule.verification.tests_run.push(...data.testsRun);
301
+ }
302
+ if (data.determinismChecks) {
303
+ capsule.verification.determinism_checks.push(...data.determinismChecks);
304
+ }
305
+ if (data.knownIssues) {
306
+ capsule.known_issues.push(...data.knownIssues);
307
+ }
308
+
309
+ // Record current commit as a checkpoint
310
+ const currentRev = getHeadRev(root);
311
+ if (currentRev !== capsule.base_state.head_rev) {
312
+ capsule.work_summary.commits.push({
313
+ rev: currentRev,
314
+ checkpoint_at: new Date().toISOString(),
315
+ });
316
+ }
317
+
318
+ // Write updated capsule
319
+ fs.writeFileSync(capsulePath, JSON.stringify(capsule, null, 2));
320
+
321
+ return capsule;
322
+ }
323
+
324
+ /**
325
+ * End a session, finalizing the capsule with handoff information
326
+ * @param {Object} data - End session data
327
+ * @param {string} [data.sessionId] - Specific session ID (uses latest active if omitted)
328
+ * @param {string[]} [data.nextActions] - What the next session should do
329
+ * @param {string[]} [data.riskNotes] - Risk notes for handoff
330
+ * @returns {Object} Finalized capsule
331
+ */
332
+ function endSession(data = {}) {
333
+ const root = getRepoRoot();
334
+ const registry = loadRegistry(root);
335
+
336
+ const sessionId = data.sessionId || findActiveSession(registry);
337
+ if (!sessionId) {
338
+ throw new Error('No active session found.');
339
+ }
340
+
341
+ const capsulePath = path.join(root, SESSIONS_DIR, `${sessionId}.json`);
342
+ if (!fs.existsSync(capsulePath)) {
343
+ throw new Error(`Session capsule not found: ${sessionId}`);
344
+ }
345
+
346
+ const capsule = JSON.parse(fs.readFileSync(capsulePath, 'utf8'));
347
+
348
+ // Finalize
349
+ capsule.ended_at = new Date().toISOString();
350
+
351
+ // Capture final workspace state
352
+ const fingerprint = getWorkspaceFingerprint(root);
353
+ capsule.work_summary.paths_touched = [
354
+ ...new Set([...capsule.work_summary.paths_touched, ...fingerprint.paths_touched]),
355
+ ];
356
+
357
+ // Record final commit
358
+ const finalRev = getHeadRev(root);
359
+ if (
360
+ finalRev !== capsule.base_state.head_rev &&
361
+ !capsule.work_summary.commits.some((c) => c.rev === finalRev)
362
+ ) {
363
+ capsule.work_summary.commits.push({
364
+ rev: finalRev,
365
+ checkpoint_at: new Date().toISOString(),
366
+ });
367
+ }
368
+
369
+ // Handoff
370
+ if (data.nextActions) {
371
+ capsule.handoff.next_actions = data.nextActions;
372
+ }
373
+ if (data.riskNotes) {
374
+ capsule.handoff.risk_notes = data.riskNotes;
375
+ }
376
+
377
+ // Flag if dirty
378
+ if (fingerprint.dirty) {
379
+ capsule.known_issues.push({
380
+ type: 'warning',
381
+ description: `Session ended with ${fingerprint.paths_touched.length} uncommitted file(s).`,
382
+ });
383
+ }
384
+
385
+ // Write finalized capsule
386
+ fs.writeFileSync(capsulePath, JSON.stringify(capsule, null, 2));
387
+
388
+ // Update registry
389
+ registry.sessions[sessionId].status = 'completed';
390
+ registry.sessions[sessionId].ended_at = capsule.ended_at;
391
+ saveRegistry(root, registry);
392
+
393
+ return capsule;
394
+ }
395
+
396
+ /**
397
+ * List all sessions
398
+ * @param {Object} [options] - List options
399
+ * @param {string} [options.status] - Filter by status (active, completed)
400
+ * @param {number} [options.limit] - Max entries to return
401
+ * @returns {Object[]} Session entries
402
+ */
403
+ function listSessions(options = {}) {
404
+ const root = getRepoRoot();
405
+ const registry = loadRegistry(root);
406
+
407
+ let entries = Object.entries(registry.sessions).map(([id, meta]) => ({
408
+ id,
409
+ ...meta,
410
+ }));
411
+
412
+ if (options.status) {
413
+ entries = entries.filter((e) => e.status === options.status);
414
+ }
415
+
416
+ // Sort by started_at descending (most recent first)
417
+ entries.sort((a, b) => new Date(b.started_at) - new Date(a.started_at));
418
+
419
+ if (options.limit) {
420
+ entries = entries.slice(0, options.limit);
421
+ }
422
+
423
+ return entries;
424
+ }
425
+
426
+ /**
427
+ * Show a specific session's full capsule
428
+ * @param {string} sessionId - Session ID (or "latest" for most recent)
429
+ * @returns {Object} Full capsule
430
+ */
431
+ function showSession(sessionId) {
432
+ const root = getRepoRoot();
433
+
434
+ if (sessionId === 'latest') {
435
+ const registry = loadRegistry(root);
436
+ const active = findActiveSession(registry);
437
+ if (active) {
438
+ sessionId = active;
439
+ } else {
440
+ // Find most recent completed
441
+ const entries = Object.entries(registry.sessions).sort(
442
+ (a, b) => new Date(b[1].started_at) - new Date(a[1].started_at)
443
+ );
444
+ if (entries.length === 0) throw new Error('No sessions found.');
445
+ sessionId = entries[0][0];
446
+ }
447
+ }
448
+
449
+ const capsulePath = path.join(root, SESSIONS_DIR, `${sessionId}.json`);
450
+ if (!fs.existsSync(capsulePath)) {
451
+ throw new Error(`Session capsule not found: ${sessionId}`);
452
+ }
453
+
454
+ return JSON.parse(fs.readFileSync(capsulePath, 'utf8'));
455
+ }
456
+
457
+ /**
458
+ * Briefing output for session start hooks - returns structured text
459
+ * @returns {string} Briefing text
460
+ */
461
+ function getBriefing() {
462
+ const root = getRepoRoot();
463
+ const registry = loadRegistry(root);
464
+
465
+ const lines = [];
466
+ lines.push('--- CAWS Session Briefing ---');
467
+
468
+ // Git baseline
469
+ const headRev = getHeadRev(root);
470
+ const branch = getCurrentBranch(root);
471
+ const fingerprint = getWorkspaceFingerprint(root);
472
+ lines.push(`Git: ${branch} @ ${headRev} (${fingerprint.paths_touched.length} dirty files)`);
473
+
474
+ if (fingerprint.dirty) {
475
+ lines.push('WARNING: Working tree has uncommitted changes from a prior session.');
476
+ }
477
+
478
+ // Active sessions
479
+ const activeSessions = Object.entries(registry.sessions)
480
+ .filter(([, meta]) => meta.status === 'active')
481
+ .map(([id, meta]) => ({ id, ...meta }));
482
+
483
+ if (activeSessions.length > 0) {
484
+ lines.push(`Active sessions: ${activeSessions.length}`);
485
+ for (const s of activeSessions) {
486
+ lines.push(` - ${s.id} (${s.role}, spec: ${s.spec_id || 'none'})`);
487
+ }
488
+ }
489
+
490
+ // Last completed session handoff
491
+ const completedSessions = Object.entries(registry.sessions)
492
+ .filter(([, meta]) => meta.status === 'completed')
493
+ .sort((a, b) => new Date(b[1].ended_at) - new Date(a[1].ended_at));
494
+
495
+ if (completedSessions.length > 0) {
496
+ const [lastId] = completedSessions[0];
497
+ try {
498
+ const capsule = showSession(lastId);
499
+ if (capsule.handoff.next_actions.length > 0) {
500
+ lines.push('Handoff from prior session:');
501
+ for (const action of capsule.handoff.next_actions) {
502
+ lines.push(` - ${action}`);
503
+ }
504
+ }
505
+ if (capsule.known_issues.length > 0) {
506
+ lines.push('Known issues from prior session:');
507
+ for (const issue of capsule.known_issues) {
508
+ lines.push(` - [${issue.type}] ${issue.description}`);
509
+ }
510
+ }
511
+ } catch {
512
+ // Non-fatal
513
+ }
514
+ }
515
+
516
+ lines.push('---');
517
+ lines.push("Run 'caws session start' to begin a tracked session.");
518
+ lines.push('--- End CAWS Briefing ---');
519
+
520
+ return lines.join('\n');
521
+ }
522
+
523
+ /**
524
+ * Find the most recent active session
525
+ * @param {Object} registry - Session registry
526
+ * @returns {string|null} Session ID or null
527
+ */
528
+ function findActiveSession(registry) {
529
+ const active = Object.entries(registry.sessions)
530
+ .filter(([, meta]) => meta.status === 'active')
531
+ .sort((a, b) => new Date(b[1].started_at) - new Date(a[1].started_at));
532
+
533
+ return active.length > 0 ? active[0][0] : null;
534
+ }
535
+
536
+ module.exports = {
537
+ startSession,
538
+ checkpointSession,
539
+ endSession,
540
+ listSessions,
541
+ showSession,
542
+ getBriefing,
543
+ loadRegistry,
544
+ getRepoRoot,
545
+ SESSIONS_DIR,
546
+ REGISTRY_FILE,
547
+ CAPSULE_SCHEMA_VERSION,
548
+ };
@@ -607,7 +607,7 @@ async function testAnalysisCommand(subcommand, options = []) {
607
607
  case 'find-similar':
608
608
  return await handleFindSimilar(options);
609
609
  default:
610
- console.log(chalk.red('Unknown test-analysis subcommand'));
610
+ console.log(chalk.red('Unknown test-analysis subcommand'));
611
611
  console.log('Available commands:');
612
612
  console.log(' assess-budget - Analyze budget needs for current spec');
613
613
  console.log(' analyze-patterns - Show waiver pattern analysis');
@@ -615,7 +615,7 @@ async function testAnalysisCommand(subcommand, options = []) {
615
615
  return;
616
616
  }
617
617
  } catch (error) {
618
- console.error(chalk.red('Test analysis failed:'), error.message);
618
+ console.error(chalk.red('Test analysis failed:'), error.message);
619
619
  }
620
620
  }
621
621
 
@@ -639,8 +639,8 @@ async function handleAssessBudget(options) {
639
639
  const specContent = fs.readFileSync(specPath, 'utf8');
640
640
  const spec = yaml.load(specContent);
641
641
 
642
- console.log(chalk.cyan(`📊 Budget Assessment for ${spec.id}`));
643
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
642
+ console.log(chalk.cyan(`Budget Assessment for ${spec.id}`));
643
+ console.log('==============================================');
644
644
 
645
645
  const result = predictor.assessBudget(spec);
646
646
 
@@ -650,14 +650,14 @@ async function handleAssessBudget(options) {
650
650
  `Historical Analysis: ${assessment.similar_projects_analyzed} similar projects analyzed`
651
651
  );
652
652
  console.log(
653
- `🎯 Recommended Budget: ${assessment.recommended_budget.files} files, ${assessment.recommended_budget.loc} LOC (+${assessment.buffer_applied.files_percent}% buffer)`
653
+ `Recommended Budget: ${assessment.recommended_budget.files} files, ${assessment.recommended_budget.loc} LOC (+${assessment.buffer_applied.files_percent}% buffer)`
654
654
  );
655
- console.log(`💡 Rationale: ${assessment.rationale.join('; ')}`);
655
+ console.log(`Rationale: ${assessment.rationale.join('; ')}`);
656
656
 
657
657
  if (assessment.risk_factors.length > 0) {
658
658
  console.log(
659
659
  chalk.yellow(
660
- `⚠️ Risk Factors: ${assessment.risk_factors.map((f) => f.description).join('; ')}`
660
+ `Risk Factors: ${assessment.risk_factors.map((f) => f.description).join('; ')}`
661
661
  )
662
662
  );
663
663
  }
@@ -666,15 +666,15 @@ async function handleAssessBudget(options) {
666
666
  assessment.confidence > 0.8 ? 'High' : assessment.confidence > 0.6 ? 'Medium' : 'Low';
667
667
  console.log(
668
668
  chalk.green(
669
- `✅ Confidence: ${confidenceLevel} (${Math.round(assessment.confidence * 100)}%)`
669
+ `Confidence: ${confidenceLevel} (${Math.round(assessment.confidence * 100)}%)`
670
670
  )
671
671
  );
672
672
  } else {
673
- console.log(chalk.yellow(`⚠️ ${result.message}`));
674
- console.log('💡 Consider using default tier-based budgeting for now');
673
+ console.log(chalk.yellow(`${result.message}`));
674
+ console.log('Consider using default tier-based budgeting for now');
675
675
  }
676
676
  } catch (error) {
677
- console.error(chalk.red('Failed to load spec:'), error.message);
677
+ console.error(chalk.red('Failed to load spec:'), error.message);
678
678
  }
679
679
  }
680
680
 
@@ -685,8 +685,8 @@ async function handleAnalyzePatterns(options) {
685
685
  const chalk = (await import('chalk')).default;
686
686
  const learner = new WaiverPatternLearner();
687
687
 
688
- console.log(chalk.cyan('🔍 Analyzing Waiver Patterns'));
689
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
688
+ console.log(chalk.cyan('Analyzing Waiver Patterns'));
689
+ console.log('==============================================');
690
690
 
691
691
  const result = learner.analyzePatterns();
692
692
 
@@ -696,7 +696,7 @@ async function handleAnalyzePatterns(options) {
696
696
  console.log(`Total waivers analyzed: ${patterns.total_waivers}`);
697
697
 
698
698
  if (patterns.budget_overruns) {
699
- console.log('\n💰 Budget Overrun Patterns:');
699
+ console.log('\nBudget Overrun Patterns:');
700
700
  console.log(
701
701
  ` Average overrun: ${patterns.budget_overruns.average_overrun_files} files, ${patterns.budget_overruns.average_overrun_loc} LOC`
702
702
  );
@@ -712,7 +712,7 @@ async function handleAnalyzePatterns(options) {
712
712
  }
713
713
 
714
714
  if (patterns.common_reasons.length > 0) {
715
- console.log('\n📋 Most Common Waiver Reasons:');
715
+ console.log('\nMost Common Waiver Reasons:');
716
716
  patterns.common_reasons.slice(0, 5).forEach((reason) => {
717
717
  console.log(
718
718
  ` ${reason.reason}: ${reason.count} times (${Math.round(reason.frequency * 100)}%)`
@@ -720,7 +720,7 @@ async function handleAnalyzePatterns(options) {
720
720
  });
721
721
  }
722
722
  } else {
723
- console.log(chalk.yellow(`⚠️ ${result.message}`));
723
+ console.log(chalk.yellow(`${result.message}`));
724
724
  }
725
725
  }
726
726
 
@@ -744,8 +744,8 @@ async function handleFindSimilar(options) {
744
744
  const specContent = fs.readFileSync(specPath, 'utf8');
745
745
  const spec = yaml.load(specContent);
746
746
 
747
- console.log(chalk.cyan(`🔍 Finding projects similar to ${spec.id}`));
748
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
747
+ console.log(chalk.cyan(`Finding projects similar to ${spec.id}`));
748
+ console.log('==============================================');
749
749
 
750
750
  const similar = matcher.findSimilarProjects(spec);
751
751
 
@@ -758,10 +758,10 @@ async function handleFindSimilar(options) {
758
758
  );
759
759
  });
760
760
  } else {
761
- console.log(chalk.yellow('⚠️ No similar projects found'));
761
+ console.log(chalk.yellow('No similar projects found'));
762
762
  }
763
763
  } catch (error) {
764
- console.error(chalk.red('Failed to load spec:'), error.message);
764
+ console.error(chalk.red('Failed to load spec:'), error.message);
765
765
  }
766
766
  }
767
767