@lumenflow/cli 2.2.1 → 2.3.1

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 (119) hide show
  1. package/README.md +147 -57
  2. package/dist/__tests__/agent-log-issue.test.js +56 -0
  3. package/dist/__tests__/cli-entry-point.test.js +66 -17
  4. package/dist/__tests__/cli-subprocess.test.js +25 -0
  5. package/dist/__tests__/init.test.js +298 -0
  6. package/dist/__tests__/initiative-plan.test.js +340 -0
  7. package/dist/__tests__/mem-cleanup-execution.test.js +19 -0
  8. package/dist/__tests__/merge-block.test.js +220 -0
  9. package/dist/__tests__/release.test.js +28 -0
  10. package/dist/__tests__/safe-git.test.js +191 -0
  11. package/dist/__tests__/state-doctor.test.js +274 -0
  12. package/dist/__tests__/wu-done.test.js +36 -0
  13. package/dist/__tests__/wu-edit.test.js +119 -0
  14. package/dist/__tests__/wu-prep.test.js +108 -0
  15. package/dist/agent-issues-query.js +4 -3
  16. package/dist/agent-log-issue.js +25 -4
  17. package/dist/backlog-prune.js +5 -4
  18. package/dist/cli-entry-point.js +11 -1
  19. package/dist/doctor.js +368 -0
  20. package/dist/flow-bottlenecks.js +6 -5
  21. package/dist/flow-report.js +4 -3
  22. package/dist/gates.js +468 -116
  23. package/dist/guard-locked.js +4 -3
  24. package/dist/guard-worktree-commit.js +4 -3
  25. package/dist/init.js +508 -86
  26. package/dist/initiative-add-wu.js +4 -3
  27. package/dist/initiative-bulk-assign-wus.js +8 -5
  28. package/dist/initiative-create.js +73 -37
  29. package/dist/initiative-edit.js +37 -21
  30. package/dist/initiative-list.js +4 -3
  31. package/dist/initiative-plan.js +337 -0
  32. package/dist/initiative-status.js +4 -3
  33. package/dist/lane-health.js +377 -0
  34. package/dist/lane-suggest.js +382 -0
  35. package/dist/mem-checkpoint.js +2 -2
  36. package/dist/mem-cleanup.js +2 -2
  37. package/dist/mem-context.js +306 -0
  38. package/dist/mem-create.js +2 -2
  39. package/dist/mem-delete.js +293 -0
  40. package/dist/mem-inbox.js +2 -2
  41. package/dist/mem-index.js +211 -0
  42. package/dist/mem-init.js +1 -1
  43. package/dist/mem-profile.js +207 -0
  44. package/dist/mem-promote.js +254 -0
  45. package/dist/mem-ready.js +2 -2
  46. package/dist/mem-signal.js +2 -2
  47. package/dist/mem-start.js +2 -2
  48. package/dist/mem-summarize.js +2 -2
  49. package/dist/mem-triage.js +2 -2
  50. package/dist/merge-block.js +222 -0
  51. package/dist/metrics-cli.js +7 -4
  52. package/dist/metrics-snapshot.js +4 -3
  53. package/dist/orchestrate-initiative.js +10 -4
  54. package/dist/orchestrate-monitor.js +379 -31
  55. package/dist/signal-cleanup.js +296 -0
  56. package/dist/spawn-list.js +6 -5
  57. package/dist/state-bootstrap.js +5 -4
  58. package/dist/state-cleanup.js +360 -0
  59. package/dist/state-doctor-fix.js +196 -0
  60. package/dist/state-doctor.js +501 -0
  61. package/dist/validate-agent-skills.js +4 -3
  62. package/dist/validate-agent-sync.js +4 -3
  63. package/dist/validate-backlog-sync.js +7 -84
  64. package/dist/validate-skills-spec.js +4 -3
  65. package/dist/validate.js +7 -107
  66. package/dist/wu-block.js +3 -3
  67. package/dist/wu-claim.js +208 -98
  68. package/dist/wu-cleanup.js +5 -4
  69. package/dist/wu-create.js +71 -46
  70. package/dist/wu-delete.js +88 -60
  71. package/dist/wu-deps.js +6 -5
  72. package/dist/wu-done-check.js +34 -0
  73. package/dist/wu-done.js +60 -24
  74. package/dist/wu-edit.js +63 -28
  75. package/dist/wu-infer-lane.js +7 -6
  76. package/dist/wu-preflight.js +23 -81
  77. package/dist/wu-prep.js +125 -0
  78. package/dist/wu-prune.js +4 -3
  79. package/dist/wu-recover.js +88 -22
  80. package/dist/wu-repair.js +7 -6
  81. package/dist/wu-spawn.js +226 -270
  82. package/dist/wu-status.js +4 -3
  83. package/dist/wu-unblock.js +5 -5
  84. package/dist/wu-unlock-lane.js +4 -3
  85. package/dist/wu-validate.js +5 -4
  86. package/package.json +16 -7
  87. package/templates/core/.lumenflow/constraints.md.template +192 -0
  88. package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
  89. package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
  90. package/templates/core/AGENTS.md.template +60 -0
  91. package/templates/core/LUMENFLOW.md.template +255 -0
  92. package/templates/core/UPGRADING.md.template +121 -0
  93. package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
  94. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
  95. package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
  96. package/templates/core/ai/onboarding/release-process.md.template +362 -0
  97. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
  98. package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
  99. package/templates/vendors/aider/.aider.conf.yml.template +27 -0
  100. package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
  101. package/templates/vendors/claude/.claude/settings.json.template +49 -0
  102. package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
  103. package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
  104. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
  105. package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
  106. package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
  107. package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
  108. package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
  109. package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
  110. package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
  111. package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
  112. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
  113. package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
  114. package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
  115. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
  116. package/templates/vendors/cline/.clinerules.template +53 -0
  117. package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
  118. package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
  119. package/templates/vendors/windsurf/.windsurf/rules/lumenflow.md.template +34 -0
