@lumenflow/cli 2.2.2 → 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 (118) 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__/safe-git.test.js +191 -0
  10. package/dist/__tests__/state-doctor.test.js +274 -0
  11. package/dist/__tests__/wu-done.test.js +36 -0
  12. package/dist/__tests__/wu-edit.test.js +119 -0
  13. package/dist/__tests__/wu-prep.test.js +108 -0
  14. package/dist/agent-issues-query.js +4 -3
  15. package/dist/agent-log-issue.js +25 -4
  16. package/dist/backlog-prune.js +5 -4
  17. package/dist/cli-entry-point.js +11 -1
  18. package/dist/doctor.js +368 -0
  19. package/dist/flow-bottlenecks.js +6 -5
  20. package/dist/flow-report.js +4 -3
  21. package/dist/gates.js +356 -101
  22. package/dist/guard-locked.js +4 -3
  23. package/dist/guard-worktree-commit.js +4 -3
  24. package/dist/init.js +508 -86
  25. package/dist/initiative-add-wu.js +4 -3
  26. package/dist/initiative-bulk-assign-wus.js +8 -5
  27. package/dist/initiative-create.js +73 -37
  28. package/dist/initiative-edit.js +37 -21
  29. package/dist/initiative-list.js +4 -3
  30. package/dist/initiative-plan.js +337 -0
  31. package/dist/initiative-status.js +4 -3
  32. package/dist/lane-health.js +377 -0
  33. package/dist/lane-suggest.js +382 -0
  34. package/dist/mem-checkpoint.js +2 -2
  35. package/dist/mem-cleanup.js +2 -2
  36. package/dist/mem-context.js +306 -0
  37. package/dist/mem-create.js +2 -2
  38. package/dist/mem-delete.js +293 -0
  39. package/dist/mem-inbox.js +2 -2
  40. package/dist/mem-index.js +211 -0
  41. package/dist/mem-init.js +1 -1
  42. package/dist/mem-profile.js +207 -0
  43. package/dist/mem-promote.js +254 -0
  44. package/dist/mem-ready.js +2 -2
  45. package/dist/mem-signal.js +2 -2
  46. package/dist/mem-start.js +2 -2
  47. package/dist/mem-summarize.js +2 -2
  48. package/dist/mem-triage.js +2 -2
  49. package/dist/merge-block.js +222 -0
  50. package/dist/metrics-cli.js +7 -4
  51. package/dist/metrics-snapshot.js +4 -3
  52. package/dist/orchestrate-initiative.js +10 -4
  53. package/dist/orchestrate-monitor.js +379 -31
  54. package/dist/signal-cleanup.js +296 -0
  55. package/dist/spawn-list.js +6 -5
  56. package/dist/state-bootstrap.js +5 -4
  57. package/dist/state-cleanup.js +360 -0
  58. package/dist/state-doctor-fix.js +196 -0
  59. package/dist/state-doctor.js +501 -0
  60. package/dist/validate-agent-skills.js +4 -3
  61. package/dist/validate-agent-sync.js +4 -3
  62. package/dist/validate-backlog-sync.js +4 -3
  63. package/dist/validate-skills-spec.js +4 -3
  64. package/dist/validate.js +4 -3
  65. package/dist/wu-block.js +3 -3
  66. package/dist/wu-claim.js +208 -98
  67. package/dist/wu-cleanup.js +5 -4
  68. package/dist/wu-create.js +71 -46
  69. package/dist/wu-delete.js +88 -60
  70. package/dist/wu-deps.js +6 -5
  71. package/dist/wu-done-check.js +34 -0
  72. package/dist/wu-done.js +39 -12
  73. package/dist/wu-edit.js +63 -28
  74. package/dist/wu-infer-lane.js +7 -6
  75. package/dist/wu-preflight.js +23 -81
  76. package/dist/wu-prep.js +125 -0
  77. package/dist/wu-prune.js +4 -3
  78. package/dist/wu-recover.js +88 -22
  79. package/dist/wu-repair.js +7 -6
  80. package/dist/wu-spawn.js +226 -270
  81. package/dist/wu-status.js +4 -3
  82. package/dist/wu-unblock.js +5 -5
  83. package/dist/wu-unlock-lane.js +4 -3
  84. package/dist/wu-validate.js +5 -4
  85. package/package.json +16 -7
  86. package/templates/core/.lumenflow/constraints.md.template +192 -0
  87. package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
  88. package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
  89. package/templates/core/AGENTS.md.template +60 -0
  90. package/templates/core/LUMENFLOW.md.template +255 -0
  91. package/templates/core/UPGRADING.md.template +121 -0
  92. package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
  93. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
  94. package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
  95. package/templates/core/ai/onboarding/release-process.md.template +362 -0
  96. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
  97. package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
  98. package/templates/vendors/aider/.aider.conf.yml.template +27 -0
  99. package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
  100. package/templates/vendors/claude/.claude/settings.json.template +49 -0
  101. package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
  102. package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
  103. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
  104. package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
  105. package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
  106. package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
  107. package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
  108. package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
  109. package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
  110. package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
  111. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
  112. package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
  113. package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
  114. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
  115. package/templates/vendors/cline/.clinerules.template +53 -0
  116. package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
  117. package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
  118. package/templates/vendors/windsurf/.windsurf/rules/lumenflow.md.template +34 -0
