@lumenflow/cli 2.8.0 → 2.10.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.
package/dist/doctor.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * @file doctor.ts
3
3
  * LumenFlow health check command (WU-1177)
4
4
  * WU-1191: Lane health check integration
5
+ * WU-1386: Agent-friction checks (managed-file dirty, WU validity, worktree sanity)
5
6
  * Verifies all safety components are installed and configured correctly
6
7
  */
7
8
  import * as fs from 'node:fs';
@@ -9,8 +10,20 @@ import * as path from 'node:path';
9
10
  import { execFileSync } from 'node:child_process';
10
11
  import { createWUParser } from '@lumenflow/core';
11
12
  import { loadLaneDefinitions, detectLaneOverlaps } from './lane-health.js';
13
+ /**
14
+ * WU-1386: Managed files that should not have uncommitted changes
15
+ */
16
+ const MANAGED_FILE_PATTERNS = [
17
+ '.lumenflow.config.yaml',
18
+ '.lumenflow.lane-inference.yaml',
19
+ 'AGENTS.md',
20
+ 'CLAUDE.md',
21
+ ];
22
+ /** WU-1386: Managed directories with glob patterns */
23
+ const MANAGED_DIR_PATTERNS = ['docs/04-operations/tasks/'];
12
24
  /**
13
25
  * CLI option definitions for doctor command
26
+ * WU-1386: Added --deep flag
14
27
  */
