@lumenflow/cli 2.7.0 → 2.9.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 (84) hide show
  1. package/README.md +121 -105
  2. package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
  3. package/dist/__tests__/commands/integrate.test.js +165 -0
  4. package/dist/__tests__/commands.test.js +75 -0
  5. package/dist/__tests__/doctor.test.js +510 -0
  6. package/dist/__tests__/gates-config.test.js +0 -1
  7. package/dist/__tests__/hooks/enforcement.test.js +279 -0
  8. package/dist/__tests__/init-greenfield.test.js +247 -0
  9. package/dist/__tests__/init-quick-ref.test.js +0 -1
  10. package/dist/__tests__/init-template-portability.test.js +0 -1
  11. package/dist/__tests__/init.test.js +249 -0
  12. package/dist/__tests__/initiative-e2e.test.js +442 -0
  13. package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
  14. package/dist/__tests__/memory-integration.test.js +333 -0
  15. package/dist/__tests__/release.test.js +1 -1
  16. package/dist/__tests__/safe-git.test.js +0 -1
  17. package/dist/__tests__/state-doctor.test.js +54 -0
  18. package/dist/__tests__/sync-templates.test.js +255 -0
  19. package/dist/__tests__/wu-create-required-fields.test.js +121 -0
  20. package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
  21. package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
  22. package/dist/backlog-prune.js +0 -1
  23. package/dist/cli-entry-point.js +0 -1
  24. package/dist/commands/integrate.js +229 -0
  25. package/dist/commands.js +171 -0
  26. package/dist/docs-sync.js +46 -0
  27. package/dist/doctor.js +479 -10
  28. package/dist/gates.js +0 -7
  29. package/dist/hooks/enforcement-checks.js +209 -0
  30. package/dist/hooks/enforcement-generator.js +365 -0
  31. package/dist/hooks/enforcement-sync.js +243 -0
  32. package/dist/hooks/index.js +7 -0
  33. package/dist/init.js +502 -17
  34. package/dist/initiative-add-wu.js +0 -2
  35. package/dist/initiative-create.js +0 -3
  36. package/dist/initiative-edit.js +0 -5
  37. package/dist/initiative-plan.js +0 -1
  38. package/dist/initiative-remove-wu.js +0 -2
  39. package/dist/lane-health.js +0 -2
  40. package/dist/lane-suggest.js +0 -1
  41. package/dist/mem-checkpoint.js +0 -2
  42. package/dist/mem-cleanup.js +0 -2
  43. package/dist/mem-context.js +0 -3
  44. package/dist/mem-create.js +0 -2
  45. package/dist/mem-delete.js +0 -3
  46. package/dist/mem-inbox.js +0 -2
  47. package/dist/mem-index.js +0 -1
  48. package/dist/mem-init.js +0 -2
  49. package/dist/mem-profile.js +0 -1
  50. package/dist/mem-promote.js +0 -1
  51. package/dist/mem-ready.js +0 -2
  52. package/dist/mem-signal.js +0 -2
  53. package/dist/mem-start.js +0 -2
  54. package/dist/mem-summarize.js +0 -2
  55. package/dist/metrics-cli.js +1 -1
  56. package/dist/metrics-snapshot.js +1 -1
  57. package/dist/onboarding-smoke-test.js +0 -5
  58. package/dist/orchestrate-init-status.js +0 -1
  59. package/dist/orchestrate-initiative.js +0 -1
  60. package/dist/orchestrate-monitor.js +0 -1
  61. package/dist/plan-create.js +0 -2
  62. package/dist/plan-edit.js +0 -2
  63. package/dist/plan-link.js +0 -2
  64. package/dist/plan-promote.js +0 -2
  65. package/dist/signal-cleanup.js +0 -4
  66. package/dist/state-bootstrap.js +0 -1
  67. package/dist/state-cleanup.js +0 -4
  68. package/dist/state-doctor-fix.js +5 -8
  69. package/dist/state-doctor.js +0 -11
  70. package/dist/sync-templates.js +188 -34
  71. package/dist/wu-block.js +100 -48
  72. package/dist/wu-claim.js +1 -22
  73. package/dist/wu-cleanup.js +0 -1
  74. package/dist/wu-create.js +0 -2
  75. package/dist/wu-done-auto-cleanup.js +139 -0
  76. package/dist/wu-done.js +11 -4
  77. package/dist/wu-edit.js +0 -12
  78. package/dist/wu-preflight.js +0 -1
  79. package/dist/wu-prep.js +0 -1
  80. package/dist/wu-proto.js +0 -1
  81. package/dist/wu-spawn.js +0 -3
  82. package/dist/wu-unblock.js +0 -2
  83. package/dist/wu-validate.js +0 -1
  84. package/package.json +9 -7
