@lumenflow/cli 1.6.0 → 2.1.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 (42) hide show
  1. package/README.md +19 -0
  2. package/dist/__tests__/backlog-prune.test.js +478 -0
  3. package/dist/__tests__/deps-operations.test.js +206 -0
  4. package/dist/__tests__/file-operations.test.js +906 -0
  5. package/dist/__tests__/git-operations.test.js +668 -0
  6. package/dist/__tests__/guards-validation.test.js +416 -0
  7. package/dist/__tests__/init-plan.test.js +340 -0
  8. package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
  9. package/dist/__tests__/metrics-cli.test.js +619 -0
  10. package/dist/__tests__/rotate-progress.test.js +127 -0
  11. package/dist/__tests__/session-coordinator.test.js +109 -0
  12. package/dist/__tests__/state-bootstrap.test.js +432 -0
  13. package/dist/__tests__/trace-gen.test.js +115 -0
  14. package/dist/backlog-prune.js +299 -0
  15. package/dist/deps-add.js +215 -0
  16. package/dist/deps-remove.js +94 -0
  17. package/dist/docs-sync.js +72 -326
  18. package/dist/file-delete.js +236 -0
  19. package/dist/file-edit.js +247 -0
  20. package/dist/file-read.js +197 -0
  21. package/dist/file-write.js +220 -0
  22. package/dist/git-branch.js +187 -0
  23. package/dist/git-diff.js +177 -0
  24. package/dist/git-log.js +230 -0
  25. package/dist/git-status.js +208 -0
  26. package/dist/guard-locked.js +169 -0
  27. package/dist/guard-main-branch.js +202 -0
  28. package/dist/guard-worktree-commit.js +160 -0
  29. package/dist/init-plan.js +337 -0
  30. package/dist/lumenflow-upgrade.js +178 -0
  31. package/dist/metrics-cli.js +433 -0
  32. package/dist/rotate-progress.js +247 -0
  33. package/dist/session-coordinator.js +300 -0
  34. package/dist/state-bootstrap.js +307 -0
  35. package/dist/sync-templates.js +212 -0
  36. package/dist/trace-gen.js +331 -0
  37. package/dist/validate-agent-skills.js +218 -0
  38. package/dist/validate-agent-sync.js +148 -0
  39. package/dist/validate-backlog-sync.js +152 -0
  40. package/dist/validate-skills-spec.js +206 -0
  41. package/dist/validate.js +230 -0
  42. package/package.json +37 -7