15
28
  const DOCTOR_OPTIONS = {
16
29
  verbose: {
@@ -23,9 +36,15 @@ const DOCTOR_OPTIONS = {
23
36
  flags: '--json',
24
37
  description: 'Output results as JSON',
25
38
  },
39
+ deep: {
40
+ name: 'deep',
41
+ flags: '--deep',
42
+ description: 'Run heavier checks including WU validation (slower)',
43
+ },
26
44
  };
27
45
  /**
28
46
  * Parse doctor command options
47
+ * WU-1386: Added deep flag
29
48
  */
30
49
  export function parseDoctorOptions() {
31
50
  const opts = createWUParser({
@@ -36,6 +55,7 @@ export function parseDoctorOptions() {
36
55
  return {
37
56
  verbose: opts.verbose ?? false,
38
57
  json: opts.json ?? false,
58
+ deep: opts.deep ?? false,
39
59
  };
40
60
  }
41
61
  /**
@@ -252,10 +272,314 @@ function checkPrerequisites() {
252
272
  },
253
273
  };
254
274
  }
275
+ /**
276
+ * WU-1387: Get git repository root directory
277
+ * This ensures managed-file detection works from subdirectories
278
+ */
279
+ function getGitRepoRoot(projectDir) {
280
+ try {
281
+ const repoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
282
+ cwd: projectDir,
283
+ encoding: 'utf-8',
284
+ stdio: ['pipe', 'pipe', 'pipe'],
285
+ }).trim();
286
+ return repoRoot;
287
+ }
288
+ catch {
289
+ return null;
290
+ }
291
+ }
292
+ /**
293
+ * WU-1386: Check for uncommitted changes to managed files
294
+ * WU-1387: Uses git repo root for path resolution (works from subdirectories)
295
+ */
296
+ async function checkManagedFilesDirty(projectDir) {
297
+ try {
298
+ // WU-1387: Get git repo root to handle subdirectory execution
299
+ const repoRoot = getGitRepoRoot(projectDir);
300
+ if (!repoRoot) {
301
+ return {
302
+ passed: true,
303
+ files: [],
304
+ message: 'Git status check skipped (not a git repository)',
305
+ };
306
+ }
307
+ // Run git status from repo root, not the passed projectDir
308
+ const statusOutput = execFileSync('git', ['status', '--porcelain'], {
309
+ cwd: repoRoot,
310
+ encoding: 'utf-8',
311
+ stdio: ['pipe', 'pipe', 'pipe'],
312
+ });
313
+ const dirtyFiles = [];
314
+ // Parse status output and check each line
315
+ const lines = statusOutput.split('\n').filter((line) => line.trim());
316
+ for (const line of lines) {
317
+ // Status format: XY filename (where XY is 2-char status)
318
+ const filePath = line.slice(3).trim();
319
+ if (!filePath)
320
+ continue;
321
+ // Check if file matches managed patterns
322
+ const isManaged = MANAGED_FILE_PATTERNS.some((pattern) => filePath === pattern) ||
323
+ MANAGED_DIR_PATTERNS.some((dir) => filePath.startsWith(dir));
324
+ if (isManaged) {
325
+ dirtyFiles.push(filePath);
326
+ }
327
+ }
328
+ if (dirtyFiles.length > 0) {
329
+ return {
330
+ passed: false,
331
+ files: dirtyFiles,
332
+ message: `${dirtyFiles.length} managed file(s) have uncommitted changes`,
333
+ };
334
+ }
335
+ return {
336
+ passed: true,
337
+ files: [],
338
+ message: 'No uncommitted changes to managed files',
339
+ };
340
+ }
341
+ catch {
342
+ // Not a git repo or git not available - graceful degradation
343
+ return {
344
+ passed: true,
345
+ files: [],
346
+ message: 'Git status check skipped (not a git repository)',
347
+ };
348
+ }
349
+ }
350
+ function parseWorktreePruneOutput(output) {
351
+ // Parse summary line for orphan directories: "Orphan directories: N"
352
+ const orphanSummaryMatch = output.match(/Orphan directories:\s*(\d+)/);
353
+ const orphansFromSummary = orphanSummaryMatch ? parseInt(orphanSummaryMatch[1], 10) : 0;
354
+ // Also count "orphan director" mentions (the detailed output)
355
+ const orphanDetailMatches = output.match(/orphan director/gi) || [];
356
+ const orphansFromDetail = orphanDetailMatches.length;
357
+ // Use the higher count (summary is authoritative, but detail catches other mentions)
358
+ const orphans = Math.max(orphansFromSummary, orphansFromDetail > 0 ? 1 : 0);
359
+ // Parse specific worktree warnings from wu:prune output
360
+ // These appear as "Stale worktree:", "Blocked worktree:", "Unclaimed worktree:"
361
+ const staleCount = (output.match(/Stale worktree:/gi) || []).length;
362
+ const blockedCount = (output.match(/Blocked worktree:/gi) || []).length;
363
+ const unclaimedCount = (output.match(/Unclaimed worktree:/gi) || []).length;
364
+ // Parse missing tracked worktrees: "Missing tracked worktree" header
365
+ const missingMatch = output.match(/Missing tracked worktree/gi) || [];
366
+ const missingCount = missingMatch.length > 0 ? 1 : 0; // At least one if header present
367
+ // Also count specific missing path lines
368
+ const missingPathMatches = output.match(/worktrees \(tracked by git but directory missing\)/gi);
369
+ const missingFromDetail = missingPathMatches ? missingPathMatches.length : 0;
370
+ // Parse summary warnings and errors
371
+ const warningSummaryMatch = output.match(/Warnings:\s*(\d+)/);
372
+ const warnings = warningSummaryMatch ? parseInt(warningSummaryMatch[1], 10) : 0;
373
+ const errorSummaryMatch = output.match(/Errors:\s*(\d+)/);
374
+ const errors = errorSummaryMatch ? parseInt(errorSummaryMatch[1], 10) : 0;
375
+ return {
376
+ orphans,
377
+ stale: staleCount,
378
+ blocked: blockedCount,
379
+ unclaimed: unclaimedCount,
380
+ missing: Math.max(missingCount, missingFromDetail),
381
+ warnings,
382
+ errors,
383
+ };
384
+ }
385
+ /**
386
+ * WU-1386: Check worktree sanity by calling wu:prune in dry-run mode
387
+ * WU-1387: Enhanced parsing for orphan, missing, stale, blocked, and unclaimed worktrees
388
+ */
389
+ async function checkWorktreeSanity(projectDir) {
390
+ try {
391
+ // First check if this is a git repo
392
+ execFileSync('git', ['rev-parse', '--git-dir'], {
393
+ cwd: projectDir,
394
+ encoding: 'utf-8',
395
+ stdio: ['pipe', 'pipe', 'pipe'],
396
+ });
397
+ // Call wu:prune in dry-run mode (default) via pnpm
398
+ // Capture both stdout and stderr to parse the output
399
+ let pruneOutput;
400
+ let commandFailed = false;
401
+ try {
402
+ pruneOutput = execFileSync('pnpm', ['wu:prune'], {
403
+ cwd: projectDir,
404
+ encoding: 'utf-8',
405
+ stdio: ['pipe', 'pipe', 'pipe'],
406
+ timeout: 30000, // 30 second timeout
407
+ });
408
+ }
409
+ catch (e) {
410
+ const execError = e;
411
+ pruneOutput = (execError.stdout || '') + (execError.stderr || '');
412
+ // Check if this is a "command not found" type error vs wu:prune finding issues
413
+ // If pnpm can't run the script (no package.json, script not defined, etc.), gracefully skip
414
+ const errorMsg = execError.message || pruneOutput || '';
415
+ if (errorMsg.includes('ERR_PNPM') ||
416
+ errorMsg.includes('ENOENT') ||
417
+ errorMsg.includes('Missing script') ||
418
+ errorMsg.includes('command not found') ||
419
+ !pruneOutput.includes('[wu-prune]')) {
420
+ // Command couldn't run - gracefully skip
421
+ return {
422
+ passed: true,
423
+ orphans: 0,
424
+ stale: 0,
425
+ message: 'Worktree check skipped (wu:prune not available)',
426
+ };
427
+ }
428
+ // Otherwise, wu:prune ran but found issues (non-zero exit)
429
+ commandFailed = true;
430
+ }
431
+ // WU-1387: Enhanced parsing for all worktree issue types
432
+ const parsed = parseWorktreePruneOutput(pruneOutput);
433
+ // Calculate total issues (orphans count as issues, plus stale, blocked, unclaimed, missing)
434
+ const totalIssues = parsed.orphans + parsed.stale + parsed.blocked + parsed.unclaimed + parsed.missing;
435
+ const hasIssues = totalIssues > 0 || parsed.warnings > 0 || parsed.errors > 0;
436
+ const passed = !hasIssues && !commandFailed;
437
+ // Build descriptive message
438
+ let message = 'All worktrees are valid';
439
+ if (!passed) {
440
+ const parts = [];
441
+ if (parsed.orphans > 0)
442
+ parts.push(`${parsed.orphans} orphan(s)`);
443
+ if (parsed.missing > 0)
444
+ parts.push(`${parsed.missing} missing`);
445
+ if (parsed.stale > 0)
446
+ parts.push(`${parsed.stale} stale`);
447
+ if (parsed.blocked > 0)
448
+ parts.push(`${parsed.blocked} blocked`);
449
+ if (parsed.unclaimed > 0)
450
+ parts.push(`${parsed.unclaimed} unclaimed`);
451
+ if (parts.length === 0 && (parsed.warnings > 0 || parsed.errors > 0)) {
452
+ parts.push(`${parsed.warnings} warning(s), ${parsed.errors} error(s)`);
453
+ }
454
+ message = parts.length > 0 ? `Worktree issues: ${parts.join(', ')}` : 'Worktree issues found';
455
+ }
456
+ return {
457
+ passed,
458
+ orphans: parsed.orphans,
459
+ stale: parsed.stale + parsed.blocked + parsed.unclaimed + parsed.missing, // Combined for API compat
460
+ message,
461
+ };
462
+ }
463
+ catch {
464
+ // Not a git repo or git not available
465
+ return {
466
+ passed: true,
467
+ orphans: 0,
468
+ stale: 0,
469
+ message: 'Worktree check skipped (not a git repository)',
470
+ };
471
+ }
472
+ }
473
+ /**
474
+ * WU-1386: Run WU validation (--deep mode only)
475
+ * WU-1387: Sets passed=false with clear message when CLI fails to run
476
+ * Calls wu:validate --all --no-strict for full schema/lint validation in warn-only mode
477
+ */
478
+ async function checkWUValidity(projectDir) {
479
+ const wuDir = path.join(projectDir, 'docs', '04-operations', 'tasks', 'wu');
480
+ if (!fs.existsSync(wuDir)) {
481
+ return {
482
+ passed: true,
483
+ total: 0,
484
+ valid: 0,
485
+ invalid: 0,
486
+ warnings: 0,
487
+ message: 'No WU directory found',
488
+ };
489
+ }
490
+ try {
491
+ // Call wu:validate --all --no-strict to get warn-only validation
492
+ // This runs the full schema + lint validation, treating warnings as advisory
493
+ let validateOutput;
494
+ let cliError = false;
495
+ let cliErrorMessage = '';
496
+ try {
497
+ validateOutput = execFileSync('pnpm', ['wu:validate', '--all', '--no-strict'], {
498
+ cwd: projectDir,
499
+ encoding: 'utf-8',
500
+ stdio: ['pipe', 'pipe', 'pipe'],
501
+ timeout: 60000, // 60 second timeout for full validation
502
+ });
503
+ }
504
+ catch (e) {
505
+ // wu:validate exits non-zero if validation fails
506
+ const execError = e;
507
+ validateOutput = (execError.stdout || '') + (execError.stderr || '');
508
+ const errorMsg = execError.message || '';
509
+ // WU-1387: Distinguish between CLI execution failure and validation failure
510
+ // CLI failure = script missing, command not found, ENOENT
511
+ // Validation failure = script ran but found invalid WUs
512
+ if (errorMsg.includes('ERR_PNPM') ||
513
+ errorMsg.includes('ENOENT') ||
514
+ errorMsg.includes('Missing script') ||
515
+ errorMsg.includes('command not found') ||
516
+ execError.code === 'ENOENT' ||
517
+ // If no recognizable wu:validate output, assume CLI failed
518
+ (!validateOutput.includes('[wu:validate]') && !validateOutput.includes('Valid:'))) {
519
+ cliError = true;
520
+ cliErrorMessage = errorMsg.includes('Missing script')
521
+ ? 'wu:validate script not found'
522
+ : errorMsg.includes('ENOENT')
523
+ ? 'pnpm command not available'
524
+ : 'wu:validate could not run';
525
+ }
526
+ }
527
+ // WU-1387: If CLI failed to run, report as failure with clear message
528
+ if (cliError) {
529
+ return {
530
+ passed: false,
531
+ total: 0,
532
+ valid: 0,
533
+ invalid: 0,
534
+ warnings: 0,
535
+ message: `WU validation failed: ${cliErrorMessage}`,
536
+ };
537
+ }
538
+ // Parse output for counts
539
+ // wu:validate outputs summary like:
540
+ // ✓ Valid: N
541
+ // ✗ Invalid: N
542
+ // ⚠ Warnings: N
543
+ const validMatch = validateOutput.match(/Valid:\s*(\d+)/);
544
+ const invalidMatch = validateOutput.match(/Invalid:\s*(\d+)/);
545
+ const warningMatch = validateOutput.match(/Warnings:\s*(\d+)/);
546
+ const validCount = validMatch ? parseInt(validMatch[1], 10) : 0;
547
+ const invalidCount = invalidMatch ? parseInt(invalidMatch[1], 10) : 0;
548
+ const warningCount = warningMatch ? parseInt(warningMatch[1], 10) : 0;
549
+ const total = validCount + invalidCount;
550
+ // In warn-only mode (--no-strict), we only fail on actual invalid WUs
551
+ const passed = invalidCount === 0;
552
+ return {
553
+ passed,
554
+ total,
555
+ valid: validCount,
556
+ invalid: invalidCount,
557
+ warnings: warningCount,
558
+ message: passed
559
+ ? total > 0
560
+ ? `All ${total} WU(s) valid${warningCount > 0 ? ` (${warningCount} warning(s))` : ''}`
561
+ : 'No WUs to validate'
562
+ : `${invalidCount}/${total} WU(s) have issues`,
563
+ };
564
+ }
565
+ catch (e) {
566
+ // WU-1387: Unexpected errors should report failure, not silently pass
567
+ const error = e;
568
+ return {
569
+ passed: false,
570
+ total: 0,
571
+ valid: 0,
572
+ invalid: 0,
573
+ warnings: 0,
574
+ message: `WU validation failed: ${error.message || 'unknown error'}`,
575
+ };
576
+ }
577
+ }
255
578
  /**
256
579
  * Run all doctor checks
580
+ * WU-1386: Added options parameter for --deep flag
257
581
  */
258
- export async function runDoctor(projectDir) {
582
+ export async function runDoctor(projectDir, options = {}) {
259
583
  const checks = {
260
584
  husky: checkHusky(projectDir),
261
585
  safeGit: checkSafeGit(projectDir),
@@ -266,19 +590,137 @@ export async function runDoctor(projectDir) {
266
590
  };
267
591
  const vendorConfigs = checkVendorConfigs(projectDir);
268
592
  const prerequisites = checkPrerequisites();
593
+ // WU-1386: Workflow health checks
594
+ const managedFilesDirty = await checkManagedFilesDirty(projectDir);
595
+ const worktreeSanity = await checkWorktreeSanity(projectDir);
596
+ const workflowHealth = {
597
+ managedFilesDirty,
598
+ worktreeSanity,
599
+ };
600
+ // WU-1386: WU validity check only in --deep mode
601
+ if (options.deep) {
602
+ workflowHealth.wuValidity = await checkWUValidity(projectDir);
603
+ }
269
604
  // Determine overall status
270
605
  // Note: laneHealth is advisory (not included in critical checks)
271
606
  const criticalChecks = [checks.husky, checks.safeGit, checks.agentsMd];
272
607
  const allCriticalPassed = criticalChecks.every((check) => check.passed);
608
+ // WU-1386: Calculate exit code
609
+ // 0 = healthy (all checks pass, no warnings)
610
+ // 1 = warnings (non-critical issues)
611
+ // 2 = errors (critical safety checks failed)
612
+ let exitCode = 0;
613
+ if (!allCriticalPassed) {
614
+ exitCode = 2;
615
+ }
616
+ else if (!managedFilesDirty.passed ||
617
+ !worktreeSanity.passed ||
618
+ (workflowHealth.wuValidity && !workflowHealth.wuValidity.passed)) {
619
+ exitCode = 1;
620
+ }
273
621
  return {
274
622
  status: allCriticalPassed ? 'ACTIVE' : 'INCOMPLETE',
623
+ exitCode,
275
624
  checks,
276
625
  vendorConfigs,
277
626
  prerequisites,
627
+ workflowHealth,
628
+ };
629
+ }
630
+ /**
631
+ * WU-1386: Run doctor for init (non-blocking, warnings only)
632
+ * WU-1387: Shows accurate status including lane health and prerequisite failures
633
+ * This is used after lumenflow init to provide feedback without blocking
634
+ */
635
+ export async function runDoctorForInit(projectDir) {
636
+ const result = await runDoctor(projectDir, { deep: false });
637
+ // Count warnings and errors using check keys, not messages
638
+ let warnings = 0;
639
+ let errors = 0;
640
+ // Critical checks that count as errors (if they fail, safety is compromised)
641
+ const criticalCheckKeys = ['husky', 'safeGit', 'agentsMd'];
642
+ for (const [key, check] of Object.entries(result.checks)) {
643
+ if (!check.passed) {
644
+ if (criticalCheckKeys.includes(key)) {
645
+ errors++;
646
+ }
647
+ else {
648
+ warnings++;
649
+ }
650
+ }
651
+ }
652
+ // WU-1387: Count prerequisite failures as warnings
653
+ for (const [, prereq] of Object.entries(result.prerequisites)) {
654
+ if (!prereq.passed) {
655
+ warnings++;
656
+ }
657
+ }
658
+ // Count workflow health issues as warnings (not errors)
659
+ if (result.workflowHealth) {
660
+ if (!result.workflowHealth.managedFilesDirty.passed)
661
+ warnings++;
662
+ if (!result.workflowHealth.worktreeSanity.passed)
663
+ warnings++;
664
+ }
665
+ // Format concise output
666
+ const lines = [];
667
+ lines.push('[lumenflow doctor] Quick health check...');
668
+ if (result.exitCode === 0 && warnings === 0) {
669
+ lines.push(' All checks passed');
670
+ }
671
+ else {
672
+ // Show critical errors first
673
+ if (!result.checks.husky.passed) {
674
+ lines.push(' Error: Husky hooks not installed');
675
+ }
676
+ if (!result.checks.safeGit.passed) {
677
+ lines.push(' Error: safe-git script not found');
678
+ }
679
+ if (!result.checks.agentsMd.passed) {
680
+ lines.push(' Error: AGENTS.md not found');
681
+ }
682
+ // WU-1387: Show lane health issues
683
+ if (!result.checks.laneHealth.passed) {
684
+ lines.push(` Warning: Lane overlap detected - ${result.checks.laneHealth.message}`);
685
+ }
686
+ // WU-1387: Show prerequisite failures
687
+ if (!result.prerequisites.node.passed) {
688
+ lines.push(` Warning: Node.js version ${result.prerequisites.node.version} (required: ${result.prerequisites.node.required})`);
689
+ }
690
+ if (!result.prerequisites.pnpm.passed) {
691
+ lines.push(` Warning: pnpm version ${result.prerequisites.pnpm.version} (required: ${result.prerequisites.pnpm.required})`);
692
+ }
693
+ if (!result.prerequisites.git.passed) {
694
+ lines.push(` Warning: Git version ${result.prerequisites.git.version} (required: ${result.prerequisites.git.required})`);
695
+ }
696
+ // Workflow health warnings
697
+ if (result.workflowHealth?.managedFilesDirty.files.length) {
698
+ lines.push(` Warning: ${result.workflowHealth.managedFilesDirty.files.length} managed file(s) have uncommitted changes`);
699
+ for (const file of result.workflowHealth.managedFilesDirty.files.slice(0, 3)) {
700
+ lines.push(` -> ${file}`);
701
+ }
702
+ }
703
+ if (result.workflowHealth?.worktreeSanity.orphans) {
704
+ lines.push(` Warning: ${result.workflowHealth.worktreeSanity.orphans} orphan worktree(s) found`);
705
+ }
706
+ if (result.workflowHealth && !result.workflowHealth.worktreeSanity.passed) {
707
+ // WU-1387: Show worktree sanity message if there are issues beyond orphans
708
+ const wsSanity = result.workflowHealth.worktreeSanity;
709
+ if (wsSanity.stale > 0 && wsSanity.orphans === 0) {
710
+ lines.push(` Warning: ${wsSanity.message}`);
711
+ }
712
+ }
713
+ }
714
+ return {
715
+ blocked: false, // Never blocks init
716
+ warnings,
717
+ errors,
718
+ output: lines.join('\n'),
278
719
  };
279
720
  }
280
721
  /**
281
722
  * Format doctor output for terminal display
723
+ * WU-1386: Added workflow health section
282
724
  */
283
725
  export function formatDoctorOutput(result) {
284
726
  const lines = [];
@@ -300,6 +742,31 @@ export function formatDoctorOutput(result) {
300
742
  lines.push('');
301
743
  lines.push('Lane Health:');
302
744
  lines.push(formatCheck(result.checks.laneHealth));
745
+ // WU-1386: Workflow Health section
746
+ if (result.workflowHealth) {
747
+ lines.push('');
748
+ lines.push('Workflow Health:');
749
+ const mfd = result.workflowHealth.managedFilesDirty;
750
+ const mfdSymbol = mfd.passed ? '✓' : '⚠';
751
+ lines.push(` ${mfdSymbol} ${mfd.message}`);
752
+ if (!mfd.passed && mfd.files.length > 0) {
753
+ for (const file of mfd.files.slice(0, 5)) {
754
+ lines.push(` → ${file}`);
755
+ }
756
+ if (mfd.files.length > 5) {
757
+ lines.push(` → ... and ${mfd.files.length - 5} more`);
758
+ }
759
+ }
760
+ const ws = result.workflowHealth.worktreeSanity;
761
+ const wsSymbol = ws.passed ? '✓' : '⚠';
762
+ lines.push(` ${wsSymbol} ${ws.message}`);
763
+ // WU validity (only in --deep mode)
764
+ if (result.workflowHealth.wuValidity) {
765
+ const wv = result.workflowHealth.wuValidity;
766
+ const wvSymbol = wv.passed ? '✓' : '⚠';
767
+ lines.push(` ${wvSymbol} ${wv.message}`);
768
+ }
769
+ }
303
770
  // Vendor configs
304
771
  lines.push('');
305
772
  lines.push('Vendor Configs:');
@@ -324,7 +791,12 @@ export function formatDoctorOutput(result) {
324
791
  lines.push('');
325
792
  lines.push('─'.repeat(45));
326
793
  lines.push(`LumenFlow safety: ${result.status}`);
327
- if (result.status === 'INCOMPLETE') {
794
+ // WU-1386: Show exit code meaning
795
+ if (result.exitCode === 1) {
796
+ lines.push('');
797
+ lines.push('Warnings detected (exit code 1). Use --deep for full WU validation.');
798
+ }
799
+ else if (result.exitCode === 2) {
328
800
  lines.push('');
329
801
  lines.push('To fix missing components:');
330
802
  lines.push(' pnpm install && pnpm prepare # Install Husky hooks');
@@ -341,26 +813,25 @@ export function formatDoctorJson(result) {
341
813
  }
342
814
  /**
343
815
  * CLI entry point
816
+ * WU-1386: Updated to use new exit codes
344
817
  */
345
818
  export async function main() {
346
819
  const opts = parseDoctorOptions();
347
820
  const projectDir = process.cwd();
348
- const result = await runDoctor(projectDir);
821
+ const result = await runDoctor(projectDir, { deep: opts.deep });
349
822
  if (opts.json) {
350
823
  console.log(formatDoctorJson(result));
351
824
  }
352
825
  else {
353
826
  console.log(formatDoctorOutput(result));
354
827
  }
355
- // Exit with error code if incomplete
356
- if (result.status === 'INCOMPLETE') {
357
- process.exit(1);
358
- }
828
+ // WU-1386: Use new exit code system
829
+ process.exit(result.exitCode);
359
830
  }
360
831
  // CLI invocation when run directly
361
832
  if (import.meta.main) {
362
833
  main().catch((error) => {
363
834
  console.error('Doctor failed:', error);
364
- process.exit(1);
835
+ process.exit(2);
365
836
  });
366
837
  }