@lumenflow/cli 2.4.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -8
- 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 +207 -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__/templates-sync.test.js +219 -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 +670 -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/orchestrate-init-status.js +37 -9
- package/dist/orchestrate-initiative.js +10 -4
- 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/sync-templates.js +137 -5
- 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-prep.js +131 -8
- package/dist/wu-spawn.js +14 -1
- package/dist/wu-unblock.js +34 -2
- package/dist/wu-validate.js +25 -17
- package/package.json +11 -7
- package/templates/core/.lumenflow/constraints.md.template +61 -3
- package/templates/core/AGENTS.md.template +2 -2
- package/templates/core/LUMENFLOW.md.template +85 -23
- package/templates/core/ai/onboarding/agent-invocation-guide.md.template +157 -0
- package/templates/core/ai/onboarding/agent-safety-card.md.template +227 -0
- package/templates/core/ai/onboarding/docs-generation.md.template +277 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +49 -7
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +343 -110
- package/templates/core/ai/onboarding/release-process.md.template +8 -2
- package/templates/core/ai/onboarding/starting-prompt.md.template +407 -0
- package/templates/core/ai/onboarding/test-ratchet.md.template +131 -0
- package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +91 -38
- package/templates/core/ai/onboarding/vendor-support.md.template +219 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +13 -1
- package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +14 -16
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +48 -4
- package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +5 -1
- package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +19 -8
- 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
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
2
3
|
/**
|
|
3
4
|
* Orchestrate Initiative Status CLI
|
|
4
5
|
*
|
|
@@ -10,10 +11,14 @@
|
|
|
10
11
|
*/
|
|
11
12
|
import { Command } from 'commander';
|
|
12
13
|
import { existsSync, readdirSync } from 'node:fs';
|
|
13
|
-
import { loadInitiativeWUs, calculateProgress, formatProgress } from '@lumenflow/initiatives';
|
|
14
|
+
import { loadInitiativeWUs, calculateProgress, formatProgress, getLaneAvailability, resolveLaneConfigsFromConfig, } from '@lumenflow/initiatives';
|
|
14
15
|
import { EXIT_CODES, LUMENFLOW_PATHS } from '@lumenflow/core/dist/wu-constants.js';
|
|
16
|
+
import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
|
|
15
17
|
import chalk from 'chalk';
|
|
16
18
|
const LOG_PREFIX = '[orchestrate:init-status]';
|
|
19
|
+
function getErrorMessage(error) {
|
|
20
|
+
return error instanceof Error ? error.message : String(error);
|
|
21
|
+
}
|
|
17
22
|
function getCompletedWUs(wuIds) {
|
|
18
23
|
const completed = new Set();
|
|
19
24
|
if (!existsSync(LUMENFLOW_PATHS.STAMPS_DIR)) {
|
|
@@ -27,6 +32,21 @@ function getCompletedWUs(wuIds) {
|
|
|
27
32
|
}
|
|
28
33
|
return completed;
|
|
29
34
|
}
|
|
35
|
+
function formatLaneAvailability(availability, laneConfigs) {
|
|
36
|
+
const lanes = Object.keys(availability).sort((a, b) => a.localeCompare(b));
|
|
37
|
+
if (lanes.length === 0) {
|
|
38
|
+
return ' (no lanes found)';
|
|
39
|
+
}
|
|
40
|
+
return lanes
|
|
41
|
+
.map((lane) => {
|
|
42
|
+
const entry = availability[lane];
|
|
43
|
+
const wipLimit = laneConfigs[lane]?.wip_limit ?? 1;
|
|
44
|
+
const statusLabel = entry.available ? chalk.green('available') : chalk.red('occupied');
|
|
45
|
+
const occupiedBy = entry.occupiedBy ?? 'none';
|
|
46
|
+
return ` ${lane}: ${statusLabel} (wip_limit=${wipLimit}, lock_policy=${entry.policy}, in_progress=${entry.inProgressCount}, blocked=${entry.blockedCount}, occupied_by=${occupiedBy})`;
|
|
47
|
+
})
|
|
48
|
+
.join('\n');
|
|
49
|
+
}
|
|
30
50
|
const program = new Command()
|
|
31
51
|
.name('orchestrate:init-status')
|
|
32
52
|
.description('Show initiative progress status')
|
|
@@ -45,18 +65,26 @@ const program = new Command()
|
|
|
45
65
|
const completed = getCompletedWUs(wus.map((w) => w.id));
|
|
46
66
|
console.log(chalk.bold('WUs:'));
|
|
47
67
|
for (const wu of wus) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
68
|
+
let status = chalk.gray('○ ready');
|
|
69
|
+
if (completed.has(wu.id)) {
|
|
70
|
+
status = chalk.green('✓ done');
|
|
71
|
+
}
|
|
72
|
+
else if (wu.doc.status === 'in_progress') {
|
|
73
|
+
status = chalk.yellow('⟳ in_progress');
|
|
74
|
+
}
|
|
75
|
+
else if (wu.doc.status === 'blocked') {
|
|
76
|
+
status = chalk.red('⛔ blocked');
|
|
77
|
+
}
|
|
55
78
|
console.log(` ${wu.id}: ${wu.doc.title} [${status}]`);
|
|
56
79
|
}
|
|
80
|
+
const laneConfigs = resolveLaneConfigsFromConfig(getConfig());
|
|
81
|
+
const availability = getLaneAvailability(wus, { laneConfigs });
|
|
82
|
+
console.log('');
|
|
83
|
+
console.log(chalk.bold('Lane Availability:'));
|
|
84
|
+
console.log(formatLaneAvailability(availability, laneConfigs));
|
|
57
85
|
}
|
|
58
86
|
catch (err) {
|
|
59
|
-
console.error(chalk.red(`${LOG_PREFIX} Error: ${err
|
|
87
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${getErrorMessage(err)}`));
|
|
60
88
|
process.exit(EXIT_CODES.ERROR);
|
|
61
89
|
}
|
|
62
90
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
2
3
|
/**
|
|
3
4
|
* Orchestrate Initiative CLI
|
|
4
5
|
*
|
|
@@ -11,8 +12,12 @@
|
|
|
11
12
|
*/
|
|
12
13
|
import { Command } from 'commander';
|
|
13
14
|
import chalk from 'chalk';
|
|
14
|
-
import { loadInitiativeWUs, loadMultipleInitiatives,
|
|
15
|
+
import { loadInitiativeWUs, loadMultipleInitiatives, buildExecutionPlanWithLockPolicy, resolveLaneConfigsFromConfig, formatExecutionPlan, formatExecutionPlanWithEmbeddedSpawns, calculateProgress, formatProgress, buildCheckpointWave, formatCheckpointOutput, validateCheckpointFlags, resolveCheckpointModeAsync, LOG_PREFIX, } from '@lumenflow/initiatives';
|
|
15
16
|
import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
|
|
17
|
+
import { getConfig } from '@lumenflow/core/dist/lumenflow-config.js';
|
|
18
|
+
function getErrorMessage(error) {
|
|
19
|
+
return error instanceof Error ? error.message : String(error);
|
|
20
|
+
}
|
|
16
21
|
const program = new Command()
|
|
17
22
|
.name('orchestrate-initiative')
|
|
18
23
|
.description('Orchestrate initiative execution with parallel agent spawning')
|
|
@@ -28,7 +33,7 @@ const program = new Command()
|
|
|
28
33
|
validateCheckpointFlags({ checkpointPerWave, dryRun, noCheckpoint });
|
|
29
34
|
}
|
|
30
35
|
catch (error) {
|
|
31
|
-
console.error(chalk.red(`${LOG_PREFIX} Error: ${error
|
|
36
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${getErrorMessage(error)}`));
|
|
32
37
|
process.exit(EXIT_CODES.ERROR);
|
|
33
38
|
}
|
|
34
39
|
if (!initIds || initIds.length === 0) {
|
|
@@ -76,7 +81,8 @@ const program = new Command()
|
|
|
76
81
|
return;
|
|
77
82
|
}
|
|
78
83
|
console.log(chalk.cyan(`${LOG_PREFIX} Building execution plan...`));
|
|
79
|
-
const
|
|
84
|
+
const laneConfigs = resolveLaneConfigsFromConfig(getConfig());
|
|
85
|
+
const plan = buildExecutionPlanWithLockPolicy(wus, { laneConfigs });
|
|
80
86
|
if (plan.waves.length === 0) {
|
|
81
87
|
console.log(chalk.green(`${LOG_PREFIX} All WUs are complete! Nothing to execute.`));
|
|
82
88
|
return;
|
|
@@ -99,7 +105,7 @@ const program = new Command()
|
|
|
99
105
|
console.log(chalk.cyan('Copy the spawn XML above to execute agents.'));
|
|
100
106
|
}
|
|
101
107
|
catch (error) {
|
|
102
|
-
console.error(chalk.red(`${LOG_PREFIX} Error: ${error
|
|
108
|
+
console.error(chalk.red(`${LOG_PREFIX} Error: ${getErrorMessage(error)}`));
|
|
103
109
|
process.exit(EXIT_CODES.ERROR);
|
|
104
110
|
}
|
|
105
111
|
});
|