@@ -0,0 +1,501 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console -- CLI tool requires console output */
3
+ /**
4
+ * State Doctor CLI (WU-1209)
5
+ *
6
+ * Integrity checker for LumenFlow state that detects:
7
+ * - Orphaned WUs (done status but no stamp)
8
+ * - Dangling signals (reference non-existent WUs)
9
+ * - Broken memory relationships (events for missing WU specs)
10
+ *
11
+ * Inspired by Beads bd doctor command.
12
+ *
13
+ * Features:
14
+ * - Human-readable output with actionable suggestions
15
+ * - --fix flag for safe auto-repair of resolvable issues
16
+ * - --dry-run to preview what would be fixed
17
+ * - --json for machine-readable output
18
+ *
19
+ * Usage:
20
+ * pnpm state:doctor # Run integrity checks
21
+ * pnpm state:doctor --fix # Auto-repair safe issues
22
+ * pnpm state:doctor --fix --dry-run # Preview repairs
23
+ * pnpm state:doctor --json # Output as JSON
24
+ *
25
+ * @see {@link packages/@lumenflow/core/src/state-doctor-core.ts} - Core logic
26
+ * @see {@link packages/@lumenflow/core/src/__tests__/state-doctor-core.test.ts} - Tests
27
+ */
28
+ import fs from 'node:fs/promises';
29
+ import path from 'node:path';
30
+ import fg from 'fast-glob';
31
+ import { parse as parseYaml } from 'yaml';
32
+ import { diagnoseState, ISSUE_TYPES, ISSUE_SEVERITY, } from '@lumenflow/core/dist/state-doctor-core.js';
33
+ import { createWUParser } from '@lumenflow/core/dist/arg-parser.js';
34
+ import { EXIT_CODES, LUMENFLOW_PATHS } from '@lumenflow/core/dist/wu-constants.js';
35
+ import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
36
+ import { createStamp } from '@lumenflow/core/dist/stamp-utils.js';
37
+ import { createStateDoctorFixDeps } from './state-doctor-fix.js';
38
+ /**
39
+ * Log prefix for state:doctor output
40
+ */
41
+ const LOG_PREFIX = '[state:doctor]';
42
+ /**
43
+ * Signals file path (relative to project root)
44
+ * Located in memory subdirectory: .lumenflow/memory/signals.jsonl
45
+ */
46
+ const SIGNALS_FILE = '.lumenflow/memory/signals.jsonl';
47
+ /**
48
+ * WU events file path (relative to project root)
49
+ * Located in state directory: .lumenflow/state/wu-events.jsonl
50
+ */
51
+ const WU_EVENTS_FILE = '.lumenflow/state/wu-events.jsonl';
52
+ /**
53
+ * Tool name for audit logging
54
+ */
55
+ const TOOL_NAME = 'state:doctor';
56
+ /**
57
+ * Emoji constants for output formatting
58
+ */
59
+ const EMOJI = {
60
+ SUCCESS: '\u2705',
61
+ FAILURE: '\u274C',
62
+ WARNING: '\u26A0\uFE0F',
63
+ FIX: '\u{1F527}',
64
+ INFO: '\u2139\uFE0F',
65
+ };
66
+ /**
67
+ * CLI argument options for state:doctor
68
+ */
69
+ const CLI_OPTIONS = {
70
+ fix: {
71
+ name: 'fix',
72
+ flags: '--fix',
73
+ description: 'Auto-repair safe issues',
74
+ },
75
+ dryRun: {
76
+ name: 'dryRun',
77
+ flags: '--dry-run',
78
+ description: 'Preview repairs without making changes',
79
+ },
80
+ json: {
81
+ name: 'json',
82
+ flags: '--json',
83
+ description: 'Output as JSON',
84
+ },
85
+ quiet: {
86
+ name: 'quiet',
87
+ flags: '-q, --quiet',
88
+ description: 'Suppress output except errors and summary',
89
+ },
90
+ baseDir: {
91
+ name: 'baseDir',
92
+ flags: '-b, --base-dir <path>',
93
+ description: 'Base directory (defaults to current directory)',
94
+ },
95
+ };
96
+ /**
97
+ * Write audit log entry for tool execution
98
+ */
99
+ async function writeAuditLog(baseDir, entry) {
100
+ try {
101
+ const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
102
+ const logDir = path.dirname(logPath);
103
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
104
+ await fs.mkdir(logDir, { recursive: true });
105
+ const line = `${JSON.stringify(entry)}\n`;
106
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes audit log
107
+ await fs.appendFile(logPath, line, 'utf-8');
108
+ }
109
+ catch {
110
+ // Audit logging is non-fatal - silently ignore errors
111
+ }
112
+ }
113
+ /**
114
+ * Parse CLI arguments
115
+ */
116
+ function parseArguments() {
117
+ return createWUParser({
118
+ name: 'state-doctor',
119
+ description: 'Check state integrity and optionally repair issues',
120
+ options: [
121
+ CLI_OPTIONS.fix,
122
+ CLI_OPTIONS.dryRun,
123
+ CLI_OPTIONS.json,
124
+ CLI_OPTIONS.quiet,
125
+ CLI_OPTIONS.baseDir,
126
+ ],
127
+ required: [],
128
+ allowPositionalId: false,
129
+ });
130
+ }
131
+ /**
132
+ * Create dependencies for state doctor from filesystem
133
+ */
134
+ async function createDeps(baseDir) {
135
+ const config = getConfig({ projectRoot: baseDir });
136
+ return {
137
+ /**
138
+ * List all WU YAML files
139
+ */
140
+ listWUs: async () => {
141
+ try {
142
+ const wuDir = path.join(baseDir, config.directories.wuDir);
143
+ const wuFiles = await fg('WU-*.yaml', { cwd: wuDir });
144
+ const wus = [];
145
+ for (const file of wuFiles) {
146
+ try {
147
+ const filePath = path.join(wuDir, file);
148
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
149
+ const content = await fs.readFile(filePath, 'utf-8');
150
+ const wu = parseYaml(content);
151
+ if (wu.id && wu.status) {
152
+ wus.push({
153
+ id: wu.id,
154
+ status: wu.status,
155
+ lane: wu.lane,
156
+ title: wu.title,
157
+ });
158
+ }
159
+ }
160
+ catch {
161
+ // Skip files that fail to parse
162
+ continue;
163
+ }
164
+ }
165
+ return wus;
166
+ }
167
+ catch {
168
+ return [];
169
+ }
170
+ },
171
+ /**
172
+ * List all stamp file IDs
173
+ */
174
+ listStamps: async () => {
175
+ try {
176
+ const stampsDir = path.join(baseDir, LUMENFLOW_PATHS.STAMPS_DIR);
177
+ const stampFiles = await fg('WU-*.done', { cwd: stampsDir });
178
+ return stampFiles.map((file) => file.replace('.done', ''));
179
+ }
180
+ catch {
181
+ return [];
182
+ }
183
+ },
184
+ /**
185
+ * List all signals from NDJSON file
186
+ */
187
+ listSignals: async () => {
188
+ try {
189
+ const signalsPath = path.join(baseDir, SIGNALS_FILE);
190
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
191
+ const content = await fs.readFile(signalsPath, 'utf-8');
192
+ const signals = [];
193
+ for (const line of content.split('\n')) {
194
+ if (!line.trim())
195
+ continue;
196
+ try {
197
+ const signal = JSON.parse(line);
198
+ if (signal.id) {
199
+ signals.push({
200
+ id: signal.id,
201
+ wuId: signal.wuId,
202
+ timestamp: signal.timestamp,
203
+ message: signal.message,
204
+ });
205
+ }
206
+ }
207
+ catch {
208
+ // Skip malformed lines
209
+ continue;
210
+ }
211
+ }
212
+ return signals;
213
+ }
214
+ catch {
215
+ return [];
216
+ }
217
+ },
218
+ /**
219
+ * List all events from NDJSON file
220
+ */
221
+ listEvents: async () => {
222
+ try {
223
+ const eventsPath = path.join(baseDir, WU_EVENTS_FILE);
224
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
225
+ const content = await fs.readFile(eventsPath, 'utf-8');
226
+ const events = [];
227
+ for (const line of content.split('\n')) {
228
+ if (!line.trim())
229
+ continue;
230
+ try {
231
+ const event = JSON.parse(line);
232
+ if (event.wuId && event.type) {
233
+ events.push({
234
+ wuId: event.wuId,
235
+ type: event.type,
236
+ timestamp: event.timestamp,
237
+ });
238
+ }
239
+ }
240
+ catch {
241
+ // Skip malformed lines
242
+ continue;
243
+ }
244
+ }
245
+ return events;
246
+ }
247
+ catch {
248
+ return [];
249
+ }
250
+ },
251
+ /**
252
+ * Remove a signal by ID (rewrite signals file without the target)
253
+ */
254
+ removeSignal: async (id) => {
255
+ const signalsPath = path.join(baseDir, SIGNALS_FILE);
256
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
257
+ const content = await fs.readFile(signalsPath, 'utf-8');
258
+ const lines = content.split('\n').filter((line) => {
259
+ if (!line.trim())
260
+ return false;
261
+ try {
262
+ const signal = JSON.parse(line);
263
+ return signal.id !== id;
264
+ }
265
+ catch {
266
+ return true; // Keep malformed lines
267
+ }
268
+ });
269
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes known path
270
+ await fs.writeFile(signalsPath, lines.join('\n') + '\n', 'utf-8');
271
+ },
272
+ /**
273
+ * Remove events for a WU (rewrite events file without the target WU)
274
+ */
275
+ removeEvent: async (wuId) => {
276
+ const eventsPath = path.join(baseDir, WU_EVENTS_FILE);
277
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
278
+ const content = await fs.readFile(eventsPath, 'utf-8');
279
+ const lines = content.split('\n').filter((line) => {
280
+ if (!line.trim())
281
+ return false;
282
+ try {
283
+ const event = JSON.parse(line);
284
+ return event.wuId !== wuId;
285
+ }
286
+ catch {
287
+ return true; // Keep malformed lines
288
+ }
289
+ });
290
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes known path
291
+ await fs.writeFile(eventsPath, lines.join('\n') + '\n', 'utf-8');
292
+ },
293
+ /**
294
+ * Create a stamp for a WU
295
+ */
296
+ createStamp: async (wuId, title) => {
297
+ const stampsDir = path.join(baseDir, LUMENFLOW_PATHS.STAMPS_DIR);
298
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
299
+ await fs.mkdir(stampsDir, { recursive: true });
300
+ await createStamp({
301
+ id: wuId,
302
+ title,
303
+ });
304
+ },
305
+ };
306
+ }
307
+ /**
308
+ * Get emoji for issue severity
309
+ */
310
+ function getSeverityEmoji(severity) {
311
+ switch (severity) {
312
+ case ISSUE_SEVERITY.ERROR:
313
+ return EMOJI.FAILURE;
314
+ case ISSUE_SEVERITY.WARNING:
315
+ return EMOJI.WARNING;
316
+ case ISSUE_SEVERITY.INFO:
317
+ return EMOJI.INFO;
318
+ default:
319
+ return EMOJI.WARNING;
320
+ }
321
+ }
322
+ /**
323
+ * Get human-readable issue type label
324
+ */
325
+ function getIssueTypeLabel(type) {
326
+ switch (type) {
327
+ case ISSUE_TYPES.ORPHANED_WU:
328
+ return 'Orphaned WU';
329
+ case ISSUE_TYPES.DANGLING_SIGNAL:
330
+ return 'Dangling Signal';
331
+ case ISSUE_TYPES.BROKEN_EVENT:
332
+ return 'Broken Event';
333
+ default:
334
+ return type;
335
+ }
336
+ }
337
+ /**
338
+ * Print summary section
339
+ */
340
+ function printSummary(result) {
341
+ console.log('=== Summary ===');
342
+ console.log(` Orphaned WUs: ${result.summary.orphanedWUs}`);
343
+ console.log(` Dangling Signals: ${result.summary.danglingSignals}`);
344
+ console.log(` Broken Events: ${result.summary.brokenEvents}`);
345
+ console.log(` Total Issues: ${result.summary.totalIssues}`);
346
+ }
347
+ /**
348
+ * Print fixed issues section
349
+ */
350
+ function printFixedSection(result) {
351
+ if (result.fixed.length > 0) {
352
+ console.log('\n=== Fixed ===');
353
+ for (const issue of result.fixed) {
354
+ const id = issue.wuId || issue.signalId;
355
+ console.log(` ${EMOJI.SUCCESS} Fixed: ${getIssueTypeLabel(issue.type)} - ${id}`);
356
+ }
357
+ }
358
+ if (result.fixErrors.length > 0) {
359
+ console.log('\n=== Fix Errors ===');
360
+ for (const err of result.fixErrors) {
361
+ const id = err.wuId || err.signalId;
362
+ console.log(` ${EMOJI.FAILURE} Failed to fix: ${id} - ${err.error}`);
363
+ }
364
+ }
365
+ }
366
+ /**
367
+ * Print dry-run section
368
+ */
369
+ function printDryRunSection(result) {
370
+ if (!result.dryRun || !result.wouldFix || result.wouldFix.length === 0) {
371
+ return;
372
+ }
373
+ console.log('\n=== Would Fix (dry-run) ===');
374
+ for (const issue of result.wouldFix) {
375
+ const id = issue.wuId || issue.signalId;
376
+ console.log(` ${EMOJI.FIX} Would fix: ${getIssueTypeLabel(issue.type)} - ${id}`);
377
+ }
378
+ console.log('\n To apply fixes, run without --dry-run');
379
+ }
380
+ /**
381
+ * Print issues list
382
+ */
383
+ function printIssues(result) {
384
+ console.log(`${LOG_PREFIX} ${EMOJI.WARNING} Found ${result.summary.totalIssues} issue(s)\n`);
385
+ console.log('=== Issues ===');
386
+ for (const issue of result.issues) {
387
+ const emoji = getSeverityEmoji(issue.severity);
388
+ const label = getIssueTypeLabel(issue.type);
389
+ console.log(` ${emoji} [${label}] ${issue.description}`);
390
+ console.log(` ${EMOJI.FIX} ${issue.suggestion}`);
391
+ if (issue.canAutoFix) {
392
+ console.log(` ${EMOJI.INFO} Can be auto-fixed with --fix`);
393
+ }
394
+ console.log('');
395
+ }
396
+ }
397
+ /**
398
+ * Print diagnosis result to console
399
+ */
400
+ function printResult(result, quiet) {
401
+ if (result.healthy) {
402
+ console.log(`${LOG_PREFIX} ${EMOJI.SUCCESS} State is healthy - no issues found`);
403
+ return;
404
+ }
405
+ if (!quiet) {
406
+ printIssues(result);
407
+ }
408
+ printSummary(result);
409
+ printFixedSection(result);
410
+ printDryRunSection(result);
411
+ if (result.issues.length > 0 && result.fixed.length === 0 && !result.dryRun) {
412
+ console.log(`\n ${EMOJI.INFO} Run with --fix to auto-repair fixable issues`);
413
+ }
414
+ }
415
+ /**
416
+ * Determine audit log status from result
417
+ */
418
+ function getAuditStatus(error, result) {
419
+ if (error)
420
+ return 'failed';
421
+ if (result?.healthy)
422
+ return 'success';
423
+ if (result?.fixErrors.length === 0 && result?.fixed.length > 0)
424
+ return 'success';
425
+ return 'partial';
426
+ }
427
+ /**
428
+ * Build audit log output entry
429
+ */
430
+ function buildAuditOutput(result) {
431
+ if (!result)
432
+ return null;
433
+ return {
434
+ healthy: result.healthy,
435
+ totalIssues: result.summary.totalIssues,
436
+ fixedCount: result.fixed.length,
437
+ fixErrorCount: result.fixErrors.length,
438
+ dryRun: result.dryRun,
439
+ };
440
+ }
441
+ /**
442
+ * Main CLI entry point
443
+ */
444
+ async function main() {
445
+ const args = parseArguments();
446
+ const baseDir = args.baseDir || process.cwd();
447
+ const startedAt = new Date().toISOString();
448
+ const startTime = Date.now();
449
+ let result = null;
450
+ let error = null;
451
+ try {
452
+ // Create base deps for read operations
453
+ const baseDeps = await createDeps(baseDir);
454
+ // WU-1230: When --fix is enabled, use micro-worktree isolation for all
455
+ // tracked file modifications. This prevents direct modifications to main
456
+ // and ensures changes are pushed via merge.
457
+ const deps = args.fix && !args.dryRun
458
+ ? {
459
+ ...baseDeps,
460
+ ...createStateDoctorFixDeps(baseDir),
461
+ }
462
+ : baseDeps;
463
+ result = await diagnoseState(baseDir, deps, {
464
+ fix: args.fix,
465
+ dryRun: args.dryRun,
466
+ });
467
+ }
468
+ catch (err) {
469
+ error = err.message;
470
+ }
471
+ const durationMs = Date.now() - startTime;
472
+ const auditStatus = getAuditStatus(error, result);
473
+ await writeAuditLog(baseDir, {
474
+ tool: TOOL_NAME,
475
+ status: auditStatus,
476
+ startedAt,
477
+ completedAt: new Date().toISOString(),
478
+ durationMs,
479
+ input: { baseDir, fix: args.fix, dryRun: args.dryRun },
480
+ output: buildAuditOutput(result),
481
+ error: error ? { message: error } : null,
482
+ });
483
+ if (error) {
484
+ console.error(`${LOG_PREFIX} Error: ${error}`);
485
+ process.exit(EXIT_CODES.ERROR);
486
+ }
487
+ if (args.json) {
488
+ console.log(JSON.stringify(result, null, 2));
489
+ process.exit(result?.healthy ? EXIT_CODES.SUCCESS : EXIT_CODES.ERROR);
490
+ }
491
+ if (result) {
492
+ printResult(result, args.quiet ?? false);
493
+ const shouldError = !result.healthy && result.fixed.length === 0;
494
+ if (shouldError)
495
+ process.exit(EXIT_CODES.ERROR);
496
+ }
497
+ }
498
+ main().catch((e) => {
499
+ console.error(`${LOG_PREFIX} ${e.message}`);
500
+ process.exit(EXIT_CODES.ERROR);
501
+ });
@@ -18,7 +18,6 @@
18
18
  */