@@ -0,0 +1,296 @@
1
+ #!/usr/bin/env node
2
+ /* eslint-disable no-console -- CLI tool requires console output */
3
+ /**
4
+ * Signal Cleanup CLI (WU-1204)
5
+ *
6
+ * Prune old signals based on TTL policy to prevent unbounded growth.
7
+ * Implements configurable retention:
8
+ * - Read signals: 7 days default TTL
9
+ * - Unread signals: 30 days default TTL
10
+ * - Max entries: 500 default
11
+ * - Active WU protection: signals linked to in_progress/blocked WUs are never removed
12
+ *
13
+ * Usage:
14
+ * pnpm signal:cleanup # Cleanup based on TTL policy
15
+ * pnpm signal:cleanup --dry-run # Preview without changes
16
+ * pnpm signal:cleanup --ttl 3d # Override read signal TTL
17
+ * pnpm signal:cleanup --max-entries 100 # Override max entries
18
+ * pnpm signal:cleanup --json # Output as JSON
19
+ *
20
+ * @see {@link packages/@lumenflow/memory/src/signal-cleanup-core.ts} - Core logic
21
+ * @see {@link packages/@lumenflow/memory/__tests__/signal-cleanup-core.test.ts} - Tests
22
+ */
23
+ import fs from 'node:fs/promises';
24
+ import path from 'node:path';
25
+ import fg from 'fast-glob';
26
+ import { parse as parseYaml } from 'yaml';
27
+ import { cleanupSignals } from '@lumenflow/memory/dist/signal-cleanup-core.js';
28
+ import { createWUParser } from '@lumenflow/core/dist/arg-parser.js';
29
+ import { EXIT_CODES, LUMENFLOW_PATHS } from '@lumenflow/core/dist/wu-constants.js';
30
+ import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
31
+ /**
32
+ * Log prefix for signal:cleanup output
33
+ */
34
+ const LOG_PREFIX = '[signal:cleanup]';
35
+ /**
36
+ * Tool name for audit logging
37
+ */
38
+ const TOOL_NAME = 'signal:cleanup';
39
+ /**
40
+ * Bytes per KB for formatting
41
+ */
42
+ const BYTES_PER_KB = 1024;
43
+ /**
44
+ * Active WU statuses that should protect signals
45
+ */
46
+ const ACTIVE_WU_STATUSES = ['in_progress', 'blocked'];
47
+ /**
48
+ * CLI argument options specific to signal:cleanup
49
+ */
50
+ const CLI_OPTIONS = {
51
+ dryRun: {
52
+ name: 'dryRun',
53
+ flags: '--dry-run',
54
+ description: 'Preview cleanup without making changes',
55
+ },
56
+ ttl: {
57
+ name: 'ttl',
58
+ flags: '--ttl <duration>',
59
+ description: 'TTL for read signals (e.g., 7d, 24h). Default: 7d',
60
+ },
61
+ unreadTtl: {
62
+ name: 'unreadTtl',
63
+ flags: '--unread-ttl <duration>',
64
+ description: 'TTL for unread signals (e.g., 30d). Default: 30d',
65
+ },
66
+ maxEntries: {
67
+ name: 'maxEntries',
68
+ flags: '--max-entries <count>',
69
+ description: 'Maximum signals to retain. Default: 500',
70
+ },
71
+ json: {
72
+ name: 'json',
73
+ flags: '--json',
74
+ description: 'Output as JSON',
75
+ },
76
+ quiet: {
77
+ name: 'quiet',
78
+ flags: '-q, --quiet',
79
+ description: 'Suppress output except errors',
80
+ },
81
+ baseDir: {
82
+ name: 'baseDir',
83
+ flags: '-b, --base-dir <path>',
84
+ description: 'Base directory (defaults to current directory)',
85
+ },
86
+ };
87
+ /**
88
+ * Write audit log entry for tool execution
89
+ *
90
+ * @param baseDir - Base directory
91
+ * @param entry - Audit log entry
92
+ */
93
+ async function writeAuditLog(baseDir, entry) {
94
+ try {
95
+ const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
96
+ const logDir = path.dirname(logPath);
97
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool creates known directory
98
+ await fs.mkdir(logDir, { recursive: true });
99
+ const line = `${JSON.stringify(entry)}\n`;
100
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool writes audit log
101
+ await fs.appendFile(logPath, line, 'utf-8');
102
+ }
103
+ catch {
104
+ // Audit logging is non-fatal - silently ignore errors
105
+ }
106
+ }
107
+ /**
108
+ * Format bytes as human-readable string
109
+ *
110
+ * @param bytes - Number of bytes
111
+ * @returns Formatted string (e.g., "1.5 KB")
112
+ */
113
+ function formatBytes(bytes) {
114
+ if (bytes < BYTES_PER_KB) {
115
+ return `${bytes} B`;
116
+ }
117
+ const kb = (bytes / BYTES_PER_KB).toFixed(1);
118
+ return `${kb} KB`;
119
+ }
120
+ /**
121
+ * Format compaction ratio as percentage
122
+ *
123
+ * @param ratio - Compaction ratio (0-1)
124
+ * @returns Formatted percentage
125
+ */
126
+ function formatRatio(ratio) {
127
+ return `${(ratio * 100).toFixed(1)}%`;
128
+ }
129
+ /**
130
+ * Get active WU IDs (in_progress or blocked) by scanning WU YAML files.
131
+ *
132
+ * @param baseDir - Base directory
133
+ * @returns Set of active WU IDs
134
+ */
135
+ async function getActiveWuIds(baseDir) {
136
+ const activeIds = new Set();
137
+ try {
138
+ const config = getConfig({ projectRoot: baseDir });
139
+ const wuDir = path.join(baseDir, config.directories.wuDir);
140
+ // Find all WU YAML files
141
+ const wuFiles = await fg('WU-*.yaml', { cwd: wuDir });
142
+ for (const file of wuFiles) {
143
+ try {
144
+ const filePath = path.join(wuDir, file);
145
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- CLI tool reads known path
146
+ const content = await fs.readFile(filePath, 'utf-8');
147
+ const wu = parseYaml(content);
148
+ if (wu.id && wu.status && ACTIVE_WU_STATUSES.includes(wu.status)) {
149
+ activeIds.add(wu.id);
150
+ }
151
+ }
152
+ catch {
153
+ // Skip files that fail to parse
154
+ continue;
155
+ }
156
+ }
157
+ }
158
+ catch {
159
+ // If we can't read WU files, return empty set (safer to remove nothing)
160
+ }
161
+ return activeIds;
162
+ }
163
+ /**
164
+ * Parse CLI arguments
165
+ *
166
+ * @returns Parsed arguments
167
+ */
168
+ function parseArguments() {
169
+ return createWUParser({
170
+ name: 'signal-cleanup',
171
+ description: 'Prune old signals based on TTL policy to prevent unbounded growth',
172
+ options: [
173
+ CLI_OPTIONS.dryRun,
174
+ CLI_OPTIONS.ttl,
175
+ CLI_OPTIONS.unreadTtl,
176
+ CLI_OPTIONS.maxEntries,
177
+ CLI_OPTIONS.json,
178
+ CLI_OPTIONS.quiet,
179
+ CLI_OPTIONS.baseDir,
180
+ ],
181
+ required: [],
182
+ allowPositionalId: false,
183
+ });
184
+ }
185
+ /**
186
+ * Print cleanup result to console
187
+ *
188
+ * @param result - Cleanup result
189
+ * @param quiet - Suppress verbose output
190
+ */
191
+ function printResult(result, quiet) {
192
+ if (result.dryRun) {
193
+ console.log(`${LOG_PREFIX} Dry-run: Would remove ${result.removedIds.length} signal(s)`);
194
+ }
195
+ else {
196
+ console.log(`${LOG_PREFIX} Cleanup complete`);
197
+ }
198
+ if (quiet) {
199
+ console.log(`${result.removedIds.length} removed, ${result.retainedIds.length} retained`);
200
+ return;
201
+ }
202
+ console.log('');
203
+ console.log('Summary:');
204
+ console.log(` Removed: ${result.removedIds.length} signal(s)`);
205
+ console.log(` Retained: ${result.retainedIds.length} signal(s)`);
206
+ console.log(` Bytes Freed: ${formatBytes(result.bytesFreed)}`);
207
+ console.log(` Compaction Ratio: ${formatRatio(result.compactionRatio)}`);
208
+ console.log('');
209
+ console.log('Breakdown:');
210
+ console.log(` TTL Expired (read): ${result.breakdown.ttlExpired} removed`);
211
+ console.log(` TTL Expired (unread): ${result.breakdown.unreadTtlExpired} removed`);
212
+ console.log(` Count Limit Exceeded: ${result.breakdown.countLimitExceeded} removed`);
213
+ console.log(` Active WU Protected: ${result.breakdown.activeWuProtected} retained`);
214
+ if (result.removedIds.length > 0 && !result.dryRun) {
215
+ console.log('');
216
+ console.log('Removed Signal IDs:');
217
+ // Show first 10, then "and X more" if needed
218
+ const displayLimit = 10;
219
+ const displayIds = result.removedIds.slice(0, displayLimit);
220
+ for (const id of displayIds) {
221
+ console.log(` - ${id}`);
222
+ }
223
+ if (result.removedIds.length > displayLimit) {
224
+ console.log(` ... and ${result.removedIds.length - displayLimit} more`);
225
+ }
226
+ }
227
+ if (result.dryRun) {
228
+ console.log('');
229
+ console.log('To execute, run without --dry-run');
230
+ }
231
+ }
232
+ /**
233
+ * Main CLI entry point
234
+ */
235
+ async function main() {
236
+ const args = parseArguments();
237
+ const baseDir = args.baseDir || process.cwd();
238
+ const startedAt = new Date().toISOString();
239
+ const startTime = Date.now();
240
+ let result = null;
241
+ let error = null;
242
+ try {
243
+ result = await cleanupSignals(baseDir, {
244
+ dryRun: args.dryRun,
245
+ ttl: args.ttl,
246
+ unreadTtl: args.unreadTtl,
247
+ maxEntries: args.maxEntries ? parseInt(args.maxEntries, 10) : undefined,
248
+ getActiveWuIds: () => getActiveWuIds(baseDir),
249
+ });
250
+ }
251
+ catch (err) {
252
+ error = err.message;
253
+ }
254
+ const durationMs = Date.now() - startTime;
255
+ await writeAuditLog(baseDir, {
256
+ tool: TOOL_NAME,
257
+ status: error ? 'failed' : 'success',
258
+ startedAt,
259
+ completedAt: new Date().toISOString(),
260
+ durationMs,
261
+ input: {
262
+ baseDir,
263
+ dryRun: args.dryRun,
264
+ ttl: args.ttl,
265
+ unreadTtl: args.unreadTtl,
266
+ maxEntries: args.maxEntries,
267
+ },
268
+ output: result
269
+ ? {
270
+ success: result.success,
271
+ removedCount: result.removedIds.length,
272
+ retainedCount: result.retainedIds.length,
273
+ bytesFreed: result.bytesFreed,
274
+ compactionRatio: result.compactionRatio,
275
+ breakdown: result.breakdown,
276
+ dryRun: result.dryRun,
277
+ }
278
+ : null,
279
+ error: error ? { message: error } : null,
280
+ });
281
+ if (error) {
282
+ console.error(`${LOG_PREFIX} Error: ${error}`);
283
+ process.exit(EXIT_CODES.ERROR);
284
+ }
285
+ if (args.json) {
286
+ console.log(JSON.stringify(result, null, 2));
287
+ process.exit(EXIT_CODES.SUCCESS);
288
+ }
289
+ if (result) {
290
+ printResult(result, args.quiet ?? false);
291
+ }
292
+ }
293
+ main().catch((e) => {
294
+ console.error(`${LOG_PREFIX} ${e.message}`);
295
+ process.exit(EXIT_CODES.ERROR);
296
+ });
@@ -10,8 +10,8 @@
10
10
  * pnpm spawn:list --initiative INIT-XXX # All spawns in an initiative