@@ -0,0 +1,247 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Rotate Progress CLI Command
4
+ *
5
+ * Moves completed WUs from status.md In Progress section to Completed section.
6
+ * Keeps the status file tidy by archiving done work.
7
+ *
8
+ * WU-1112: INIT-003 Phase 6 - Migrate remaining Tier 1 tools
9
+ *
10
+ * Usage:
11
+ * pnpm rotate:progress
12
+ * pnpm rotate:progress --dry-run
13
+ * pnpm rotate:progress --limit 10
14
+ */
15
+ import { readFileSync, writeFileSync, existsSync, readdirSync } from 'node:fs';
16
+ import { join } from 'node:path';
17
+ import { parse as parseYaml } from 'yaml';
18
+ import { EXIT_CODES, STATUS_SECTIONS, DIRECTORIES, FILE_SYSTEM, } from '@lumenflow/core/dist/wu-constants.js';
19
+ import { runCLI } from './cli-entry-point.js';
20
+ /** Log prefix for console output */
21
+ const LOG_PREFIX = '[rotate:progress]';
22
+ /**
23
+ * Parse command line arguments for rotate-progress
24
+ *
25
+ * @param argv - Process argv array
26
+ * @returns Parsed arguments
27
+ */
28
+ export function parseRotateArgs(argv) {
29
+ const args = {};
30
+ // Skip node and script name
31
+ const cliArgs = argv.slice(2);
32
+ for (let i = 0; i < cliArgs.length; i++) {
33
+ const arg = cliArgs[i];
34
+ if (arg === '--help' || arg === '-h') {
35
+ args.help = true;
36
+ }
37
+ else if (arg === '--dry-run' || arg === '-n') {
38
+ args.dryRun = true;
39
+ }
40
+ else if (arg === '--limit' || arg === '-l') {
41
+ const val = cliArgs[++i];
42
+ if (val)
43
+ args.limit = parseInt(val, 10);
44
+ }
45
+ }
46
+ return args;
47
+ }
48
+ /**
49
+ * Get WU status from YAML file
50
+ */
51
+ function getWuStatus(wuId, baseDir = process.cwd()) {
52
+ const yamlPath = join(baseDir, DIRECTORIES.WU_DIR, `${wuId}.yaml`);
53
+ if (!existsSync(yamlPath)) {
54
+ return null;
55
+ }
56
+ try {
57
+ const content = readFileSync(yamlPath, { encoding: FILE_SYSTEM.ENCODING });
58
+ const yaml = parseYaml(content);
59
+ return yaml?.status || null;
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ /**
66
+ * Get all WU statuses from YAML files
67
+ */
68
+ function getAllWuStatuses(baseDir = process.cwd()) {
69
+ const statuses = new Map();
70
+ const wuDir = join(baseDir, DIRECTORIES.WU_DIR);
71
+ if (!existsSync(wuDir)) {
72
+ return statuses;
73
+ }
74
+ const files = readdirSync(wuDir);
75
+ for (const file of files) {
76
+ if (file.endsWith('.yaml') || file.endsWith('.yml')) {
77
+ const wuId = file.replace(/\.ya?ml$/, '');
78
+ const status = getWuStatus(wuId, baseDir);
79
+ if (status) {
80
+ statuses.set(wuId, status);
81
+ }
82
+ }
83
+ }
84
+ return statuses;
85
+ }
86
+ /**
87
+ * Find WUs in the In Progress section that have status=done in YAML
88
+ *
89
+ * @param statusContent - Content of status.md file
90
+ * @param wuStatuses - Map of WU IDs to their statuses from YAML
91
+ * @returns Array of WU IDs that should be moved to Completed
92
+ */
93
+ export function findCompletedWUs(statusContent, wuStatuses) {
94
+ const completed = [];
95
+ // Find the In Progress section
96
+ const inProgressStart = statusContent.indexOf(STATUS_SECTIONS.IN_PROGRESS);
97
+ if (inProgressStart === -1) {
98
+ return completed;
99
+ }
100
+ // Find the end of In Progress section (next ## heading or end of file)
101
+ const afterInProgress = statusContent.slice(inProgressStart + STATUS_SECTIONS.IN_PROGRESS.length);
102
+ const nextSectionMatch = afterInProgress.match(/\n##/);
103
+ const inProgressSection = nextSectionMatch
104
+ ? afterInProgress.slice(0, nextSectionMatch.index)
105
+ : afterInProgress;
106
+ // Extract WU IDs from In Progress section
107
+ const wuIdMatches = inProgressSection.match(/WU-\d+/g) || [];
108
+ const uniqueWuIds = [...new Set(wuIdMatches)];
109
+ // Check which ones have done status
110
+ for (const wuId of uniqueWuIds) {
111
+ const status = wuStatuses.get(wuId);
112
+ if (status === 'done' || status === 'completed') {
113
+ completed.push(wuId);
114
+ }
115
+ }
116
+ return completed;
117
+ }
118
+ /**
119
+ * Build the rotated status.md content
120
+ *
121
+ * @param statusContent - Original status.md content
122
+ * @param completedWUs - WU IDs to move to Completed
123
+ * @returns Updated status.md content
124
+ */
125
+ export function buildRotatedContent(statusContent, completedWUs) {
126
+ if (completedWUs.length === 0) {
127
+ return statusContent;
128
+ }
129
+ let content = statusContent;
130
+ const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
131
+ // For each completed WU, move it from In Progress to Completed
132
+ for (const wuId of completedWUs) {
133
+ // Find and remove the line from In Progress section
134
+ const wuLineRegex = new RegExp(`\\n?-\\s*\\[?[\\sx]?\\]?\\s*\\[?${wuId}[^\\n]*`, 'gi');
135
+ const match = content.match(wuLineRegex);
136
+ if (match) {
137
+ // Extract the original line text
138
+ const originalLine = match[0].trim();
139
+ // Remove from current position
140
+ content = content.replace(wuLineRegex, '');
141
+ // Extract title from the original line
142
+ // Match "WU-XXXX - Title" or "WU-XXXX Title" patterns
143
+ const titleMatch = originalLine.match(/WU-\d+\s*[-—]?\s*([^(]*)/);
144
+ let title = wuId;
145
+ if (titleMatch) {
146
+ const fullMatch = titleMatch[0].trim();
147
+ const wuPart = wuId;
148
+ // Get everything after the WU ID
149
+ const rest = fullMatch
150
+ .slice(wuId.length)
151
+ .replace(/^[\s-—]+/, '')
152
+ .trim();
153
+ title = rest ? `${wuPart} - ${rest}` : wuPart;
154
+ }
155
+ // Build the completed entry with date
156
+ const completedEntry = `- [x] ${title} (${today})`;
157
+ // Add to Completed section
158
+ const completedSectionIndex = content.indexOf(STATUS_SECTIONS.COMPLETED);
159
+ if (completedSectionIndex !== -1) {
160
+ const insertPoint = completedSectionIndex + STATUS_SECTIONS.COMPLETED.length;
161
+ content =
162
+ content.slice(0, insertPoint) + '\n' + completedEntry + content.slice(insertPoint);
163
+ }
164
+ }
165
+ }
166
+ // Clean up any double newlines
167
+ content = content.replace(/\n{3,}/g, '\n\n');
168
+ return content;
169
+ }
170
+ /**
171
+ * Print help message for rotate-progress
172
+ */
173
+ /* istanbul ignore next -- CLI entry point */
174
+ function printHelp() {
175
+ console.log(`
176
+ Usage: rotate-progress [options]
177
+
178
+ Move completed WUs from status.md In Progress to Completed section.
179
+
180
+ Options:
181
+ -n, --dry-run Show changes without writing
182
+ -l, --limit <n> Maximum number of WUs to rotate
183
+ -h, --help Show this help message
184
+
185
+ How it works:
186
+ 1. Scans status.md for WUs listed in "In Progress" section
187
+ 2. Checks each WU's YAML file for status=done
188
+ 3. Moves done WUs to "Completed" section with date stamp
189
+
190
+ Examples:
191
+ rotate:progress # Rotate all completed WUs
192
+ rotate:progress --dry-run # Preview what would be rotated
193
+ rotate:progress --limit 5 # Rotate at most 5 WUs
194
+ `);
195
+ }
196
+ /**
197
+ * Main entry point for rotate-progress command
198
+ */
199
+ /* istanbul ignore next -- CLI entry point */
200
+ async function main() {
201
+ const args = parseRotateArgs(process.argv);
202
+ if (args.help) {
203
+ printHelp();
204
+ process.exit(EXIT_CODES.SUCCESS);
205
+ }
206
+ // Read status.md
207
+ const statusPath = join(process.cwd(), DIRECTORIES.STATUS_PATH);
208
+ if (!existsSync(statusPath)) {
209
+ console.error(`${LOG_PREFIX} Error: ${statusPath} not found`);
210
+ process.exit(EXIT_CODES.ERROR);
211
+ }
212
+ const statusContent = readFileSync(statusPath, {
213
+ encoding: FILE_SYSTEM.ENCODING,
214
+ });
215
+ // Get all WU statuses
216
+ const wuStatuses = getAllWuStatuses();
217
+ // Find completed WUs
218
+ let completedWUs = findCompletedWUs(statusContent, wuStatuses);
219
+ if (completedWUs.length === 0) {
220
+ console.log(`${LOG_PREFIX} No completed WUs to rotate.`);
221
+ process.exit(EXIT_CODES.SUCCESS);
222
+ }
223
+ // Apply limit if specified
224
+ if (args.limit && args.limit > 0) {
225
+ completedWUs = completedWUs.slice(0, args.limit);
226
+ }
227
+ console.log(`${LOG_PREFIX} Found ${completedWUs.length} WU(s) to rotate:`);
228
+ for (const wuId of completedWUs) {
229
+ console.log(` - ${wuId}`);
230
+ }
231
+ if (args.dryRun) {
232
+ console.log(`\n${LOG_PREFIX} DRY RUN - No changes made.`);
233
+ const newContent = buildRotatedContent(statusContent, completedWUs);
234
+ console.log(`\n${LOG_PREFIX} Preview of changes:`);
235
+ console.log('---');
236
+ console.log(newContent.slice(0, 500) + '...');
237
+ process.exit(EXIT_CODES.SUCCESS);
238
+ }
239
+ // Build and write updated content
240
+ const newContent = buildRotatedContent(statusContent, completedWUs);
241
+ writeFileSync(statusPath, newContent, { encoding: FILE_SYSTEM.ENCODING });
242
+ console.log(`\n${LOG_PREFIX} ✅ Rotated ${completedWUs.length} WU(s) to Completed section.`);
243
+ }
244
+ // Run main if executed directly
245
+ if (import.meta.main) {
246
+ runCLI(main);
247
+ }
@@ -0,0 +1,300 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Session Coordinator CLI Command
4
+ *
5
+ * Manages agent sessions - starting, stopping, status, and handoffs.
6
+ * Sessions track which agent is working on which WU and facilitate
7
+ * coordination between multiple agents.
8
+ *
9
+ * WU-1112: INIT-003 Phase 6 - Migrate remaining Tier 1 tools
10
+ *
11
+ * Usage:
12
+ * pnpm session:start --wu WU-1112 --agent claude-code
13
+ * pnpm session:stop --reason "Completed work"
14
+ * pnpm session:status
15
+ * pnpm session:handoff --wu WU-1112 --agent cursor
16
+ */
17
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
18
+ import { join, dirname } from 'node:path';
19
+ import { EXIT_CODES, LUMENFLOW_PATHS, FILE_SYSTEM } from '@lumenflow/core/dist/wu-constants.js';
20
+ import { runCLI } from './cli-entry-point.js';
21
+ /** Log prefix for console output */
22
+ const LOG_PREFIX = '[session]';
23
+ /**
24
+ * Session subcommands
25
+ */
26
+ export var SessionCommand;
27
+ (function (SessionCommand) {
28
+ SessionCommand["START"] = "start";
29
+ SessionCommand["STOP"] = "stop";
30
+ SessionCommand["STATUS"] = "status";
31
+ SessionCommand["HANDOFF"] = "handoff";
32
+ })(SessionCommand || (SessionCommand = {}));
33
+ /**
34
+ * Parse command line arguments for session-coordinator
35
+ *
36
+ * @param argv - Process argv array
37
+ * @returns Parsed arguments
38
+ */
39
+ export function parseSessionArgs(argv) {
40
+ const args = {};
41
+ // Skip node and script name
42
+ const cliArgs = argv.slice(2);
43
+ for (let i = 0; i < cliArgs.length; i++) {
44
+ const arg = cliArgs[i];
45
+ if (arg === '--help' || arg === '-h') {
46
+ args.help = true;
47
+ }
48
+ else if (arg === '--wu' || arg === '-w') {
49
+ args.wuId = cliArgs[++i];
50
+ }
51
+ else if (arg === '--agent' || arg === '-a') {
52
+ args.agent = cliArgs[++i];
53
+ }
54
+ else if (arg === '--reason' || arg === '-r') {
55
+ args.reason = cliArgs[++i];
56
+ }
57
+ else if (!arg.startsWith('-')) {
58
+ // Subcommand
59
+ if (Object.values(SessionCommand).includes(arg)) {
60
+ args.command = arg;
61
+ }
62
+ }
63
+ }
64
+ // Default to status if no command given
65
+ if (!args.command && !args.help) {
66
+ args.command = SessionCommand.STATUS;
67
+ }
68
+ return args;
69
+ }
70
+ /**
71
+ * Validate session command arguments
72
+ *
73
+ * @param args - Parsed session arguments
74
+ * @returns Validation result
75
+ */
76
+ export function validateSessionCommand(args) {
77
+ const { command, wuId } = args;
78
+ switch (command) {
79
+ case SessionCommand.START:
80
+ if (!wuId) {
81
+ return {
82
+ valid: false,
83
+ error: 'session start requires --wu <id> to specify which WU to work on',
84
+ };
85
+ }
86
+ break;
87
+ case SessionCommand.HANDOFF:
88
+ if (!wuId) {
89
+ return {
90
+ valid: false,
91
+ error: 'session handoff requires --wu <id> to specify which WU to hand off',
92
+ };
93
+ }
94
+ break;
95
+ case SessionCommand.STOP:
96
+ case SessionCommand.STATUS:
97
+ // No required arguments
98
+ break;
99
+ default:
100
+ return {
101
+ valid: false,
102
+ error: `Unknown command: ${command}`,
103
+ };
104
+ }
105
+ return { valid: true };
106
+ }
107
+ /**
108
+ * Get path to current session file
109
+ */
110
+ function getSessionPath() {
111
+ return join(process.cwd(), LUMENFLOW_PATHS.SESSION_CURRENT);
112
+ }
113
+ /**
114
+ * Read current session state
115
+ */
116
+ function readCurrentSession() {
117
+ const path = getSessionPath();
118
+ if (!existsSync(path)) {
119
+ return null;
120
+ }
121
+ try {
122
+ const content = readFileSync(path, { encoding: FILE_SYSTEM.ENCODING });
123
+ return JSON.parse(content);
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ }
129
+ /**
130
+ * Write session state
131
+ */
132
+ function writeSession(session) {
133
+ const path = getSessionPath();
134
+ const dir = dirname(path);
135
+ if (!existsSync(dir)) {
136
+ mkdirSync(dir, { recursive: true });
137
+ }
138
+ if (session === null) {
139
+ // Remove session file if clearing
140
+ if (existsSync(path)) {
141
+ const { unlinkSync } = require('node:fs');
142
+ unlinkSync(path);
143
+ }
144
+ }
145
+ else {
146
+ writeFileSync(path, JSON.stringify(session, null, 2), {
147
+ encoding: FILE_SYSTEM.ENCODING,
148
+ });
149
+ }
150
+ }
151
+ /**
152
+ * Print help message for session-coordinator
153
+ */
154
+ /* istanbul ignore next -- CLI entry point */
155
+ function printHelp() {
156
+ console.log(`
157
+ Usage: session <command> [options]
158
+
159
+ Manage agent sessions for WU work coordination.
160
+
161
+ Commands:
162
+ start Start a new session
163
+ stop Stop current session
164
+ status Show current session status
165
+ handoff Hand off session to another agent
166
+
167
+ Options:
168
+ -w, --wu <id> WU ID to work on (required for start/handoff)
169
+ -a, --agent <type> Agent type (e.g., claude-code, cursor, aider)
170
+ -r, --reason <msg> Reason for stopping session
171
+ -h, --help Show this help message
172
+
173
+ Examples:
174
+ session start --wu WU-1112 --agent claude-code
175
+ session stop --reason "Completed acceptance criteria"
176
+ session status
177
+ session handoff --wu WU-1112 --agent cursor
178
+
179
+ Session files are stored in: ${LUMENFLOW_PATHS.SESSION_CURRENT}
180
+ `);
181
+ }
182
+ /**
183
+ * Handle start command
184
+ */
185
+ /* istanbul ignore next -- CLI entry point */
186
+ function handleStart(args) {
187
+ const current = readCurrentSession();
188
+ if (current) {
189
+ console.log(`${LOG_PREFIX} Active session already exists:`);
190
+ console.log(` WU: ${current.wuId}`);
191
+ console.log(` Agent: ${current.agent}`);
192
+ console.log(` Started: ${current.startedAt}`);
193
+ console.log(`\n${LOG_PREFIX} Stop current session first with: session stop`);
194
+ process.exit(EXIT_CODES.ERROR);
195
+ }
196
+ const session = {
197
+ wuId: args.wuId,
198
+ agent: args.agent || 'unknown',
199
+ startedAt: new Date().toISOString(),
200
+ lastActivity: new Date().toISOString(),
201
+ };
202
+ writeSession(session);
203
+ console.log(`${LOG_PREFIX} ✅ Session started`);
204
+ console.log(` WU: ${session.wuId}`);
205
+ console.log(` Agent: ${session.agent}`);
206
+ console.log(` Started: ${session.startedAt}`);
207
+ }
208
+ /**
209
+ * Handle stop command
210
+ */
211
+ /* istanbul ignore next -- CLI entry point */
212
+ function handleStop(args) {
213
+ const current = readCurrentSession();
214
+ if (!current) {
215
+ console.log(`${LOG_PREFIX} No active session to stop.`);
216
+ process.exit(EXIT_CODES.SUCCESS);
217
+ }
218
+ const duration = Date.now() - new Date(current.startedAt).getTime();
219
+ const durationMin = Math.round(duration / 60000);
220
+ console.log(`${LOG_PREFIX} ✅ Session stopped`);
221
+ console.log(` WU: ${current.wuId}`);
222
+ console.log(` Agent: ${current.agent}`);
223
+ console.log(` Duration: ${durationMin} minutes`);
224
+ if (args.reason) {
225
+ console.log(` Reason: ${args.reason}`);
226
+ }
227
+ writeSession(null);
228
+ }
229
+ /**
230
+ * Handle status command
231
+ */
232
+ /* istanbul ignore next -- CLI entry point */
233
+ function handleStatus() {
234
+ const current = readCurrentSession();
235
+ if (!current) {
236
+ console.log(`${LOG_PREFIX} No active session.`);
237
+ console.log(`\n${LOG_PREFIX} Start a session with: session start --wu WU-XXXX`);
238
+ return;
239
+ }
240
+ const duration = Date.now() - new Date(current.startedAt).getTime();
241
+ const durationMin = Math.round(duration / 60000);
242
+ console.log(`${LOG_PREFIX} Active session:`);
243
+ console.log(` WU: ${current.wuId}`);
244
+ console.log(` Agent: ${current.agent}`);
245
+ console.log(` Started: ${current.startedAt}`);
246
+ console.log(` Duration: ${durationMin} minutes`);
247
+ console.log(` Last activity: ${current.lastActivity}`);
248
+ }
249
+ /**
250
+ * Handle handoff command
251
+ */
252
+ /* istanbul ignore next -- CLI entry point */
253
+ function handleHandoff(args) {
254
+ const current = readCurrentSession();
255
+ if (current) {
256
+ console.log(`${LOG_PREFIX} Stopping current session...`);
257
+ handleStop({ reason: `Handoff to ${args.agent || 'another agent'}` });
258
+ }
259
+ console.log(`\n${LOG_PREFIX} Starting new session for handoff...`);
260
+ handleStart(args);
261
+ }
262
+ /**
263
+ * Main entry point for session-coordinator command
264
+ */
265
+ /* istanbul ignore next -- CLI entry point */
266
+ async function main() {
267
+ const args = parseSessionArgs(process.argv);
268
+ if (args.help) {
269
+ printHelp();
270
+ process.exit(EXIT_CODES.SUCCESS);
271
+ }
272
+ const validation = validateSessionCommand(args);
273
+ if (!validation.valid) {
274
+ console.error(`${LOG_PREFIX} Error: ${validation.error}`);
275
+ printHelp();
276
+ process.exit(EXIT_CODES.ERROR);
277
+ }
278
+ switch (args.command) {
279
+ case SessionCommand.START:
280
+ handleStart(args);
281
+ break;
282
+ case SessionCommand.STOP:
283
+ handleStop(args);
284
+ break;
285
+ case SessionCommand.STATUS:
286
+ handleStatus();
287
+ break;
288
+ case SessionCommand.HANDOFF:
289
+ handleHandoff(args);
290
+ break;
291
+ default:
292
+ console.error(`${LOG_PREFIX} Unknown command: ${args.command}`);
293
+ printHelp();
294
+ process.exit(EXIT_CODES.ERROR);
295
+ }
296
+ }
297
+ // Run main if executed directly
298
+ if (import.meta.main) {
299
+ runCLI(main);
300
+ }