package/dist/doctor.js CHANGED
@@ -1,8 +1,8 @@
1
- /* eslint-disable no-console -- CLI command uses console for status output */
2
1
  /**
3
2
  * @file doctor.ts
4
3
  * LumenFlow health check command (WU-1177)
5
4
  * WU-1191: Lane health check integration
5
+ * WU-1386: Agent-friction checks (managed-file dirty, WU validity, worktree sanity)
6
6
  * Verifies all safety components are installed and configured correctly
7
7
  */
8
8
  import * as fs from 'node:fs';
@@ -10,8 +10,20 @@ import * as path from 'node:path';
10
10
  import { execFileSync } from 'node:child_process';
11
11
  import { createWUParser } from '@lumenflow/core';
12
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/'];
13
24
  /**
14
25
  * CLI option definitions for doctor command
26
+ * WU-1386: Added --deep flag
15
27
  */
16
28
  const DOCTOR_OPTIONS = {
17
29
  verbose: {
@@ -24,9 +36,15 @@ const DOCTOR_OPTIONS = {
24
36
  flags: '--json',
25
37
  description: 'Output results as JSON',
26
38
  },
39
+ deep: {
40
+ name: 'deep',
41
+ flags: '--deep',
42
+ description: 'Run heavier checks including WU validation (slower)',
43
+ },
27
44
  };
28
45
  /**
29
46
  * Parse doctor command options
47
+ * WU-1386: Added deep flag
30
48
  */
31
49
  export function parseDoctorOptions() {
32
50
  const opts = createWUParser({
@@ -37,6 +55,7 @@ export function parseDoctorOptions() {
37
55
  return {
38
56
  verbose: opts.verbose ?? false,
39
57
  json: opts.json ?? false,
58
+ deep: opts.deep ?? false,
40
59
  };
41
60
  }
42
61
  /**
@@ -167,7 +186,6 @@ function getCommandVersion(command, args) {
167
186
  * Parse semver version string to compare
168
187
  */
169
188
  function parseVersion(versionStr) {
170
- // eslint-disable-next-line sonarjs/slow-regex, sonarjs/prefer-regexp-exec -- Simple semver extraction, no backtracking risk
171
189
  const match = versionStr.match(/(\d+)\.(\d+)\.?(\d+)?/);
172
190
  if (!match) {
173
191
  return [0, 0, 0];
@@ -254,10 +272,314 @@ function checkPrerequisites() {
254
272
  },
255
273
  };
256
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
+ }
257
578
  /**
258
579
  * Run all doctor checks
580
+ * WU-1386: Added options parameter for --deep flag
259
581
  */
260
- export async function runDoctor(projectDir) {
582
+ export async function runDoctor(projectDir, options = {}) {
261
583
  const checks = {
262
584
  husky: checkHusky(projectDir),
263
585
  safeGit: checkSafeGit(projectDir),
@@ -268,19 +590,137 @@ export async function runDoctor(projectDir) {
268
590
  };
269
591
  const vendorConfigs = checkVendorConfigs(projectDir);
270
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
+ }
271
604
  // Determine overall status
272
605
  // Note: laneHealth is advisory (not included in critical checks)
273
606
  const criticalChecks = [checks.husky, checks.safeGit, checks.agentsMd];
274
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
+ }
275
621
  return {
276
622
  status: allCriticalPassed ? 'ACTIVE' : 'INCOMPLETE',
623
+ exitCode,
277
624
  checks,
278
625
  vendorConfigs,
279
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'),
280
719
  };
281
720
  }
282
721
  /**
283
722
  * Format doctor output for terminal display
723
+ * WU-1386: Added workflow health section
284
724
  */
285
725
  export function formatDoctorOutput(result) {
286
726
  const lines = [];
@@ -302,6 +742,31 @@ export function formatDoctorOutput(result) {
302
742
  lines.push('');
303
743
  lines.push('Lane Health:');
304
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
+ }
305
770
  // Vendor configs
306
771
  lines.push('');
307
772
  lines.push('Vendor Configs:');
@@ -326,7 +791,12 @@ export function formatDoctorOutput(result) {
326
791
  lines.push('');
327
792
  lines.push('─'.repeat(45));
328
793
  lines.push(`LumenFlow safety: ${result.status}`);
329
- 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) {
330
800
  lines.push('');
331
801
  lines.push('To fix missing components:');
332
802
  lines.push(' pnpm install && pnpm prepare # Install Husky hooks');
@@ -343,26 +813,25 @@ export function formatDoctorJson(result) {
343
813
  }
344
814
  /**
345
815
  * CLI entry point
816
+ * WU-1386: Updated to use new exit codes
346
817
  */
347
818
  export async function main() {
348
819
  const opts = parseDoctorOptions();
349
820
  const projectDir = process.cwd();
350
- const result = await runDoctor(projectDir);
821
+ const result = await runDoctor(projectDir, { deep: opts.deep });
351
822
  if (opts.json) {
352
823
  console.log(formatDoctorJson(result));
353
824
  }
354
825
  else {
355
826
  console.log(formatDoctorOutput(result));
356
827
  }
357
- // Exit with error code if incomplete
358
- if (result.status === 'INCOMPLETE') {
359
- process.exit(1);
360
- }
828
+ // WU-1386: Use new exit code system
829
+ process.exit(result.exitCode);
361
830
  }
362
831
  // CLI invocation when run directly
363
832
  if (import.meta.main) {
364
833
  main().catch((error) => {
365
834
  console.error('Doctor failed:', error);
366
- process.exit(1);
835
+ process.exit(2);
367
836
  });
368
837
  }
package/dist/gates.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- /* eslint-disable no-console -- Gates runner uses console for status output; refactoring to logger is tracked for future work */
3
2
  /**
4
3
  * Quality Gates Runner
5
4
  *
@@ -469,7 +468,6 @@ export function formatFormatCheckGuidance(files) {
469
468
  function collectPrettierListDifferent(cwd, files = []) {
470
469
  const filesArg = files.length > 0 ? quoteShellArgs(files) : '.';
471
470
  const cmd = pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_ARGS.LIST_DIFFERENT, filesArg);
472
- // eslint-disable-next-line sonarjs/os-command -- Pre-existing: executes trusted pnpm prettier command
473
471
  const result = spawnSync(cmd, [], {
474
472
  shell: true,
475
473
  cwd,
@@ -534,7 +532,6 @@ function run(cmd, { agentLog } = {}) {
534
532
  if (!agentLog) {
535
533
  console.log(`\n> ${cmd}\n`);
536
534
  try {
537
- // eslint-disable-next-line sonarjs/os-command -- Pre-existing: cmd is built from trusted constants
538
535
  execSync(cmd, { stdio: 'inherit', encoding: FILE_SYSTEM.ENCODING });
539
536
  return { ok: true, duration: Date.now() - start };
540
537
  }
@@ -543,7 +540,6 @@ function run(cmd, { agentLog } = {}) {
543
540
  }
544
541
  }
545
542
  writeSync(agentLog.logFd, `\n> ${cmd}\n\n`);
546
- // eslint-disable-next-line sonarjs/os-command -- Pre-existing: cmd is built from trusted constants
547
543
  const result = spawnSync(cmd, [], {
548
544
  shell: true,
549
545
  stdio: ['ignore', agentLog.logFd, agentLog.logFd],
@@ -704,13 +700,11 @@ async function runFormatCheckGate({ agentLog, useAgentMode }) {
704
700
  return { ok: true, duration: Date.now() - start, fileCount: 0, filesChecked: [] };
705
701
  }
706
702
  if (plan.mode === 'full') {
707
- /* eslint-disable sonarjs/no-nested-conditional -- Pre-existing: simple reason mapping, readable as-is */
708
703
  const reason = plan.reason === 'prettier-config'
709
704
  ? ' (prettier config changed)'
710
705
  : plan.reason === 'file-list-error'
711
706
  ? ' (file list unavailable)'
712
707
  : '';
713
- /* eslint-enable sonarjs/no-nested-conditional */
714
708
  logLine(`📋 Running full format check${reason}`);
715
709
  const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog });
716
710
  return { ...result, duration: Date.now() - start, fileCount: -1 };
@@ -1408,7 +1402,6 @@ async function executeGates(opts) {
1408
1402
  // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
1409
1403
  // path but import.meta.url resolves to the real path - they never match
1410
1404
  if (import.meta.main) {
1411
- // eslint-disable-next-line sonarjs/deprecation -- Pre-existing: parseGatesArgs kept for backwards compatibility
1412
1405
  const opts = parseGatesArgs();
1413
1406
  executeGates({ ...opts, argv: process.argv.slice(2) })
1414
1407
  .then((ok) => {