11
11
  * pnpm spawn:list --json # JSON output
12
12
  *
13
- * @see {@link tools/lib/spawn-tree.mjs} - Tree builder
14
- * @see {@link tools/__tests__/spawn-list.test.mjs} - Tests
13
+ * @see {@link packages/@lumenflow/cli/src/lib/spawn-tree.ts} - Tree builder
14
+ * @see {@link packages/@lumenflow/cli/src/__tests__/spawn-list.test.ts} - Tests
15
15
  */
16
16
  import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
17
17
  import { die } from '@lumenflow/core/dist/error-handler.js';
@@ -130,9 +130,10 @@ async function main() {
130
130
  console.log(`\nTotal: ${spawns.length} spawn(s) across ${rootWuIds.length} root WU(s)`);
131
131
  }
132
132
  }
133
- // Guard main() for testability
134
- import { fileURLToPath } from 'node:url';
135
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
133
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
134
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
135
+ // path but import.meta.url resolves to the real path - they never match
136
+ if (import.meta.main) {
136
137
  main().catch((err) => {
137
138
  console.error(`${LOG_PREFIX} ${EMOJI.ERROR} ${err.message}`);
138
139
  process.exit(1);
@@ -5,7 +5,7 @@
5
5
  * One-time migration utility from WU YAMLs to event-sourced state store.
6
6
  * Reads all WU YAML files and generates corresponding events in the state store.
7
7
  *
8
- * WU-1107: INIT-003 Phase 3c - Migrate state-bootstrap.mjs from PatientPath
8
+ * WU-1107: INIT-003 Phase 3c - Migrate state-bootstrap.ts from PatientPath
9
9
  *
10
10
  * Usage:
11
11
  * pnpm state:bootstrap # Dry-run mode (shows what would be done)
@@ -299,9 +299,10 @@ async function main() {
299
299
  }
300
300
  process.exit(EXIT_CODES.SUCCESS);
301
301
  }
302
- // Guard main() for testability
303
- import { fileURLToPath } from 'node:url';
302
+ // WU-1181: Use import.meta.main instead of process.argv[1] comparison
303
+ // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
304
+ // path but import.meta.url resolves to the real path - they never match
304
305
  import { runCLI } from './cli-entry-point.js';
305
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
306
+ if (import.meta.main) {
306
307
  runCLI(main);
307
308
  }