19
19
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
20
20
  import path from 'node:path';
21
- import { fileURLToPath } from 'node:url';
22
21
  import { FILE_SYSTEM, EMOJI } from '@lumenflow/core/dist/wu-constants.js';
23
22
  const LOG_PREFIX = '[validate-agent-skills]';
24
23
  /**
@@ -209,8 +208,10 @@ Examples:
209
208
  }
210
209
  }
211
210
  }
212
- // Guard main() for testability
213
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
211
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
212
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
213
+ // path but import.meta.url resolves to the real path - they never match
214
+ if (import.meta.main) {
214
215
  main().catch((error) => {
215
216
  console.error(`${LOG_PREFIX} Unexpected error:`, error);
216
217
  process.exit(1);
@@ -17,7 +17,6 @@
17
17
  */
18
18
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
19
19
  import path from 'node:path';
20
- import { fileURLToPath } from 'node:url';
21
20
  import { FILE_SYSTEM, EMOJI } from '@lumenflow/core/dist/wu-constants.js';
22
21
  const LOG_PREFIX = '[validate-agent-sync]';
23
22
  /**
@@ -139,8 +138,10 @@ Examples:
139
138
  process.exit(1);
140
139
  }
141
140
  }
142
- // Guard main() for testability
143
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
141
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
142
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
143
+ // path but import.meta.url resolves to the real path - they never match
144
+ if (import.meta.main) {
144
145
  main().catch((error) => {
145
146
  console.error(`${LOG_PREFIX} Unexpected error:`, error);
146
147
  process.exit(1);
@@ -15,89 +15,10 @@
15
15
  *
16
16
  * @see {@link docs/04-operations/tasks/backlog.md} - Backlog file
17
17
  */
