@lumenflow/cli 2.9.0 → 2.11.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/README.md +23 -2
- package/dist/__tests__/gates-integration-tests.test.js +112 -0
- package/dist/__tests__/init.test.js +225 -0
- package/dist/__tests__/safe-git.test.js +4 -4
- package/dist/__tests__/wu-create-required-fields.test.js +22 -0
- package/dist/__tests__/wu-create.test.js +72 -0
- package/dist/gates.js +6 -8
- package/dist/hooks/enforcement-generator.js +256 -5
- package/dist/hooks/enforcement-sync.js +52 -6
- package/dist/init.js +195 -2
- package/dist/mem-recover.js +221 -0
- package/dist/orchestrate-initiative.js +19 -1
- package/dist/state-doctor-fix.js +36 -1
- package/dist/state-doctor.js +10 -6
- package/dist/wu-create.js +37 -15
- package/dist/wu-recover.js +53 -2
- package/dist/wu-spawn.js +2 -2
- package/package.json +6 -6
- package/templates/core/.mcp.json.template +8 -0
- package/templates/core/LUMENFLOW.md.template +24 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +47 -0
- package/templates/core/ai/onboarding/lumenflow-force-usage.md.template +183 -0
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +68 -55
- package/templates/core/ai/onboarding/release-process.md.template +58 -4
- package/templates/core/ai/onboarding/starting-prompt.md.template +67 -3
- package/templates/core/ai/onboarding/vendor-support.md.template +73 -0
- package/templates/core/scripts/safe-git.template +29 -0
- package/templates/vendors/claude/.claude/hooks/pre-compact-checkpoint.sh +102 -0
- package/templates/vendors/claude/.claude/hooks/session-start-recovery.sh +74 -0
- package/templates/vendors/claude/.claude/settings.json.template +42 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +23 -6
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Memory Recover CLI (WU-1390)
|
|
4
|
+
*
|
|
5
|
+
* Generate post-compaction recovery context for agents that have lost
|
|
6
|
+
* their LumenFlow instructions due to context compaction.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* pnpm mem:recover --wu WU-XXXX [options]
|
|
10
|
+
*
|
|
11
|
+
* Options:
|
|
12
|
+
* --max-size <bytes> Maximum output size in bytes (default: 2048)
|
|
13
|
+
* --format <json|human> Output format (default: human)
|
|
14
|
+
* --quiet Suppress header/footer output
|
|
15
|
+
*
|
|
16
|
+
* The recovery context includes:
|
|
17
|
+
* - Last checkpoint for the WU
|
|
18
|
+
* - Compact constraints (7 rules)
|
|
19
|
+
* - Essential CLI commands
|
|
20
|
+
* - Guidance to spawn fresh agent
|
|
21
|
+
*
|
|
22
|
+
* @see {@link packages/@lumenflow/memory/src/mem-recover-core.ts} - Core logic
|
|
23
|
+
* @see {@link packages/@lumenflow/memory/__tests__/mem-recover-core.test.ts} - Tests
|
|
24
|
+
*/
|
|
25
|
+
import fs from 'node:fs/promises';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { generateRecoveryContext } from '@lumenflow/memory/dist/mem-recover-core.js';
|
|
28
|
+
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
29
|
+
import { EXIT_CODES, LUMENFLOW_PATHS } from '@lumenflow/core/dist/wu-constants.js';
|
|
30
|
+
/**
|
|
31
|
+
* Log prefix for mem:recover output
|
|
32
|
+
*/
|
|
33
|
+
const LOG_PREFIX = '[mem:recover]';
|
|
34
|
+
/**
|
|
35
|
+
* Tool name for audit logging
|
|
36
|
+
*/
|
|
37
|
+
const TOOL_NAME = 'mem:recover';
|
|
38
|
+
/**
|
|
39
|
+
* Valid output formats
|
|
40
|
+
*/
|
|
41
|
+
const VALID_FORMATS = ['json', 'human'];
|
|
42
|
+
/**
|
|
43
|
+
* CLI argument options specific to mem:recover
|
|
44
|
+
*/
|
|
45
|
+
const CLI_OPTIONS = {
|
|
46
|
+
maxSize: {
|
|
47
|
+
name: 'maxSize',
|
|
48
|
+
flags: '-m, --max-size <bytes>',
|
|
49
|
+
description: 'Maximum output size in bytes (default: 2048)',
|
|
50
|
+
},
|
|
51
|
+
format: {
|
|
52
|
+
name: 'format',
|
|
53
|
+
flags: '-f, --format <format>',
|
|
54
|
+
description: 'Output format: json or human (default: human)',
|
|
55
|
+
},
|
|
56
|
+
baseDir: {
|
|
57
|
+
name: 'baseDir',
|
|
58
|
+
flags: '-d, --base-dir <path>',
|
|
59
|
+
description: 'Base directory (defaults to current directory)',
|
|
60
|
+
},
|
|
61
|
+
quiet: {
|
|
62
|
+
name: 'quiet',
|
|
63
|
+
flags: '-q, --quiet',
|
|
64
|
+
description: 'Suppress header/footer output, only show recovery context',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Write audit log entry for tool execution
|
|
69
|
+
*/
|
|
70
|
+
async function writeAuditLog(baseDir, entry) {
|
|
71
|
+
try {
|
|
72
|
+
const logPath = path.join(baseDir, LUMENFLOW_PATHS.AUDIT_LOG);
|
|
73
|
+
const logDir = path.dirname(logPath);
|
|
74
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
75
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
76
|
+
await fs.appendFile(logPath, line, 'utf-8');
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Audit logging is non-fatal - silently ignore errors
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Validate and parse a positive integer argument
|
|
84
|
+
*/
|
|
85
|
+
function parsePositiveInt(value, optionName) {
|
|
86
|
+
if (!value) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const parsed = parseInt(value, 10);
|
|
90
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
91
|
+
throw new Error(`Invalid ${optionName} value: "${value}". Must be a positive integer.`);
|
|
92
|
+
}
|
|
93
|
+
return parsed;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Validate format argument
|
|
97
|
+
*/
|
|
98
|
+
function validateFormat(format) {
|
|
99
|
+
if (!format) {
|
|
100
|
+
return 'human';
|
|
101
|
+
}
|
|
102
|
+
if (!VALID_FORMATS.includes(format)) {
|
|
103
|
+
throw new Error(`Invalid --format value: "${format}". Valid formats: ${VALID_FORMATS.join(', ')}`);
|
|
104
|
+
}
|
|
105
|
+
return format;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Print output in human-readable format
|
|
109
|
+
*/
|
|
110
|
+
function printHumanFormat(result, wuId, quiet) {
|
|
111
|
+
if (!quiet) {
|
|
112
|
+
console.log(`${LOG_PREFIX} Recovery context for ${wuId}:`);
|
|
113
|
+
console.log('');
|
|
114
|
+
}
|
|
115
|
+
console.log(result.context);
|
|
116
|
+
if (!quiet) {
|
|
117
|
+
console.log('');
|
|
118
|
+
console.log(`${LOG_PREFIX} ${result.size} bytes${result.truncated ? ' (truncated)' : ''}`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Print output in JSON format
|
|
123
|
+
*/
|
|
124
|
+
function printJsonFormat(result, wuId) {
|
|
125
|
+
const output = {
|
|
126
|
+
wuId,
|
|
127
|
+
context: result.context,
|
|
128
|
+
size: result.size,
|
|
129
|
+
truncated: result.truncated,
|
|
130
|
+
};
|
|
131
|
+
console.log(JSON.stringify(output, null, 2));
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Main CLI entry point
|
|
135
|
+
*/
|
|
136
|
+
async function main() {
|
|
137
|
+
const args = createWUParser({
|
|
138
|
+
name: 'mem-recover',
|
|
139
|
+
description: 'Generate post-compaction recovery context for agents',
|
|
140
|
+
options: [
|
|
141
|
+
WU_OPTIONS.wu,
|
|
142
|
+
CLI_OPTIONS.maxSize,
|
|
143
|
+
CLI_OPTIONS.format,
|
|
144
|
+
CLI_OPTIONS.baseDir,
|
|
145
|
+
CLI_OPTIONS.quiet,
|
|
146
|
+
],
|
|
147
|
+
required: ['wu'],
|
|
148
|
+
});
|
|
149
|
+
const baseDir = args.baseDir || process.cwd();
|
|
150
|
+
const startedAt = new Date().toISOString();
|
|
151
|
+
const startTime = Date.now();
|
|
152
|
+
let maxSize;
|
|
153
|
+
let format;
|
|
154
|
+
// Validate arguments
|
|
155
|
+
try {
|
|
156
|
+
maxSize = parsePositiveInt(args.maxSize, '--max-size');
|
|
157
|
+
format = validateFormat(args.format);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
const error = err;
|
|
161
|
+
console.error(`${LOG_PREFIX} Error: ${error.message}`);
|
|
162
|
+
process.exit(EXIT_CODES.ERROR);
|
|
163
|
+
}
|
|
164
|
+
let result;
|
|
165
|
+
let error = null;
|
|
166
|
+
try {
|
|
167
|
+
result = await generateRecoveryContext({
|
|
168
|
+
wuId: args.wu,
|
|
169
|
+
baseDir,
|
|
170
|
+
maxSize,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
const e = err;
|
|
175
|
+
error = e.message;
|
|
176
|
+
result = {
|
|
177
|
+
success: false,
|
|
178
|
+
context: '',
|
|
179
|
+
size: 0,
|
|
180
|
+
truncated: false,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const durationMs = Date.now() - startTime;
|
|
184
|
+
// Write audit log entry
|
|
185
|
+
await writeAuditLog(baseDir, {
|
|
186
|
+
tool: TOOL_NAME,
|
|
187
|
+
status: error ? 'failed' : 'success',
|
|
188
|
+
startedAt,
|
|
189
|
+
completedAt: new Date().toISOString(),
|
|
190
|
+
durationMs,
|
|
191
|
+
input: {
|
|
192
|
+
baseDir,
|
|
193
|
+
wuId: args.wu,
|
|
194
|
+
maxSize,
|
|
195
|
+
format,
|
|
196
|
+
quiet: args.quiet,
|
|
197
|
+
},
|
|
198
|
+
output: result.success
|
|
199
|
+
? {
|
|
200
|
+
contextSize: result.size,
|
|
201
|
+
truncated: result.truncated,
|
|
202
|
+
}
|
|
203
|
+
: null,
|
|
204
|
+
error: error ? { message: error } : null,
|
|
205
|
+
});
|
|
206
|
+
if (error) {
|
|
207
|
+
console.error(`${LOG_PREFIX} Error: ${error}`);
|
|
208
|
+
process.exit(EXIT_CODES.ERROR);
|
|
209
|
+
}
|
|
210
|
+
// Print output based on format
|
|
211
|
+
if (format === 'json') {
|
|
212
|
+
printJsonFormat(result, args.wu);
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
printHumanFormat(result, args.wu, !!args.quiet);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
main().catch((e) => {
|
|
219
|
+
console.error(`${LOG_PREFIX} ${e.message}`);
|
|
220
|
+
process.exit(EXIT_CODES.ERROR);
|
|
221
|
+
});
|
|
@@ -91,7 +91,25 @@ const program = new Command()
|
|
|
91
91
|
console.log(formatExecutionPlan(initiative, plan));
|
|
92
92
|
if (dryRun) {
|
|
93
93
|
console.log(chalk.yellow(`${LOG_PREFIX} Dry run mode - no agents spawned`));
|
|
94
|
-
console.log(
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log(chalk.bold('Next Steps (Recommended Defaults):'));
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log(chalk.cyan(' Option 1 (Recommended): Checkpoint-per-wave mode'));
|
|
98
|
+
console.log(' pnpm orchestrate:initiative -i ' + initIds[0] + ' -c');
|
|
99
|
+
console.log(' Best for: Large initiatives, context management, idempotent resumption');
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log(chalk.cyan(' Option 2: Full execution (polling mode)'));
|
|
102
|
+
console.log(' pnpm orchestrate:initiative -i ' + initIds[0]);
|
|
103
|
+
console.log(' Best for: Small initiatives (<4 WUs), quick execution');
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(chalk.cyan(' Option 3: Manual spawn per WU'));
|
|
106
|
+
console.log(' pnpm wu:spawn --id <WU-ID> --client claude-code');
|
|
107
|
+
console.log(' Best for: Testing, debugging, single WU execution');
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log(chalk.bold('Monitoring Commands:'));
|
|
110
|
+
console.log(' pnpm mem:inbox --since 10m # Check for signals from agents');
|
|
111
|
+
console.log(' pnpm orchestrate:init-status -i ' + initIds[0] + ' # Check progress');
|
|
112
|
+
console.log(' pnpm orchestrate:monitor # Live agent activity');
|
|
95
113
|
return;
|
|
96
114
|
}
|
|
97
115
|
// WU-1202: Output spawn XML for actual execution (not dry-run)
|
package/dist/state-doctor-fix.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* State Doctor Fix Operations (WU-1230)
|
|
2
|
+
* State Doctor Fix Operations (WU-1230, WU-1420)
|
|
3
3
|
*
|
|
4
4
|
* Provides fix dependencies for state:doctor --fix that use micro-worktree
|
|
5
5
|
* isolation for all tracked file changes. This ensures:
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* 2. Removal of stale WU references from backlog.md and status.md
|
|
9
9
|
* 3. All changes pushed via merge, not direct file modification
|
|
10
10
|
* 4. WU-1362: Retry logic for push failures (inherited from withMicroWorktree)
|
|
11
|
+
* 5. WU-1420: Emit corrective events to reconcile YAML vs state store mismatches
|
|
11
12
|
*
|
|
12
13
|
* Retry behavior is configured via .lumenflow.config.yaml git.push_retry section.
|
|
13
14
|
* Default: 3 retries with exponential backoff and jitter.
|
|
@@ -72,6 +73,9 @@ async function readFileSafe(filePath) {
|
|
|
72
73
|
* WU-1230: All file modifications happen in a micro-worktree and are pushed
|
|
73
74
|
* to origin/main via merge. This prevents direct modifications to local main.
|
|
74
75
|
*
|
|
76
|
+
* WU-1420: Includes emitEvent for fixing status mismatches by emitting
|
|
77
|
+
* corrective events (release, complete) to reconcile state store with YAML.
|
|
78
|
+
*
|
|
75
79
|
* @param baseDir - Project base directory
|
|
76
80
|
* @returns Partial StateDoctorDeps with fix operations
|
|
77
81
|
*/
|
|
@@ -190,5 +194,36 @@ export function createStateDoctorFixDeps(_baseDir) {
|
|
|
190
194
|
},
|
|
191
195
|
});
|
|
192
196
|
},
|
|
197
|
+
/**
|
|
198
|
+
* Emit a corrective event to fix status mismatch (WU-1420)
|
|
199
|
+
*
|
|
200
|
+
* Appends a release or complete event to wu-events.jsonl to reconcile
|
|
201
|
+
* the state store with the WU YAML status.
|
|
202
|
+
*/
|
|
203
|
+
emitEvent: async (event) => {
|
|
204
|
+
await withMicroWorktree({
|
|
205
|
+
operation: OPERATION_NAME,
|
|
206
|
+
id: `emit-event-${event.wuId.toLowerCase()}-${event.type}`,
|
|
207
|
+
logPrefix: LOG_PREFIX,
|
|
208
|
+
pushOnly: true,
|
|
209
|
+
execute: async ({ worktreePath }) => {
|
|
210
|
+
const eventsPath = path.join(worktreePath, WU_EVENTS_FILE);
|
|
211
|
+
// Build the event object
|
|
212
|
+
const eventLine = JSON.stringify({
|
|
213
|
+
wuId: event.wuId,
|
|
214
|
+
type: event.type,
|
|
215
|
+
reason: event.reason,
|
|
216
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
217
|
+
});
|
|
218
|
+
// Append to events file (create if needed)
|
|
219
|
+
await fs.mkdir(path.dirname(eventsPath), { recursive: true });
|
|
220
|
+
await fs.appendFile(eventsPath, eventLine + '\n', 'utf-8');
|
|
221
|
+
return {
|
|
222
|
+
commitMessage: `fix(state-doctor): emit ${event.type} event for ${event.wuId} to reconcile state`,
|
|
223
|
+
files: [WU_EVENTS_FILE],
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
},
|
|
193
228
|
};
|
|
194
229
|
}
|
package/dist/state-doctor.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* State Doctor CLI (WU-1209)
|
|
3
|
+
* State Doctor CLI (WU-1209, WU-1420)
|
|
4
4
|
*
|
|
5
5
|
* Integrity checker for LumenFlow state that detects:
|
|
6
6
|
* - Orphaned WUs (done status but no stamp)
|
|
7
7
|
* - Dangling signals (reference non-existent WUs)
|
|
8
8
|
* - Broken memory relationships (events for missing WU specs)
|
|
9
|
+
* - Status mismatches between WU YAML and state store (WU-1420)
|
|
9
10
|
*
|
|
10
11
|
* Inspired by Beads bd doctor command.
|
|
11
12
|
*
|
|
@@ -171,7 +172,7 @@ async function createDeps(baseDir) {
|
|
|
171
172
|
*/
|
|
172
173
|
listStamps: async () => {
|
|
173
174
|
try {
|
|
174
|
-
const stampsDir = path.join(baseDir, config.
|
|
175
|
+
const stampsDir = path.join(baseDir, config.state.stampsDir);
|
|
175
176
|
const stampFiles = await fg('WU-*.done', { cwd: stampsDir });
|
|
176
177
|
return stampFiles.map((file) => file.replace('.done', ''));
|
|
177
178
|
}
|
|
@@ -321,6 +322,8 @@ function getIssueTypeLabel(type) {
|
|
|
321
322
|
return 'Dangling Signal';
|
|
322
323
|
case ISSUE_TYPES.BROKEN_EVENT:
|
|
323
324
|
return 'Broken Event';
|
|
325
|
+
case ISSUE_TYPES.STATUS_MISMATCH:
|
|
326
|
+
return 'Status Mismatch';
|
|
324
327
|
default:
|
|
325
328
|
return type;
|
|
326
329
|
}
|
|
@@ -330,10 +333,11 @@ function getIssueTypeLabel(type) {
|
|
|
330
333
|
*/
|
|
331
334
|
function printSummary(result) {
|
|
332
335
|
console.log('=== Summary ===');
|
|
333
|
-
console.log(` Orphaned WUs:
|
|
334
|
-
console.log(` Dangling Signals:
|
|
335
|
-
console.log(` Broken Events:
|
|
336
|
-
console.log(`
|
|
336
|
+
console.log(` Orphaned WUs: ${result.summary.orphanedWUs}`);
|
|
337
|
+
console.log(` Dangling Signals: ${result.summary.danglingSignals}`);
|
|
338
|
+
console.log(` Broken Events: ${result.summary.brokenEvents}`);
|
|
339
|
+
console.log(` Status Mismatches: ${result.summary.statusMismatches}`);
|
|
340
|
+
console.log(` Total Issues: ${result.summary.totalIssues}`);
|
|
337
341
|
}
|
|
338
342
|
/**
|
|
339
343
|
* Print fixed issues section
|
package/dist/wu-create.js
CHANGED
|
@@ -43,7 +43,7 @@ import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
|
43
43
|
import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
|
|
44
44
|
import { validateWU } from '@lumenflow/core/dist/wu-schema.js';
|
|
45
45
|
import { getPlanPath, getPlanProtocolRef, getPlansDir, } from '@lumenflow/core/dist/lumenflow-home.js';
|
|
46
|
-
import { validateSpecRefs } from '@lumenflow/core/dist/wu-create-validators.js';
|
|
46
|
+
import { hasSpecRefs, validateSpecRefs } from '@lumenflow/core/dist/wu-create-validators.js';
|
|
47
47
|
import { COMMIT_FORMATS, FILE_SYSTEM, READINESS_UI, STRING_LITERALS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
48
48
|
// WU-1593: Use centralized validateWUIDFormat (DRY)
|
|
49
49
|
import { ensureOnMain, validateWUIDFormat } from '@lumenflow/core/dist/wu-helpers.js';
|
|
@@ -114,6 +114,21 @@ export function warnIfBetterLaneExists(providedLane, codePathsArray, title, desc
|
|
|
114
114
|
// Non-blocking - if inference fails, continue silently
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
+
export function collectInitiativeWarnings({ initiativeId, initiativeDoc, phase, specRefs, }) {
|
|
118
|
+
const warnings = [];
|
|
119
|
+
const phaseCheck = checkInitiativePhases(initiativeDoc);
|
|
120
|
+
if (!phaseCheck.hasPhases && phaseCheck.warning) {
|
|
121
|
+
warnings.push(phaseCheck.warning);
|
|
122
|
+
}
|
|
123
|
+
if (phaseCheck.hasPhases && !phase) {
|
|
124
|
+
warnings.push(`Initiative ${initiativeId} has phases defined. Consider adding --phase to link this WU to a phase.`);
|
|
125
|
+
}
|
|
126
|
+
const relatedPlan = initiativeDoc.related_plan;
|
|
127
|
+
if (relatedPlan && !hasSpecRefs(specRefs)) {
|
|
128
|
+
warnings.push(`Initiative ${initiativeId} has related_plan (${relatedPlan}). Consider adding --spec-refs to link this WU to the plan.`);
|
|
129
|
+
}
|
|
130
|
+
return warnings;
|
|
131
|
+
}
|
|
117
132
|
/**
|
|
118
133
|
* Check if WU already exists
|
|
119
134
|
* @param {string} id - WU ID to check
|
|
@@ -211,8 +226,8 @@ function createPlanTemplate(wuId, title) {
|
|
|
211
226
|
console.log(`${LOG_PREFIX} ✅ Created plan template: ${planPath}`);
|
|
212
227
|
return planPath;
|
|
213
228
|
}
|
|
214
|
-
function buildWUContent({ id, lane, title, priority, type, created, opts, }) {
|
|
215
|
-
const { description, acceptance, codePaths, testPathsManual, testPathsUnit, testPathsE2e, initiative, phase, blockedBy, blocks, labels, assignedTo, exposure, userJourney, uiPairingWus, specRefs, } = opts;
|
|
229
|
+
export function buildWUContent({ id, lane, title, priority, type, created, opts, }) {
|
|
230
|
+
const { description, acceptance, notes, codePaths, testPathsManual, testPathsUnit, testPathsE2e, initiative, phase, blockedBy, blocks, labels, assignedTo, exposure, userJourney, uiPairingWus, specRefs, } = opts;
|
|
216
231
|
// Arrays come directly from Commander.js repeatable options - no parsing needed
|
|
217
232
|
const code_paths = codePaths ?? [];
|
|
218
233
|
const tests = {
|
|
@@ -235,7 +250,7 @@ function buildWUContent({ id, lane, title, priority, type, created, opts, }) {
|
|
|
235
250
|
artifacts: [`.lumenflow/stamps/${id}.done`],
|
|
236
251
|
dependencies: [],
|
|
237
252
|
risks: [],
|
|
238
|
-
notes: '',
|
|
253
|
+
notes: notes ?? '',
|
|
239
254
|
requires_review: false,
|
|
240
255
|
...(initiative && { initiative }),
|
|
241
256
|
...(phase && { phase: parseInt(phase, 10) }),
|
|
@@ -285,7 +300,7 @@ export function validateCreateSpec({ id, lane, title, priority, type, opts, }) {
|
|
|
285
300
|
errors.push('At least one test path flag is required (--test-paths-manual, --test-paths-unit, or --test-paths-e2e)');
|
|
286
301
|
}
|
|
287
302
|
}
|
|
288
|
-
if (effectiveType === 'feature' && !opts.specRefs) {
|
|
303
|
+
if (effectiveType === 'feature' && !hasSpecRefs(opts.specRefs)) {
|
|
289
304
|
errors.push('--spec-refs is required for type: feature WUs\n' +
|
|
290
305
|
' Tip: Create a plan first with: pnpm plan:create --id <WU-ID> --title "..."\n' +
|
|
291
306
|
' Then use --plan flag or --spec-refs lumenflow://plans/<WU-ID>-plan.md');
|
|
@@ -511,6 +526,7 @@ async function main() {
|
|
|
511
526
|
// WU-1364: Full spec inline options
|
|
512
527
|
WU_OPTIONS.description,
|
|
513
528
|
WU_OPTIONS.acceptance,
|
|
529
|
+
WU_OPTIONS.notes,
|
|
514
530
|
WU_OPTIONS.codePaths,
|
|
515
531
|
WU_OPTIONS.testPathsManual,
|
|
516
532
|
WU_OPTIONS.testPathsUnit,
|
|
@@ -588,6 +604,7 @@ async function main() {
|
|
|
588
604
|
opts: {
|
|
589
605
|
description: args.description,
|
|
590
606
|
acceptance: args.acceptance,
|
|
607
|
+
notes: args.notes,
|
|
591
608
|
codePaths: args.codePaths,
|
|
592
609
|
testPathsManual: args.testPathsManual,
|
|
593
610
|
testPathsUnit: args.testPathsUnit,
|
|
@@ -613,16 +630,6 @@ async function main() {
|
|
|
613
630
|
die(`${LOG_PREFIX} ❌ Spec validation failed:\n\n${errorList}`);
|
|
614
631
|
}
|
|
615
632
|
console.log(`${LOG_PREFIX} ✅ Spec validation passed`);
|
|
616
|
-
// WU-1211: Warn if linking to initiative with no phases defined
|
|
617
|
-
if (args.initiative) {
|
|
618
|
-
const initiative = findInitiative(args.initiative);
|
|
619
|
-
if (initiative) {
|
|
620
|
-
const phaseCheck = checkInitiativePhases(initiative.doc);
|
|
621
|
-
if (!phaseCheck.hasPhases && phaseCheck.warning) {
|
|
622
|
-
console.warn(`${LOG_PREFIX} ⚠️ ${phaseCheck.warning}`);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
633
|
const specRefsList = mergedSpecRefs;
|
|
627
634
|
const specRefsValidation = validateSpecRefs(specRefsList);
|
|
628
635
|
if (!specRefsValidation.valid) {
|
|
@@ -636,6 +643,20 @@ async function main() {
|
|
|
636
643
|
console.warn(`${LOG_PREFIX} ⚠️ ${warning}`);
|
|
637
644
|
}
|
|
638
645
|
}
|
|
646
|
+
if (args.initiative) {
|
|
647
|
+
const initiative = findInitiative(args.initiative);
|
|
648
|
+
if (initiative) {
|
|
649
|
+
const warnings = collectInitiativeWarnings({
|
|
650
|
+
initiativeId: initiative.id,
|
|
651
|
+
initiativeDoc: initiative.doc,
|
|
652
|
+
phase: args.phase,
|
|
653
|
+
specRefs: specRefsList,
|
|
654
|
+
});
|
|
655
|
+
for (const warning of warnings) {
|
|
656
|
+
console.warn(`${LOG_PREFIX} ⚠️ ${warning}`);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
639
660
|
if (args.plan) {
|
|
640
661
|
createPlanTemplate(wuId, args.title);
|
|
641
662
|
}
|
|
@@ -664,6 +685,7 @@ async function main() {
|
|
|
664
685
|
// WU-1364: Full spec inline options
|
|
665
686
|
description: args.description,
|
|
666
687
|
acceptance: args.acceptance,
|
|
688
|
+
notes: args.notes,
|
|
667
689
|
codePaths: args.codePaths,
|
|
668
690
|
testPathsManual: args.testPathsManual,
|
|
669
691
|
testPathsUnit: args.testPathsUnit,
|
package/dist/wu-recover.js
CHANGED
|
@@ -15,16 +15,19 @@
|
|
|
15
15
|
* pnpm wu:recover --id WU-123 --action resume # Apply fix
|
|
16
16
|
* pnpm wu:recover --id WU-123 --action nuke --force # Destructive
|
|
17
17
|
*/
|
|
18
|
-
import { existsSync, rmSync } from 'node:fs';
|
|
18
|
+
import { existsSync, rmSync, writeFileSync } from 'node:fs';
|
|
19
19
|
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
20
20
|
import { computeContext } from '@lumenflow/core/dist/context/index.js';
|
|
21
21
|
import { analyzeRecovery, } from '@lumenflow/core/dist/recovery/recovery-analyzer.js';
|
|
22
22
|
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
23
23
|
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
24
24
|
import { readWU, writeWU } from '@lumenflow/core/dist/wu-yaml.js';
|
|
25
|
-
import { CONTEXT_VALIDATION, EMOJI, WU_STATUS, DEFAULTS, toKebab, } from '@lumenflow/core/dist/wu-constants.js';
|
|
25
|
+
import { CONTEXT_VALIDATION, EMOJI, WU_STATUS, DEFAULTS, toKebab, FILE_SYSTEM, } from '@lumenflow/core/dist/wu-constants.js';
|
|
26
26
|
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
27
27
|
import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
|
|
28
|
+
import { WUStateStore } from '@lumenflow/core/dist/wu-state-store.js';
|
|
29
|
+
import { generateBacklog, generateStatus } from '@lumenflow/core/dist/backlog-generator.js';
|
|
30
|
+
import { releaseLaneLock } from '@lumenflow/core/dist/lane-lock.js';
|
|
28
31
|
import { join, relative } from 'node:path';
|
|
29
32
|
const { RECOVERY_ACTIONS } = CONTEXT_VALIDATION;
|
|
30
33
|
const LOG_PREFIX = '[wu:recover]';
|
|
@@ -159,6 +162,7 @@ async function executeResume(wuId) {
|
|
|
159
162
|
* Execute reset action - discard worktree and reset to ready
|
|
160
163
|
*
|
|
161
164
|
* WU-1226: Uses micro-worktree isolation for WU YAML state changes.
|
|
165
|
+
* WU-1419: Emits release event to state store so WU can be re-claimed.
|
|
162
166
|
* Worktree removal still happens directly (git operation, not file write).
|
|
163
167
|
* Changes are pushed via merge, not direct file modification on main.
|
|
164
168
|
*/
|
|
@@ -171,6 +175,7 @@ async function executeReset(wuId) {
|
|
|
171
175
|
}
|
|
172
176
|
const doc = readWU(wuPath, wuId);
|
|
173
177
|
const worktreePath = getWorktreePath(wuId, doc.lane || '');
|
|
178
|
+
const lane = doc.lane || '';
|
|
174
179
|
// Remove worktree if exists (git operation, safe to do directly)
|
|
175
180
|
// WU-1097: Use worktreeRemove() instead of deprecated run() with shell strings
|
|
176
181
|
// This properly handles paths with spaces and special characters
|
|
@@ -193,6 +198,7 @@ async function executeReset(wuId) {
|
|
|
193
198
|
}
|
|
194
199
|
}
|
|
195
200
|
// WU-1226: Use micro-worktree isolation for WU YAML state changes
|
|
201
|
+
// WU-1419: Also emit release event to state store
|
|
196
202
|
try {
|
|
197
203
|
await withMicroWorktree({
|
|
198
204
|
operation: OPERATION_NAME,
|
|
@@ -211,12 +217,57 @@ async function executeReset(wuId) {
|
|
|
211
217
|
Reflect.deleteProperty(microDoc, 'session_id');
|
|
212
218
|
Reflect.deleteProperty(microDoc, 'baseline_main_sha');
|
|
213
219
|
writeWU(microWuPath, microDoc);
|
|
220
|
+
// WU-1419: Emit release event to state store so re-claiming works
|
|
221
|
+
// Without this, state store still thinks WU is in_progress, blocking re-claim
|
|
222
|
+
const stateDir = join(microPath, '.lumenflow', 'state');
|
|
223
|
+
const store = new WUStateStore(stateDir);
|
|
224
|
+
await store.load();
|
|
225
|
+
// Only emit release event if WU is currently in_progress in state store
|
|
226
|
+
const currentState = store.getWUState(wuId);
|
|
227
|
+
if (currentState && currentState.status === 'in_progress') {
|
|
228
|
+
await store.release(wuId, 'Reset via wu:recover --action reset');
|
|
229
|
+
console.log(`${LOG_PREFIX} Emitted release event to state store`);
|
|
230
|
+
// Regenerate backlog.md and status.md from state store
|
|
231
|
+
const microBacklogPath = join(microPath, WU_PATHS.BACKLOG());
|
|
232
|
+
const microStatusPath = join(microPath, WU_PATHS.STATUS());
|
|
233
|
+
const backlogContent = await generateBacklog(store);
|
|
234
|
+
writeFileSync(microBacklogPath, backlogContent, {
|
|
235
|
+
encoding: FILE_SYSTEM.UTF8,
|
|
236
|
+
});
|
|
237
|
+
const statusContent = await generateStatus(store);
|
|
238
|
+
writeFileSync(microStatusPath, statusContent, {
|
|
239
|
+
encoding: FILE_SYSTEM.UTF8,
|
|
240
|
+
});
|
|
241
|
+
return {
|
|
242
|
+
commitMessage: `fix(wu-recover): reset ${wuId} - clear claim and emit release event`,
|
|
243
|
+
files: [
|
|
244
|
+
relative(process.cwd(), wuPath),
|
|
245
|
+
WU_PATHS.STATUS(),
|
|
246
|
+
WU_PATHS.BACKLOG(),
|
|
247
|
+
'.lumenflow/state/wu-events.jsonl',
|
|
248
|
+
],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// WU not in state store as in_progress, just update YAML
|
|
214
252
|
return {
|
|
215
253
|
commitMessage: `fix(wu-recover): reset ${wuId} - clear claim and set status to ready`,
|
|
216
254
|
files: [relative(process.cwd(), wuPath)],
|
|
217
255
|
};
|
|
218
256
|
},
|
|
219
257
|
});
|
|
258
|
+
// Release lane lock so another WU can be claimed
|
|
259
|
+
if (lane) {
|
|
260
|
+
try {
|
|
261
|
+
const releaseResult = releaseLaneLock(lane, { wuId });
|
|
262
|
+
if (releaseResult.released && !releaseResult.notFound) {
|
|
263
|
+
console.log(`${LOG_PREFIX} Lane lock released for "${lane}"`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
// Non-blocking: lock release failure should not block the reset operation
|
|
268
|
+
console.warn(`${LOG_PREFIX} Warning: Could not release lane lock: ${err.message}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
220
271
|
console.log(`${LOG_PREFIX} ${EMOJI.SUCCESS} Reset completed - ${wuId} is now ready for re-claiming`);
|
|
221
272
|
return true;
|
|
222
273
|
}
|
package/dist/wu-spawn.js
CHANGED
|
@@ -630,10 +630,10 @@ When creating \`.lumenflow/\` stamps or other artifacts:
|
|
|
630
630
|
# CORRECT: Create stamp in worktree
|
|
631
631
|
WORKTREE_ROOT=$(git rev-parse --show-toplevel)
|
|
632
632
|
mkdir -p "$WORKTREE_ROOT/.lumenflow/agent-runs"
|
|
633
|
-
touch "$WORKTREE_ROOT/.lumenflow/agent-runs/
|
|
633
|
+
touch "$WORKTREE_ROOT/.lumenflow/agent-runs/code-reviewer.stamp"
|
|
634
634
|
|
|
635
635
|
# WRONG: Hardcoded path to main
|
|
636
|
-
# touch /path/to/main/.lumenflow/agent-runs/
|
|
636
|
+
# touch /path/to/main/.lumenflow/agent-runs/code-reviewer.stamp
|
|
637
637
|
\`\`\`
|
|
638
638
|
|
|
639
639
|
### Why This Matters
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumenflow/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "Command-line interface for LumenFlow workflow framework",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lumenflow",
|
|
@@ -151,11 +151,11 @@
|
|
|
151
151
|
"pretty-ms": "^9.2.0",
|
|
152
152
|
"simple-git": "^3.30.0",
|
|
153
153
|
"yaml": "^2.8.2",
|
|
154
|
-
"@lumenflow/core": "2.
|
|
155
|
-
"@lumenflow/metrics": "2.
|
|
156
|
-
"@lumenflow/memory": "2.
|
|
157
|
-
"@lumenflow/
|
|
158
|
-
"@lumenflow/
|
|
154
|
+
"@lumenflow/core": "2.11.0",
|
|
155
|
+
"@lumenflow/metrics": "2.11.0",
|
|
156
|
+
"@lumenflow/memory": "2.11.0",
|
|
157
|
+
"@lumenflow/initiatives": "2.11.0",
|
|
158
|
+
"@lumenflow/agent": "2.11.0"
|
|
159
159
|
},
|
|
160
160
|
"devDependencies": {
|
|
161
161
|
"@vitest/coverage-v8": "^4.0.17",
|
|
@@ -53,6 +53,30 @@ cd /path/to/main && pnpm wu:done --id WU-XXXX
|
|
|
53
53
|
|
|
54
54
|
---
|
|
55
55
|
|
|
56
|
+
## When to Use Initiatives
|
|
57
|
+
|
|
58
|
+
Use **Initiatives** for multi-phase work spanning multiple WUs:
|
|
59
|
+
|
|
60
|
+
- **Product visions**: "Build a task management app"
|
|
61
|
+
- **Larger features**: Work requiring multiple WUs across lanes
|
|
62
|
+
- **Complex projects**: Anything that needs phased delivery
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# Create an initiative for multi-phase work
|
|
66
|
+
pnpm initiative:create --id INIT-001 --title "Feature Name" \
|
|
67
|
+
--description "..." --phase "Phase 1: MVP" --phase "Phase 2: Polish"
|
|
68
|
+
|
|
69
|
+
# Add WUs to the initiative
|
|
70
|
+
pnpm initiative:add-wu --initiative INIT-001 --wu WU-XXX --phase 1
|
|
71
|
+
|
|
72
|
+
# Track progress
|
|
73
|
+
pnpm initiative:status --id INIT-001
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**Skip initiatives** for: single-file bug fixes, small docs updates, isolated refactoring.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
56
80
|
## Setup Notes (Common First-Run Failures)
|
|
57
81
|
|
|
58
82
|
### Lane inference (sub-lanes)
|