@lumenflow/cli 2.4.0 → 2.5.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__/init-config-lanes.test.js +131 -0
- package/dist/__tests__/init-docs-structure.test.js +119 -0
- package/dist/__tests__/init-lane-inference.test.js +125 -0
- package/dist/__tests__/init-onboarding-docs.test.js +132 -0
- package/dist/__tests__/init-quick-ref.test.js +145 -0
- package/dist/__tests__/init-scripts.test.js +96 -0
- package/dist/__tests__/init-template-portability.test.js +97 -0
- package/dist/__tests__/init.test.js +7 -2
- package/dist/__tests__/initiative-add-wu.test.js +420 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +162 -0
- package/dist/__tests__/initiative-remove-wu.test.js +458 -0
- package/dist/__tests__/onboarding-smoke-test.test.js +211 -0
- package/dist/__tests__/path-centralization-cli.test.js +234 -0
- package/dist/__tests__/plan-create.test.js +126 -0
- package/dist/__tests__/plan-edit.test.js +157 -0
- package/dist/__tests__/plan-link.test.js +239 -0
- package/dist/__tests__/plan-promote.test.js +181 -0
- package/dist/__tests__/wu-create-strict.test.js +118 -0
- package/dist/__tests__/wu-edit-strict.test.js +109 -0
- package/dist/__tests__/wu-validate-strict.test.js +113 -0
- package/dist/flow-bottlenecks.js +4 -2
- package/dist/gates.js +22 -0
- package/dist/init.js +580 -87
- package/dist/initiative-add-wu.js +112 -16
- package/dist/initiative-remove-wu.js +248 -0
- package/dist/onboarding-smoke-test.js +400 -0
- package/dist/plan-create.js +199 -0
- package/dist/plan-edit.js +235 -0
- package/dist/plan-link.js +233 -0
- package/dist/plan-promote.js +231 -0
- package/dist/wu-block.js +16 -5
- package/dist/wu-claim.js +15 -9
- package/dist/wu-create.js +50 -2
- package/dist/wu-deps.js +3 -1
- package/dist/wu-done.js +14 -5
- package/dist/wu-edit.js +35 -0
- package/dist/wu-spawn.js +8 -0
- package/dist/wu-unblock.js +34 -2
- package/dist/wu-validate.js +25 -17
- package/package.json +10 -6
- package/templates/core/AGENTS.md.template +2 -2
- package/dist/__tests__/init-plan.test.js +0 -340
- package/dist/agent-issues-query.d.ts +0 -16
- package/dist/agent-log-issue.d.ts +0 -10
- package/dist/agent-session-end.d.ts +0 -10
- package/dist/agent-session.d.ts +0 -10
- package/dist/backlog-prune.d.ts +0 -84
- package/dist/cli-entry-point.d.ts +0 -8
- package/dist/deps-add.d.ts +0 -91
- package/dist/deps-remove.d.ts +0 -17
- package/dist/docs-sync.d.ts +0 -50
- package/dist/file-delete.d.ts +0 -84
- package/dist/file-edit.d.ts +0 -82
- package/dist/file-read.d.ts +0 -92
- package/dist/file-write.d.ts +0 -90
- package/dist/flow-bottlenecks.d.ts +0 -16
- package/dist/flow-report.d.ts +0 -16
- package/dist/gates.d.ts +0 -94
- package/dist/git-branch.d.ts +0 -65
- package/dist/git-diff.d.ts +0 -58
- package/dist/git-log.d.ts +0 -69
- package/dist/git-status.d.ts +0 -58
- package/dist/guard-locked.d.ts +0 -62
- package/dist/guard-main-branch.d.ts +0 -50
- package/dist/guard-worktree-commit.d.ts +0 -59
- package/dist/index.d.ts +0 -10
- package/dist/init-plan.d.ts +0 -80
- package/dist/init-plan.js +0 -337
- package/dist/init.d.ts +0 -46
- package/dist/initiative-add-wu.d.ts +0 -22
- package/dist/initiative-bulk-assign-wus.d.ts +0 -16
- package/dist/initiative-create.d.ts +0 -28
- package/dist/initiative-edit.d.ts +0 -34
- package/dist/initiative-list.d.ts +0 -12
- package/dist/initiative-status.d.ts +0 -11
- package/dist/lumenflow-upgrade.d.ts +0 -103
- package/dist/mem-checkpoint.d.ts +0 -16
- package/dist/mem-cleanup.d.ts +0 -29
- package/dist/mem-create.d.ts +0 -17
- package/dist/mem-export.d.ts +0 -10
- package/dist/mem-inbox.d.ts +0 -35
- package/dist/mem-init.d.ts +0 -15
- package/dist/mem-ready.d.ts +0 -16
- package/dist/mem-signal.d.ts +0 -16
- package/dist/mem-start.d.ts +0 -16
- package/dist/mem-summarize.d.ts +0 -22
- package/dist/mem-triage.d.ts +0 -22
- package/dist/metrics-cli.d.ts +0 -90
- package/dist/metrics-snapshot.d.ts +0 -18
- package/dist/orchestrate-init-status.d.ts +0 -11
- package/dist/orchestrate-initiative.d.ts +0 -12
- package/dist/orchestrate-monitor.d.ts +0 -11
- package/dist/release.d.ts +0 -117
- package/dist/rotate-progress.d.ts +0 -48
- package/dist/session-coordinator.d.ts +0 -74
- package/dist/spawn-list.d.ts +0 -16
- package/dist/state-bootstrap.d.ts +0 -92
- package/dist/sync-templates.d.ts +0 -52
- package/dist/trace-gen.d.ts +0 -84
- package/dist/validate-agent-skills.d.ts +0 -50
- package/dist/validate-agent-sync.d.ts +0 -36
- package/dist/validate-backlog-sync.d.ts +0 -37
- package/dist/validate-skills-spec.d.ts +0 -40
- package/dist/validate.d.ts +0 -60
- package/dist/wu-block.d.ts +0 -16
- package/dist/wu-claim.d.ts +0 -74
- package/dist/wu-cleanup.d.ts +0 -35
- package/dist/wu-create.d.ts +0 -69
- package/dist/wu-delete.d.ts +0 -21
- package/dist/wu-deps.d.ts +0 -13
- package/dist/wu-done.d.ts +0 -225
- package/dist/wu-edit.d.ts +0 -63
- package/dist/wu-infer-lane.d.ts +0 -17
- package/dist/wu-preflight.d.ts +0 -47
- package/dist/wu-prune.d.ts +0 -16
- package/dist/wu-recover.d.ts +0 -37
- package/dist/wu-release.d.ts +0 -19
- package/dist/wu-repair.d.ts +0 -60
- package/dist/wu-spawn-completion.d.ts +0 -10
- package/dist/wu-spawn.d.ts +0 -192
- package/dist/wu-status.d.ts +0 -25
- package/dist/wu-unblock.d.ts +0 -16
- package/dist/wu-unlock-lane.d.ts +0 -19
- package/dist/wu-validate.d.ts +0 -16
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file onboarding-smoke-test.ts
|
|
3
|
+
* Onboarding smoke-test gate for lumenflow init + wu:create flows (WU-1315)
|
|
4
|
+
*
|
|
5
|
+
* This gate creates a temp repo, runs lumenflow init --full, validates:
|
|
6
|
+
* - Injected package.json scripts use standalone binary format
|
|
7
|
+
* - Lane-inference.yaml uses hierarchical format (not flat lanes array)
|
|
8
|
+
* - wu:create works with requireRemote=false
|
|
9
|
+
*
|
|
10
|
+
* Used as part of the gates pipeline to catch regressions before release.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from 'node:fs';
|
|
13
|
+
import * as path from 'node:path';
|
|
14
|
+
import * as os from 'node:os';
|
|
15
|
+
import * as yaml from 'yaml';
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import { scaffoldProject } from './init.js';
|
|
18
|
+
/** Package.json file name constant */
|
|
19
|
+
const PACKAGE_JSON_FILE = 'package.json';
|
|
20
|
+
/** Lane inference file name constant */
|
|
21
|
+
const LANE_INFERENCE_FILE = '.lumenflow.lane-inference.yaml';
|
|
22
|
+
/** LumenFlow config file name constant */
|
|
23
|
+
const LUMENFLOW_CONFIG_FILE = '.lumenflow.config.yaml';
|
|
24
|
+
/** Git binary path - uses system PATH which is acceptable for smoke tests */
|
|
25
|
+
const GIT_BINARY = 'git';
|
|
26
|
+
/** Required package.json scripts from LumenFlow init */
|
|
27
|
+
const REQUIRED_SCRIPTS = ['wu:claim', 'wu:done', 'wu:create', 'gates'];
|
|
28
|
+
/**
|
|
29
|
+
* Validate that package.json has the required LumenFlow scripts
|
|
30
|
+
* in the correct standalone binary format.
|
|
31
|
+
*
|
|
32
|
+
* @param options - Validation options
|
|
33
|
+
* @returns Validation result
|
|
34
|
+
*/
|
|
35
|
+
export function validateInitScripts(options) {
|
|
36
|
+
const { projectDir } = options;
|
|
37
|
+
const packageJsonPath = path.join(projectDir, PACKAGE_JSON_FILE);
|
|
38
|
+
// Check if package.json exists
|
|
39
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
40
|
+
return {
|
|
41
|
+
valid: false,
|
|
42
|
+
missingScripts: [],
|
|
43
|
+
invalidScripts: [],
|
|
44
|
+
error: `${PACKAGE_JSON_FILE} not found in ${projectDir}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
let packageJson;
|
|
48
|
+
try {
|
|
49
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return {
|
|
53
|
+
valid: false,
|
|
54
|
+
missingScripts: [],
|
|
55
|
+
invalidScripts: [],
|
|
56
|
+
error: `Failed to parse ${PACKAGE_JSON_FILE}: ${err instanceof Error ? err.message : String(err)}`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const scripts = packageJson.scripts ?? {};
|
|
60
|
+
const missingScripts = [];
|
|
61
|
+
const invalidScripts = [];
|
|
62
|
+
for (const script of REQUIRED_SCRIPTS) {
|
|
63
|
+
if (!scripts[script]) {
|
|
64
|
+
missingScripts.push(script);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Validate script uses standalone binary format
|
|
68
|
+
// Should be 'wu-claim' or 'gates', not 'pnpm exec lumenflow wu:claim'
|
|
69
|
+
const value = scripts[script];
|
|
70
|
+
if (value.includes('pnpm exec') || value.includes('npx lumenflow')) {
|
|
71
|
+
invalidScripts.push(script);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
valid: missingScripts.length === 0 && invalidScripts.length === 0,
|
|
77
|
+
missingScripts,
|
|
78
|
+
invalidScripts,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Validate a single parent lane and its sub-lanes
|
|
83
|
+
*/
|
|
84
|
+
function validateParentLane(parentLane, subLanes, errors) {
|
|
85
|
+
// Skip comment-only keys or non-object values
|
|
86
|
+
if (typeof subLanes !== 'object' || subLanes === null) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
// Parent lane names should be capitalized
|
|
90
|
+
if (parentLane[0] !== parentLane[0].toUpperCase()) {
|
|
91
|
+
const capitalizedName = parentLane.charAt(0).toUpperCase() + parentLane.slice(1);
|
|
92
|
+
errors.push(`Parent lane "${parentLane}" should be capitalized (e.g., "${capitalizedName}")`);
|
|
93
|
+
}
|
|
94
|
+
// Validate each sub-lane
|
|
95
|
+
const subLaneEntries = subLanes;
|
|
96
|
+
for (const [subLaneName, subLaneConfig] of Object.entries(subLaneEntries)) {
|
|
97
|
+
validateSubLane(parentLane, subLaneName, subLaneConfig, errors);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Validate a single sub-lane configuration
|
|
102
|
+
*/
|
|
103
|
+
function validateSubLane(parentLane, subLaneName, subLaneConfig, errors) {
|
|
104
|
+
if (typeof subLaneConfig !== 'object' || subLaneConfig === null) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const config = subLaneConfig;
|
|
108
|
+
// Required fields for sub-lanes
|
|
109
|
+
if (!config.description && !config.code_paths) {
|
|
110
|
+
errors.push(`Sub-lane "${parentLane}: ${subLaneName}" is missing required fields (description, code_paths)`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Validate that lane-inference.yaml uses the correct hierarchical format.
|
|
115
|
+
*
|
|
116
|
+
* Expected format:
|
|
117
|
+
* ```yaml
|
|
118
|
+
* Framework:
|
|
119
|
+
* Core:
|
|
120
|
+
* description: '...'
|
|
121
|
+
* code_paths: [...]
|
|
122
|
+
* keywords: [...]
|
|
123
|
+
* ```
|
|
124
|
+
*
|
|
125
|
+
* Not the old flat format:
|
|
126
|
+
* ```yaml
|
|
127
|
+
* lanes:
|
|
128
|
+
* - name: Framework
|
|
129
|
+
* code_paths: [...]
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* @param options - Validation options
|
|
133
|
+
* @returns Validation result
|
|
134
|
+
*/
|
|
135
|
+
export function validateLaneInferenceFormat(options) {
|
|
136
|
+
const { projectDir } = options;
|
|
137
|
+
const laneInferencePath = path.join(projectDir, LANE_INFERENCE_FILE);
|
|
138
|
+
// Check if file exists
|
|
139
|
+
if (!fs.existsSync(laneInferencePath)) {
|
|
140
|
+
return {
|
|
141
|
+
valid: false,
|
|
142
|
+
errors: [],
|
|
143
|
+
error: `${LANE_INFERENCE_FILE} not found in ${projectDir}`,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
let content;
|
|
147
|
+
try {
|
|
148
|
+
const rawContent = fs.readFileSync(laneInferencePath, 'utf-8');
|
|
149
|
+
content = yaml.parse(rawContent);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
return {
|
|
153
|
+
valid: false,
|
|
154
|
+
errors: [],
|
|
155
|
+
error: `Failed to parse ${LANE_INFERENCE_FILE}: ${err instanceof Error ? err.message : String(err)}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const errors = [];
|
|
159
|
+
// Check for old flat 'lanes' array format
|
|
160
|
+
if ('lanes' in content && Array.isArray(content.lanes)) {
|
|
161
|
+
errors.push("Invalid format: found 'lanes' array. Use hierarchical format (Framework: Core: ...) instead.");
|
|
162
|
+
return { valid: false, errors };
|
|
163
|
+
}
|
|
164
|
+
// Validate hierarchical structure
|
|
165
|
+
for (const [parentLane, subLanes] of Object.entries(content)) {
|
|
166
|
+
validateParentLane(parentLane, subLanes, errors);
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
valid: errors.length === 0,
|
|
170
|
+
errors,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Initialize a git repository in the given directory
|
|
175
|
+
*/
|
|
176
|
+
function initializeGitRepo(projectDir) {
|
|
177
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path -- Git binary from PATH is acceptable for smoke tests
|
|
178
|
+
execFileSync(GIT_BINARY, ['init'], { cwd: projectDir, stdio: 'pipe' });
|
|
179
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path -- Git binary from PATH is acceptable for smoke tests
|
|
180
|
+
execFileSync(GIT_BINARY, ['config', 'user.email', 'test@example.com'], {
|
|
181
|
+
cwd: projectDir,
|
|
182
|
+
stdio: 'pipe',
|
|
183
|
+
});
|
|
184
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path -- Git binary from PATH is acceptable for smoke tests
|
|
185
|
+
execFileSync(GIT_BINARY, ['config', 'user.name', 'Test User'], {
|
|
186
|
+
cwd: projectDir,
|
|
187
|
+
stdio: 'pipe',
|
|
188
|
+
});
|
|
189
|
+
// Create initial commit
|
|
190
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path -- Git binary from PATH is acceptable for smoke tests
|
|
191
|
+
execFileSync(GIT_BINARY, ['add', '-A'], { cwd: projectDir, stdio: 'pipe' });
|
|
192
|
+
// eslint-disable-next-line sonarjs/no-os-command-from-path -- Git binary from PATH is acceptable for smoke tests
|
|
193
|
+
execFileSync(GIT_BINARY, ['commit', '-m', 'Initial commit', '--allow-empty'], {
|
|
194
|
+
cwd: projectDir,
|
|
195
|
+
stdio: 'pipe',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Create a sample WU YAML file for testing
|
|
200
|
+
*/
|
|
201
|
+
function createSampleWuYaml(projectDir) {
|
|
202
|
+
const wuDir = path.join(projectDir, 'docs', '04-operations', 'tasks', 'wu');
|
|
203
|
+
fs.mkdirSync(wuDir, { recursive: true });
|
|
204
|
+
const wuYaml = `id: WU-TEST-001
|
|
205
|
+
title: Test WU
|
|
206
|
+
lane: 'Framework: Core'
|
|
207
|
+
type: feature
|
|
208
|
+
status: ready
|
|
209
|
+
priority: P3
|
|
210
|
+
created: 2026-02-02
|
|
211
|
+
code_paths:
|
|
212
|
+
- 'src/**'
|
|
213
|
+
acceptance:
|
|
214
|
+
- Test passes
|
|
215
|
+
`;
|
|
216
|
+
fs.writeFileSync(path.join(wuDir, 'WU-TEST-001.yaml'), wuYaml);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Validate that wu:create works with requireRemote=false config.
|
|
220
|
+
*
|
|
221
|
+
* This creates a minimal WU in the test project to verify the flow works
|
|
222
|
+
* without a git remote.
|
|
223
|
+
*
|
|
224
|
+
* @param options - Validation options
|
|
225
|
+
* @returns Validation result
|
|
226
|
+
*/
|
|
227
|
+
async function validateWuCreate(options) {
|
|
228
|
+
const { projectDir } = options;
|
|
229
|
+
// Create .lumenflow.config.yaml with requireRemote=false
|
|
230
|
+
const configPath = path.join(projectDir, LUMENFLOW_CONFIG_FILE);
|
|
231
|
+
const config = `# LumenFlow Configuration (smoke test)
|
|
232
|
+
git:
|
|
233
|
+
requireRemote: false
|
|
234
|
+
`;
|
|
235
|
+
fs.writeFileSync(configPath, config);
|
|
236
|
+
try {
|
|
237
|
+
initializeGitRepo(projectDir);
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
return {
|
|
241
|
+
success: false,
|
|
242
|
+
error: `Failed to initialize git repo: ${err instanceof Error ? err.message : String(err)}`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
// Create a sample WU YAML to simulate wu:create output
|
|
246
|
+
// Note: We don't actually run wu:create as it requires the full CLI to be installed
|
|
247
|
+
// Instead we validate the config would allow it to work
|
|
248
|
+
createSampleWuYaml(projectDir);
|
|
249
|
+
return { success: true };
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Collect errors from validation results
|
|
253
|
+
*/
|
|
254
|
+
function collectScriptsErrors(scriptsResult) {
|
|
255
|
+
const errors = [];
|
|
256
|
+
if (scriptsResult.error) {
|
|
257
|
+
errors.push(`Init scripts validation error: ${scriptsResult.error}`);
|
|
258
|
+
}
|
|
259
|
+
if (scriptsResult.missingScripts.length > 0) {
|
|
260
|
+
errors.push(`Missing scripts: ${scriptsResult.missingScripts.join(', ')}`);
|
|
261
|
+
}
|
|
262
|
+
if (scriptsResult.invalidScripts.length > 0) {
|
|
263
|
+
errors.push(`Invalid script format: ${scriptsResult.invalidScripts.join(', ')}`);
|
|
264
|
+
}
|
|
265
|
+
return errors;
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Collect errors from lane validation results
|
|
269
|
+
*/
|
|
270
|
+
function collectLaneErrors(laneResult) {
|
|
271
|
+
const errors = [];
|
|
272
|
+
if (laneResult.error) {
|
|
273
|
+
errors.push(`Lane-inference validation error: ${laneResult.error}`);
|
|
274
|
+
}
|
|
275
|
+
errors.push(...laneResult.errors);
|
|
276
|
+
return errors;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Run validations in the temp directory
|
|
280
|
+
*/
|
|
281
|
+
async function runValidations(tempDir, skipWuCreate) {
|
|
282
|
+
const errors = [];
|
|
283
|
+
// Step 1: Run lumenflow init --full
|
|
284
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
285
|
+
// Step 2: Validate init scripts
|
|
286
|
+
const scriptsResult = validateInitScripts({ projectDir: tempDir });
|
|
287
|
+
if (!scriptsResult.valid) {
|
|
288
|
+
errors.push(...collectScriptsErrors(scriptsResult));
|
|
289
|
+
}
|
|
290
|
+
// Step 3: Validate lane-inference format
|
|
291
|
+
const laneResult = validateLaneInferenceFormat({ projectDir: tempDir });
|
|
292
|
+
if (!laneResult.valid) {
|
|
293
|
+
errors.push(...collectLaneErrors(laneResult));
|
|
294
|
+
}
|
|
295
|
+
// Step 4: Validate wu:create with requireRemote=false (if not skipped)
|
|
296
|
+
let wuResult;
|
|
297
|
+
if (!skipWuCreate) {
|
|
298
|
+
wuResult = await validateWuCreate({ projectDir: tempDir });
|
|
299
|
+
if (!wuResult.success && wuResult.error) {
|
|
300
|
+
errors.push(`wu:create validation error: ${wuResult.error}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return { scriptsResult, laneResult, wuResult, errors };
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Clean up temporary directory
|
|
307
|
+
*/
|
|
308
|
+
function cleanupTempDir(tempDir) {
|
|
309
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
310
|
+
try {
|
|
311
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
// Ignore cleanup errors
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Run the full onboarding smoke test.
|
|
320
|
+
*
|
|
321
|
+
* Creates a temp directory, runs lumenflow init --full, and validates:
|
|
322
|
+
* 1. Package.json scripts are correctly injected
|
|
323
|
+
* 2. Lane-inference.yaml uses hierarchical format
|
|
324
|
+
* 3. wu:create would work with requireRemote=false
|
|
325
|
+
*
|
|
326
|
+
* @param options - Smoke test options
|
|
327
|
+
* @returns Smoke test result
|
|
328
|
+
*/
|
|
329
|
+
export async function runOnboardingSmokeTest(options = {}) {
|
|
330
|
+
const { cleanup = true, skipWuCreate = false } = options;
|
|
331
|
+
let { tempDir } = options;
|
|
332
|
+
const result = {
|
|
333
|
+
success: false,
|
|
334
|
+
errors: [],
|
|
335
|
+
};
|
|
336
|
+
// Create temp directory if not provided
|
|
337
|
+
if (!tempDir) {
|
|
338
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-smoke-test-'));
|
|
339
|
+
result.tempDir = tempDir;
|
|
340
|
+
}
|
|
341
|
+
// Ensure directory exists
|
|
342
|
+
if (!fs.existsSync(tempDir)) {
|
|
343
|
+
try {
|
|
344
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
errors: [
|
|
350
|
+
`Failed to create temp directory: ${err instanceof Error ? err.message : String(err)}`,
|
|
351
|
+
],
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
const { scriptsResult, laneResult, wuResult, errors } = await runValidations(tempDir, skipWuCreate);
|
|
357
|
+
result.initScriptsValidation = scriptsResult;
|
|
358
|
+
result.laneInferenceValidation = laneResult;
|
|
359
|
+
result.wuCreateValidation = wuResult;
|
|
360
|
+
result.errors = errors;
|
|
361
|
+
result.success = errors.length === 0;
|
|
362
|
+
}
|
|
363
|
+
catch (err) {
|
|
364
|
+
result.errors = [`Smoke test failed: ${err instanceof Error ? err.message : String(err)}`];
|
|
365
|
+
result.success = false;
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
if (cleanup) {
|
|
369
|
+
cleanupTempDir(tempDir);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Run the onboarding smoke test as a gate.
|
|
376
|
+
*
|
|
377
|
+
* This is the entry point for the gates pipeline.
|
|
378
|
+
*
|
|
379
|
+
* @param options - Gate options
|
|
380
|
+
* @returns Gate result with ok status and duration
|
|
381
|
+
*/
|
|
382
|
+
export async function runOnboardingSmokeTestGate(options) {
|
|
383
|
+
const start = Date.now();
|
|
384
|
+
const logger = options.logger ?? console;
|
|
385
|
+
logger.log('Running onboarding smoke test...');
|
|
386
|
+
const result = await runOnboardingSmokeTest({ cleanup: true });
|
|
387
|
+
if (result.success) {
|
|
388
|
+
logger.log('Onboarding smoke test passed.');
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
logger.log('Onboarding smoke test failed:');
|
|
392
|
+
for (const error of result.errors) {
|
|
393
|
+
logger.log(` - ${error}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
ok: result.success,
|
|
398
|
+
duration: Date.now() - start,
|
|
399
|
+
};
|
|
400
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
3
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
4
|
+
/**
|
|
5
|
+
* Plan Create Command (WU-1313)
|
|
6
|
+
*
|
|
7
|
+
* Creates plan files in the repo-native plansDir (not LUMENFLOW_HOME).
|
|
8
|
+
* Plans can be linked to WUs (via spec_refs) or initiatives (via related_plan).
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* pnpm plan:create --id WU-1313 --title "Feature plan"
|
|
12
|
+
* pnpm plan:create --id INIT-001 --title "Initiative plan"
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
* - Creates plan in repo directories.plansDir
|
|
16
|
+
* - Supports both WU-XXX and INIT-XXX IDs
|
|
17
|
+
* - Uses micro-worktree isolation for atomic commits
|
|
18
|
+
* - Idempotent: fails if plan already exists (no overwrite)
|
|
19
|
+
*
|
|
20
|
+
* Context: WU-1313 (INIT-013 Plan Tooling)
|
|
21
|
+
*/
|
|
22
|
+
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
23
|
+
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
24
|
+
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import { createWUParser, WU_OPTIONS } from '@lumenflow/core/dist/arg-parser.js';
|
|
27
|
+
import { ensureOnMain } from '@lumenflow/core/dist/wu-helpers.js';
|
|
28
|
+
import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
|
|
29
|
+
import { WU_PATHS } from '@lumenflow/core/dist/wu-paths.js';
|
|
30
|
+
import { todayISO } from '@lumenflow/core/dist/date-utils.js';
|
|
31
|
+
import { LOG_PREFIX as CORE_LOG_PREFIX } from '@lumenflow/core/dist/wu-constants.js';
|
|
32
|
+
/** Log prefix for console output */
|
|
33
|
+
export const LOG_PREFIX = CORE_LOG_PREFIX.PLAN_CREATE ?? '[plan:create]';
|
|
34
|
+
/** Micro-worktree operation name */
|
|
35
|
+
const OPERATION_NAME = 'plan-create';
|
|
36
|
+
/** LumenFlow URI scheme for plan references */
|
|
37
|
+
const PLAN_URI_SCHEME = 'lumenflow://plans/';
|
|
38
|
+
/** WU ID pattern */
|
|
39
|
+
const WU_ID_PATTERN = /^WU-\d+$/;
|
|
40
|
+
/** Initiative ID pattern */
|
|
41
|
+
const INIT_ID_PATTERN = /^INIT-[A-Z0-9]+$/i;
|
|
42
|
+
/**
|
|
43
|
+
* Validate that the ID is a valid WU or Initiative ID
|
|
44
|
+
*
|
|
45
|
+
* @param id - ID to validate (WU-XXX or INIT-XXX)
|
|
46
|
+
* @throws Error if ID format is invalid
|
|
47
|
+
*/
|
|
48
|
+
export function validatePlanId(id) {
|
|
49
|
+
if (!id) {
|
|
50
|
+
die(`ID is required\n\nExpected format: WU-XXX or INIT-XXX`);
|
|
51
|
+
}
|
|
52
|
+
const isWU = WU_ID_PATTERN.test(id);
|
|
53
|
+
const isInit = INIT_ID_PATTERN.test(id);
|
|
54
|
+
if (!isWU && !isInit) {
|
|
55
|
+
die(`Invalid ID format: "${id}"\n\n` +
|
|
56
|
+
`Expected format:\n` +
|
|
57
|
+
` - WU ID: WU-<number> (e.g., WU-1313)\n` +
|
|
58
|
+
` - Initiative ID: INIT-<alphanumeric> (e.g., INIT-001, INIT-TOOLING)`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Get the lumenflow:// URI for a plan
|
|
63
|
+
*
|
|
64
|
+
* @param id - WU or Initiative ID
|
|
65
|
+
* @returns lumenflow://plans/{id}-plan.md URI
|
|
66
|
+
*/
|
|
67
|
+
export function getPlanUri(id) {
|
|
68
|
+
return `${PLAN_URI_SCHEME}${id}-plan.md`;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Create a plan file in the repo plansDir
|
|
72
|
+
*
|
|
73
|
+
* @param worktreePath - Path to repo root or worktree
|
|
74
|
+
* @param id - WU or Initiative ID
|
|
75
|
+
* @param title - Plan title
|
|
76
|
+
* @returns Path to created file
|
|
77
|
+
* @throws Error if file already exists
|
|
78
|
+
*/
|
|
79
|
+
export function createPlan(worktreePath, id, title) {
|
|
80
|
+
const plansDir = join(worktreePath, WU_PATHS.PLANS_DIR());
|
|
81
|
+
const planPath = join(plansDir, `${id}-plan.md`);
|
|
82
|
+
if (existsSync(planPath)) {
|
|
83
|
+
die(`Plan file already exists: ${planPath}\n\n` +
|
|
84
|
+
`Options:\n` +
|
|
85
|
+
` 1. Edit the existing plan: pnpm plan:edit --id ${id}\n` +
|
|
86
|
+
` 2. Delete and recreate (not recommended)\n` +
|
|
87
|
+
` 3. Use plan:link to link existing plan to a WU/initiative`);
|
|
88
|
+
}
|
|
89
|
+
// Ensure plans directory exists
|
|
90
|
+
if (!existsSync(plansDir)) {
|
|
91
|
+
mkdirSync(plansDir, { recursive: true });
|
|
92
|
+
}
|
|
93
|
+
const today = todayISO();
|
|
94
|
+
const template = `# ${id} Plan - ${title}
|
|
95
|
+
|
|
96
|
+
Created: ${today}
|
|
97
|
+
|
|
98
|
+
## Goal
|
|
99
|
+
|
|
100
|
+
<!-- What is the primary objective? -->
|
|
101
|
+
|
|
102
|
+
## Scope
|
|
103
|
+
|
|
104
|
+
<!-- What is in scope and out of scope? -->
|
|
105
|
+
|
|
106
|
+
## Approach
|
|
107
|
+
|
|
108
|
+
<!-- How will you achieve the goal? Key phases or milestones? -->
|
|
109
|
+
|
|
110
|
+
## Success Criteria
|
|
111
|
+
|
|
112
|
+
<!-- How will you know when this is complete? Measurable outcomes? -->
|
|
113
|
+
|
|
114
|
+
## Risks
|
|
115
|
+
|
|
116
|
+
<!-- What could go wrong? How will you mitigate? -->
|
|
117
|
+
|
|
118
|
+
## Open Questions
|
|
119
|
+
|
|
120
|
+
<!-- Unresolved questions or decisions needed -->
|
|
121
|
+
|
|
122
|
+
## References
|
|
123
|
+
|
|
124
|
+
- ID: ${id}
|
|
125
|
+
- Created: ${today}
|
|
126
|
+
`;
|
|
127
|
+
writeFileSync(planPath, template, { encoding: 'utf-8' });
|
|
128
|
+
console.log(`${LOG_PREFIX} Created plan: ${planPath}`);
|
|
129
|
+
return planPath;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Generate commit message for plan creation
|
|
133
|
+
*
|
|
134
|
+
* @param id - WU or Initiative ID
|
|
135
|
+
* @param title - Plan title
|
|
136
|
+
* @returns Commit message
|
|
137
|
+
*/
|
|
138
|
+
export function getCommitMessage(id, title) {
|
|
139
|
+
const idLower = id.toLowerCase();
|
|
140
|
+
return `docs: create plan for ${idLower} - ${title}`;
|
|
141
|
+
}
|
|
142
|
+
async function main() {
|
|
143
|
+
const args = createWUParser({
|
|
144
|
+
name: 'plan-create',
|
|
145
|
+
description: 'Create a new plan file in repo plansDir',
|
|
146
|
+
options: [WU_OPTIONS.id, WU_OPTIONS.title],
|
|
147
|
+
required: ['id', 'title'],
|
|
148
|
+
allowPositionalId: true,
|
|
149
|
+
});
|
|
150
|
+
const id = args.id;
|
|
151
|
+
const title = args.title;
|
|
152
|
+
// Validate inputs
|
|
153
|
+
validatePlanId(id);
|
|
154
|
+
console.log(`${LOG_PREFIX} Creating plan for ${id}...`);
|
|
155
|
+
// Ensure on main for micro-worktree operations
|
|
156
|
+
await ensureOnMain(getGitForCwd());
|
|
157
|
+
try {
|
|
158
|
+
let createdPlanPath = '';
|
|
159
|
+
await withMicroWorktree({
|
|
160
|
+
operation: OPERATION_NAME,
|
|
161
|
+
id,
|
|
162
|
+
logPrefix: LOG_PREFIX,
|
|
163
|
+
pushOnly: true,
|
|
164
|
+
execute: async ({ worktreePath }) => {
|
|
165
|
+
// Create plan file
|
|
166
|
+
createdPlanPath = createPlan(worktreePath, id, title);
|
|
167
|
+
// Get relative path for commit
|
|
168
|
+
const planRelPath = createdPlanPath.replace(worktreePath + '/', '');
|
|
169
|
+
return {
|
|
170
|
+
commitMessage: getCommitMessage(id, title),
|
|
171
|
+
files: [planRelPath],
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
const planUri = getPlanUri(id);
|
|
176
|
+
console.log(`\n${LOG_PREFIX} Plan created successfully!`);
|
|
177
|
+
console.log(`\nPlan Details:`);
|
|
178
|
+
console.log(` ID: ${id}`);
|
|
179
|
+
console.log(` Title: ${title}`);
|
|
180
|
+
console.log(` URI: ${planUri}`);
|
|
181
|
+
console.log(` File: ${createdPlanPath}`);
|
|
182
|
+
console.log(`\nNext steps:`);
|
|
183
|
+
console.log(` 1. Edit the plan file with your goals and approach`);
|
|
184
|
+
console.log(` 2. Link to WU/initiative: pnpm plan:link --id ${id} --plan ${planUri}`);
|
|
185
|
+
console.log(` 3. When ready, promote: pnpm plan:promote --id ${id}`);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
die(`Plan creation failed: ${error.message}\n\n` +
|
|
189
|
+
`Micro-worktree cleanup was attempted automatically.\n` +
|
|
190
|
+
`If issue persists, check for orphaned branches: git branch | grep tmp/${OPERATION_NAME}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Guard main() for testability
|
|
194
|
+
import { runCLI } from './cli-entry-point.js';
|
|
195
|
+
if (import.meta.main) {
|
|
196
|
+
void runCLI(main);
|
|
197
|
+
}
|
|
198
|
+
// Export for testing
|
|
199
|
+
export { main };
|