@lumenflow/cli 1.6.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/backlog-prune.test.js +478 -0
- package/dist/__tests__/deps-operations.test.js +206 -0
- package/dist/__tests__/file-operations.test.js +906 -0
- package/dist/__tests__/git-operations.test.js +668 -0
- package/dist/__tests__/guards-validation.test.js +416 -0
- package/dist/__tests__/init-plan.test.js +340 -0
- package/dist/__tests__/lumenflow-upgrade.test.js +107 -0
- package/dist/__tests__/metrics-cli.test.js +619 -0
- package/dist/__tests__/rotate-progress.test.js +127 -0
- package/dist/__tests__/session-coordinator.test.js +109 -0
- package/dist/__tests__/state-bootstrap.test.js +432 -0
- package/dist/__tests__/trace-gen.test.js +115 -0
- package/dist/backlog-prune.js +299 -0
- package/dist/deps-add.js +215 -0
- package/dist/deps-remove.js +94 -0
- package/dist/file-delete.js +236 -0
- package/dist/file-edit.js +247 -0
- package/dist/file-read.js +197 -0
- package/dist/file-write.js +220 -0
- package/dist/git-branch.js +187 -0
- package/dist/git-diff.js +177 -0
- package/dist/git-log.js +230 -0
- package/dist/git-status.js +208 -0
- package/dist/guard-locked.js +169 -0
- package/dist/guard-main-branch.js +202 -0
- package/dist/guard-worktree-commit.js +160 -0
- package/dist/init-plan.js +337 -0
- package/dist/lumenflow-upgrade.js +178 -0
- package/dist/metrics-cli.js +433 -0
- package/dist/rotate-progress.js +247 -0
- package/dist/session-coordinator.js +300 -0
- package/dist/state-bootstrap.js +307 -0
- package/dist/trace-gen.js +331 -0
- package/dist/validate-agent-skills.js +218 -0
- package/dist/validate-agent-sync.js +148 -0
- package/dist/validate-backlog-sync.js +152 -0
- package/dist/validate-skills-spec.js +206 -0
- package/dist/validate.js +230 -0
- package/package.json +34 -6
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* State Bootstrap Command
|
|
4
|
+
*
|
|
5
|
+
* One-time migration utility from WU YAMLs to event-sourced state store.
|
|
6
|
+
* Reads all WU YAML files and generates corresponding events in the state store.
|
|
7
|
+
*
|
|
8
|
+
* WU-1107: INIT-003 Phase 3c - Migrate state-bootstrap.mjs from PatientPath
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* pnpm state:bootstrap # Dry-run mode (shows what would be done)
|
|
12
|
+
* pnpm state:bootstrap --execute # Apply changes
|
|
13
|
+
*/
|
|
14
|
+
import { readdirSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import { parse as parseYaml } from 'yaml';
|
|
17
|
+
import { readFileSync } from 'node:fs';
|
|
18
|
+
import { CLI_FLAGS, EXIT_CODES, EMOJI, STRING_LITERALS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
19
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
20
|
+
/** Log prefix for consistent output */
|
|
21
|
+
const LOG_PREFIX = '[state-bootstrap]';
|
|
22
|
+
/**
|
|
23
|
+
* Default configuration for state bootstrap
|
|
24
|
+
*/
|
|
25
|
+
export const STATE_BOOTSTRAP_DEFAULTS = {
|
|
26
|
+
/** Default WU directory path */
|
|
27
|
+
wuDir: 'docs/04-operations/tasks/wu',
|
|
28
|
+
/** Default state directory path */
|
|
29
|
+
stateDir: '.lumenflow/state',
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Parse command line arguments for state-bootstrap
|
|
33
|
+
*/
|
|
34
|
+
export function parseStateBootstrapArgs(argv) {
|
|
35
|
+
const args = {
|
|
36
|
+
dryRun: true,
|
|
37
|
+
wuDir: STATE_BOOTSTRAP_DEFAULTS.wuDir,
|
|
38
|
+
stateDir: STATE_BOOTSTRAP_DEFAULTS.stateDir,
|
|
39
|
+
force: false,
|
|
40
|
+
help: false,
|
|
41
|
+
};
|
|
42
|
+
for (let i = 2; i < argv.length; i++) {
|
|
43
|
+
const arg = argv[i];
|
|
44
|
+
if (arg === CLI_FLAGS.EXECUTE) {
|
|
45
|
+
args.dryRun = false;
|
|
46
|
+
}
|
|
47
|
+
else if (arg === CLI_FLAGS.DRY_RUN) {
|
|
48
|
+
args.dryRun = true;
|
|
49
|
+
}
|
|
50
|
+
else if (arg === CLI_FLAGS.HELP || arg === CLI_FLAGS.HELP_SHORT) {
|
|
51
|
+
args.help = true;
|
|
52
|
+
}
|
|
53
|
+
else if (arg === '--force') {
|
|
54
|
+
args.force = true;
|
|
55
|
+
}
|
|
56
|
+
else if (arg === '--wu-dir' && argv[i + 1]) {
|
|
57
|
+
args.wuDir = argv[++i];
|
|
58
|
+
}
|
|
59
|
+
else if (arg === '--state-dir' && argv[i + 1]) {
|
|
60
|
+
args.stateDir = argv[++i];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return args;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Convert a date string to ISO timestamp
|
|
67
|
+
* Falls back to start of day if only date is provided
|
|
68
|
+
*/
|
|
69
|
+
function toTimestamp(dateStr, fallback) {
|
|
70
|
+
if (!dateStr) {
|
|
71
|
+
if (fallback) {
|
|
72
|
+
return toTimestamp(fallback);
|
|
73
|
+
}
|
|
74
|
+
return new Date().toISOString();
|
|
75
|
+
}
|
|
76
|
+
// If already ISO format, return as-is
|
|
77
|
+
if (dateStr.includes('T')) {
|
|
78
|
+
return dateStr;
|
|
79
|
+
}
|
|
80
|
+
// Convert date-only to ISO timestamp at midnight UTC
|
|
81
|
+
const date = new Date(dateStr);
|
|
82
|
+
if (isNaN(date.getTime())) {
|
|
83
|
+
return new Date().toISOString();
|
|
84
|
+
}
|
|
85
|
+
return date.toISOString();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Infer events from a WU based on its current status
|
|
89
|
+
*
|
|
90
|
+
* Event generation rules:
|
|
91
|
+
* - ready: No events (WU not yet claimed)
|
|
92
|
+
* - in_progress: Generate claim event
|
|
93
|
+
* - blocked: Generate claim + block events
|
|
94
|
+
* - done/completed: Generate claim + complete events
|
|
95
|
+
*/
|
|
96
|
+
export function inferEventsFromWu(wu) {
|
|
97
|
+
const events = [];
|
|
98
|
+
// Ready WUs have no events (not yet in the lifecycle)
|
|
99
|
+
if (wu.status === 'ready' || wu.status === 'backlog' || wu.status === 'todo') {
|
|
100
|
+
return events;
|
|
101
|
+
}
|
|
102
|
+
// All other states start with a claim event
|
|
103
|
+
const claimTimestamp = toTimestamp(wu.claimed_at, wu.created);
|
|
104
|
+
events.push({
|
|
105
|
+
type: 'claim',
|
|
106
|
+
wuId: wu.id,
|
|
107
|
+
lane: wu.lane,
|
|
108
|
+
title: wu.title,
|
|
109
|
+
timestamp: claimTimestamp,
|
|
110
|
+
});
|
|
111
|
+
// Handle completed/done status
|
|
112
|
+
if (wu.status === 'done' || wu.status === 'completed') {
|
|
113
|
+
const completeTimestamp = toTimestamp(wu.completed_at, wu.created);
|
|
114
|
+
events.push({
|
|
115
|
+
type: 'complete',
|
|
116
|
+
wuId: wu.id,
|
|
117
|
+
timestamp: completeTimestamp,
|
|
118
|
+
});
|
|
119
|
+
return events;
|
|
120
|
+
}
|
|
121
|
+
// Handle blocked status
|
|
122
|
+
if (wu.status === 'blocked') {
|
|
123
|
+
// Block event timestamp should be after claim
|
|
124
|
+
// We don't have exact block time, so use claim time + 1 second
|
|
125
|
+
const claimDate = new Date(claimTimestamp);
|
|
126
|
+
claimDate.setSeconds(claimDate.getSeconds() + 1);
|
|
127
|
+
events.push({
|
|
128
|
+
type: 'block',
|
|
129
|
+
wuId: wu.id,
|
|
130
|
+
timestamp: claimDate.toISOString(),
|
|
131
|
+
reason: 'Bootstrapped from WU YAML (original reason unknown)',
|
|
132
|
+
});
|
|
133
|
+
return events;
|
|
134
|
+
}
|
|
135
|
+
// in_progress status already has claim event
|
|
136
|
+
return events;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Generate all bootstrap events from a list of WUs, ordered chronologically
|
|
140
|
+
*/
|
|
141
|
+
export function generateBootstrapEvents(wus) {
|
|
142
|
+
const allEvents = [];
|
|
143
|
+
for (const wu of wus) {
|
|
144
|
+
const events = inferEventsFromWu(wu);
|
|
145
|
+
allEvents.push(...events);
|
|
146
|
+
}
|
|
147
|
+
// Sort events chronologically
|
|
148
|
+
allEvents.sort((a, b) => {
|
|
149
|
+
const dateA = new Date(a.timestamp).getTime();
|
|
150
|
+
const dateB = new Date(b.timestamp).getTime();
|
|
151
|
+
return dateA - dateB;
|
|
152
|
+
});
|
|
153
|
+
return allEvents;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Load a WU YAML file and extract bootstrap info
|
|
157
|
+
*/
|
|
158
|
+
function loadWuYaml(filePath) {
|
|
159
|
+
try {
|
|
160
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
161
|
+
const doc = parseYaml(content);
|
|
162
|
+
if (!doc || typeof doc !== 'object' || !doc.id) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
id: String(doc.id),
|
|
167
|
+
status: String(doc.status || 'ready'),
|
|
168
|
+
lane: String(doc.lane || 'Unknown'),
|
|
169
|
+
title: String(doc.title || 'Untitled'),
|
|
170
|
+
created: doc.created ? String(doc.created) : undefined,
|
|
171
|
+
claimed_at: doc.claimed_at ? String(doc.claimed_at) : undefined,
|
|
172
|
+
completed_at: doc.completed_at ? String(doc.completed_at) : undefined,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Run the state bootstrap migration
|
|
181
|
+
*/
|
|
182
|
+
export async function runStateBootstrap(args) {
|
|
183
|
+
const result = {
|
|
184
|
+
success: true,
|
|
185
|
+
eventsGenerated: 0,
|
|
186
|
+
eventsWritten: 0,
|
|
187
|
+
skipped: 0,
|
|
188
|
+
warnings: [],
|
|
189
|
+
};
|
|
190
|
+
// Check if WU directory exists
|
|
191
|
+
if (!existsSync(args.wuDir)) {
|
|
192
|
+
result.warnings.push('WU directory not found');
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
// Check if state file already exists
|
|
196
|
+
const stateFilePath = path.join(args.stateDir, 'wu-events.jsonl');
|
|
197
|
+
if (existsSync(stateFilePath) && !args.force && !args.dryRun) {
|
|
198
|
+
result.success = false;
|
|
199
|
+
result.error = `State file already exists: ${stateFilePath}. Use --force to overwrite.`;
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
// Load all WU YAML files
|
|
203
|
+
const wus = [];
|
|
204
|
+
const files = readdirSync(args.wuDir).filter((f) => f.endsWith('.yaml'));
|
|
205
|
+
for (const file of files) {
|
|
206
|
+
const filePath = path.join(args.wuDir, file);
|
|
207
|
+
const wu = loadWuYaml(filePath);
|
|
208
|
+
if (wu) {
|
|
209
|
+
wus.push(wu);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
result.skipped++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Generate events
|
|
216
|
+
const events = generateBootstrapEvents(wus);
|
|
217
|
+
result.eventsGenerated = events.length;
|
|
218
|
+
// In dry-run mode, don't write anything
|
|
219
|
+
if (args.dryRun) {
|
|
220
|
+
return result;
|
|
221
|
+
}
|
|
222
|
+
// Ensure state directory exists
|
|
223
|
+
mkdirSync(args.stateDir, { recursive: true });
|
|
224
|
+
// Write events to state file
|
|
225
|
+
const lines = events.map((event) => JSON.stringify(event));
|
|
226
|
+
const content = lines.length > 0 ? `${lines.join('\n')}\n` : '';
|
|
227
|
+
writeFileSync(stateFilePath, content, 'utf-8');
|
|
228
|
+
result.eventsWritten = events.length;
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Print help text
|
|
233
|
+
*/
|
|
234
|
+
export function printHelp() {
|
|
235
|
+
console.log(`
|
|
236
|
+
${LOG_PREFIX} State Bootstrap - One-time migration utility
|
|
237
|
+
|
|
238
|
+
Usage:
|
|
239
|
+
pnpm state:bootstrap # Dry-run mode (default, shows what would be done)
|
|
240
|
+
pnpm state:bootstrap --execute # Apply changes
|
|
241
|
+
|
|
242
|
+
Options:
|
|
243
|
+
--execute Execute migration (default is dry-run)
|
|
244
|
+
--dry-run Show what would be done without making changes
|
|
245
|
+
--wu-dir <path> WU YAML directory (default: ${STATE_BOOTSTRAP_DEFAULTS.wuDir})
|
|
246
|
+
--state-dir <path> State store directory (default: ${STATE_BOOTSTRAP_DEFAULTS.stateDir})
|
|
247
|
+
--force Overwrite existing state file
|
|
248
|
+
--help, -h Show this help message
|
|
249
|
+
|
|
250
|
+
This tool:
|
|
251
|
+
${EMOJI.SUCCESS} Reads all WU YAML files from the WU directory
|
|
252
|
+
${EMOJI.SUCCESS} Generates events based on WU status (claim, complete, block)
|
|
253
|
+
${EMOJI.SUCCESS} Writes events to .lumenflow/state/wu-events.jsonl
|
|
254
|
+
${EMOJI.WARNING} One-time migration - run only when setting up event-sourced state
|
|
255
|
+
|
|
256
|
+
Supported WU statuses:
|
|
257
|
+
ready -> No events (WU not yet claimed)
|
|
258
|
+
in_progress -> claim event
|
|
259
|
+
blocked -> claim + block events
|
|
260
|
+
done -> claim + complete events
|
|
261
|
+
`);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Main function
|
|
265
|
+
*/
|
|
266
|
+
async function main() {
|
|
267
|
+
const args = parseStateBootstrapArgs(process.argv);
|
|
268
|
+
if (args.help) {
|
|
269
|
+
printHelp();
|
|
270
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
271
|
+
}
|
|
272
|
+
console.log(`${LOG_PREFIX} State Bootstrap Migration`);
|
|
273
|
+
console.log(`${LOG_PREFIX} =========================${STRING_LITERALS.NEWLINE}`);
|
|
274
|
+
if (args.dryRun) {
|
|
275
|
+
console.log(`${LOG_PREFIX} ${EMOJI.INFO} DRY-RUN MODE (use --execute to apply changes)${STRING_LITERALS.NEWLINE}`);
|
|
276
|
+
}
|
|
277
|
+
console.log(`${LOG_PREFIX} WU directory: ${args.wuDir}`);
|
|
278
|
+
console.log(`${LOG_PREFIX} State directory: ${args.stateDir}${STRING_LITERALS.NEWLINE}`);
|
|
279
|
+
const result = await runStateBootstrap(args);
|
|
280
|
+
if (!result.success) {
|
|
281
|
+
console.error(`${LOG_PREFIX} ${EMOJI.FAILURE} ${result.error}`);
|
|
282
|
+
process.exit(EXIT_CODES.ERROR);
|
|
283
|
+
}
|
|
284
|
+
// Report warnings
|
|
285
|
+
for (const warning of result.warnings) {
|
|
286
|
+
console.log(`${LOG_PREFIX} ${EMOJI.WARNING} ${warning}`);
|
|
287
|
+
}
|
|
288
|
+
// Summary
|
|
289
|
+
console.log(`${STRING_LITERALS.NEWLINE}${LOG_PREFIX} Summary`);
|
|
290
|
+
console.log(`${LOG_PREFIX} ========`);
|
|
291
|
+
console.log(`${LOG_PREFIX} Events generated: ${result.eventsGenerated}`);
|
|
292
|
+
console.log(`${LOG_PREFIX} Events written: ${result.eventsWritten}`);
|
|
293
|
+
console.log(`${LOG_PREFIX} Files skipped: ${result.skipped}`);
|
|
294
|
+
if (args.dryRun && result.eventsGenerated > 0) {
|
|
295
|
+
console.log(`${STRING_LITERALS.NEWLINE}${LOG_PREFIX} ${EMOJI.INFO} This was a dry-run. Use --execute to apply changes.`);
|
|
296
|
+
}
|
|
297
|
+
else if (result.eventsWritten > 0) {
|
|
298
|
+
console.log(`${STRING_LITERALS.NEWLINE}${LOG_PREFIX} ${EMOJI.SUCCESS} State bootstrap complete!`);
|
|
299
|
+
}
|
|
300
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
301
|
+
}
|
|
302
|
+
// Guard main() for testability
|
|
303
|
+
import { fileURLToPath } from 'node:url';
|
|
304
|
+
import { runCLI } from './cli-entry-point.js';
|
|
305
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
306
|
+
runCLI(main);
|
|
307
|
+
}
|