18
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
19
- import path from 'node:path';
20
- import { fileURLToPath } from 'node:url';
21
- import { FILE_SYSTEM, EMOJI } from '@lumenflow/core/dist/wu-constants.js';
18
+ import { validateBacklogSync, } from '@lumenflow/core/dist/validators/backlog-sync.js';
19
+ import { EMOJI } from '@lumenflow/core/dist/wu-constants.js';
22
20
  const LOG_PREFIX = '[validate-backlog-sync]';
23
- /**
24
- * Extract WU IDs from backlog.md content
25
- *
26
- * @param content - backlog.md content
27
- * @returns Array of WU IDs found
28
- */
29
- function extractWUIDsFromBacklog(content) {
30
- const wuIds = [];
31
- const pattern = /WU-\d+/gi;
32
- let match;
33
- while ((match = pattern.exec(content)) !== null) {
34
- const wuId = match[0].toUpperCase();
35
- if (!wuIds.includes(wuId)) {
36
- wuIds.push(wuId);
37
- }
38
- }
39
- return wuIds;
40
- }
41
- /**
42
- * Get all WU IDs from YAML files
43
- *
44
- * @param wuDir - Path to WU directory
45
- * @returns Array of WU IDs
46
- */
47
- function getWUIDsFromFiles(wuDir) {
48
- if (!existsSync(wuDir)) {
49
- return [];
50
- }
51
- return readdirSync(wuDir)
52
- .filter((f) => f.endsWith('.yaml'))
53
- .map((f) => f.replace('.yaml', '').toUpperCase());
54
- }
55
- /**
56
- * Validate that backlog.md is in sync with WU YAML files
57
- *
58
- * @param options - Validation options
59
- * @param options.cwd - Working directory (default: process.cwd())
60
- * @returns Validation result
61
- */
62
- export async function validateBacklogSync(options = {}) {
63
- const { cwd = process.cwd() } = options;
64
- const errors = [];
65
- const warnings = [];
66
- // Paths
67
- const backlogPath = path.join(cwd, 'docs', '04-operations', 'tasks', 'backlog.md');
68
- const wuDir = path.join(cwd, 'docs', '04-operations', 'tasks', 'wu');
69
- // Check backlog.md exists
70
- if (!existsSync(backlogPath)) {
71
- errors.push(`Backlog file not found: ${backlogPath}`);
72
- return { valid: false, errors, warnings, wuCount: 0, backlogCount: 0 };
73
- }
74
- // Get WU IDs from files
75
- const wuIdsFromFiles = getWUIDsFromFiles(wuDir);
76
- // Get WU IDs from backlog
77
- const backlogContent = readFileSync(backlogPath, {
78
- encoding: FILE_SYSTEM.UTF8,
79
- });
80
- const wuIdsFromBacklog = extractWUIDsFromBacklog(backlogContent);
81
- // Check for WUs in files but not in backlog
82
- for (const wuId of wuIdsFromFiles) {
83
- if (!wuIdsFromBacklog.includes(wuId)) {
84
- errors.push(`${wuId} not found in backlog.md (exists as ${wuId}.yaml)`);
85
- }
86
- }
87
- // Check for WUs in backlog but not as files (warning only)
88
- for (const wuId of wuIdsFromBacklog) {
89
- if (!wuIdsFromFiles.includes(wuId)) {
90
- warnings.push(`${wuId} referenced in backlog.md but ${wuId}.yaml not found`);
91
- }
92
- }
93
- return {
94
- valid: errors.length === 0,
95
- errors,
96
- warnings,
97
- wuCount: wuIdsFromFiles.length,
98
- backlogCount: wuIdsFromBacklog.length,
99
- };
100
- }
21
+ export { validateBacklogSync };
101
22
  /**
102
23
  * Main CLI entry point
103
24
  */
@@ -143,8 +64,10 @@ Examples:
143
64
  process.exit(1);
144
65
  }
145
66
  }
146
- // Guard main() for testability
147
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
67
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
68
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
69
+ // path but import.meta.url resolves to the real path - they never match
70
+ if (import.meta.main) {
148
71
  main().catch((error) => {
149
72
  console.error(`${LOG_PREFIX} Unexpected error:`, error);
150
73
  process.exit(1);
@@ -18,7 +18,6 @@
18
18
  */
19
19
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
20
20
  import path from 'node:path';
21
- import { fileURLToPath } from 'node:url';
22
21
  import { FILE_SYSTEM, EMOJI } from '@lumenflow/core/dist/wu-constants.js';
23
22
  const LOG_PREFIX = '[validate-skills-spec]';
24
23
  /**
@@ -197,8 +196,10 @@ Examples:
197
196
  }
198
197
  }
199
198
  }
200
- // Guard main() for testability
201
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
199
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
200
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
201
+ // path but import.meta.url resolves to the real path - they never match
202
+ if (import.meta.main) {
202
203
  main().catch((error) => {
203
204
  console.error(`${LOG_PREFIX} Unexpected error:`, error);
204
205
  process.exit(1);