@lumenflow/cli 2.5.0 → 2.6.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 +11 -8
- package/dist/__tests__/gates-config.test.js +304 -0
- package/dist/__tests__/init-scripts.test.js +111 -0
- package/dist/__tests__/templates-sync.test.js +219 -0
- package/dist/gates.js +64 -15
- package/dist/init.js +90 -0
- package/dist/orchestrate-init-status.js +37 -9
- package/dist/orchestrate-initiative.js +10 -4
- package/dist/sync-templates.js +137 -5
- package/dist/wu-prep.js +131 -8
- package/dist/wu-spawn.js +7 -2
- package/package.json +7 -7
- package/templates/core/.lumenflow/constraints.md.template +61 -3
- 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/gates.js
CHANGED
|
@@ -65,7 +65,8 @@ import { runSystemMapValidation } from '@lumenflow/core/dist/system-map-validato
|
|
|
65
65
|
// WU-1191: Lane health gate configuration
|
|
66
66
|
// WU-1262: Coverage config from methodology policy
|
|
67
67
|
// WU-1280: Test policy for tests_required (warn vs block on test failures)
|
|
68
|
-
|
|
68
|
+
// WU-1356: Configurable package manager and test commands
|
|
69
|
+
import { loadLaneHealthConfig, resolveTestPolicy, resolveGatesCommands, resolveTestRunner, } from '@lumenflow/core/dist/gates-config.js';
|
|
69
70
|
// WU-1191: Lane health check
|
|
70
71
|
import { runLaneHealthCheck } from './lane-health.js';
|
|
71
72
|
// WU-1315: Onboarding smoke test
|
|
@@ -184,8 +185,24 @@ const PRETTIER_CONFIG_FILES = new Set([
|
|
|
184
185
|
'prettier.config.mjs',
|
|
185
186
|
'.prettierignore',
|
|
186
187
|
]);
|
|
187
|
-
|
|
188
|
-
const
|
|
188
|
+
// WU-1356: Extended to support multiple build tools and test runners
|
|
189
|
+
const TEST_CONFIG_BASENAMES = new Set([
|
|
190
|
+
'turbo.json', // Turborepo
|
|
191
|
+
'nx.json', // Nx
|
|
192
|
+
'lerna.json', // Lerna
|
|
193
|
+
'pnpm-lock.yaml',
|
|
194
|
+
'package-lock.json',
|
|
195
|
+
'yarn.lock',
|
|
196
|
+
'bun.lockb',
|
|
197
|
+
'package.json',
|
|
198
|
+
]);
|
|
199
|
+
// WU-1356: Extended to support vitest, jest, and mocha config patterns
|
|
200
|
+
const TEST_CONFIG_PATTERNS = [
|
|
201
|
+
/^vitest\.config\.(ts|mts|js|mjs|cjs)$/i,
|
|
202
|
+
/^jest\.config\.(ts|js|mjs|cjs|json)$/i,
|
|
203
|
+
/^\.mocharc\.(js|json|yaml|yml)$/i,
|
|
204
|
+
/^tsconfig(\..+)?\.json$/i,
|
|
205
|
+
];
|
|
189
206
|
function normalizePath(filePath) {
|
|
190
207
|
return filePath.replace(/\\/g, '/');
|
|
191
208
|
}
|
|
@@ -380,9 +397,11 @@ export function loadCurrentWUCodePaths(options = {}) {
|
|
|
380
397
|
}
|
|
381
398
|
/**
|
|
382
399
|
* WU-1299: Run filtered tests for docs-only mode
|
|
400
|
+
* WU-1356: Updated to use configured test command
|
|
383
401
|
*
|
|
384
402
|
* When --docs-only is passed and code_paths contains packages, this runs tests
|
|
385
|
-
* only for those specific packages
|
|
403
|
+
* only for those specific packages. The filter syntax adapts to the configured
|
|
404
|
+
* build tool (turbo, nx, or plain package manager).
|
|
386
405
|
*
|
|
387
406
|
* @param options - Options including packages to test and agent log context
|
|
388
407
|
* @returns Result object with ok status and duration
|
|
@@ -395,10 +414,20 @@ async function runDocsOnlyFilteredTests({ packages, agentLog, }) {
|
|
|
395
414
|
return { ok: true, duration: Date.now() - start };
|
|
396
415
|
}
|
|
397
416
|
logLine(`\n> Tests (docs-only filtered: ${packages.join(', ')})\n`);
|
|
398
|
-
//
|
|
399
|
-
|
|
417
|
+
// WU-1356: Use configured test command with filter
|
|
418
|
+
const gatesCommands = resolveGatesCommands(process.cwd());
|
|
419
|
+
// If there's a configured test_docs_only command, use it
|
|
420
|
+
if (gatesCommands.test_docs_only) {
|
|
421
|
+
const result = run(gatesCommands.test_docs_only, { agentLog });
|
|
422
|
+
return { ok: result.ok, duration: Date.now() - start };
|
|
423
|
+
}
|
|
424
|
+
// Otherwise, use the full test command with filter args
|
|
425
|
+
// Build filter args for each package (works with turbo, nx, and pnpm/yarn workspaces)
|
|
400
426
|
const filterArgs = packages.map((pkg) => `--filter=${pkg}`);
|
|
401
|
-
const
|
|
427
|
+
const baseCmd = gatesCommands.test_full;
|
|
428
|
+
// Append filter args to the base command
|
|
429
|
+
const filteredCmd = `${baseCmd} ${filterArgs.join(' ')}`;
|
|
430
|
+
const result = run(filteredCmd, { agentLog });
|
|
402
431
|
return { ok: result.ok, duration: Date.now() - start };
|
|
403
432
|
}
|
|
404
433
|
export function parsePrettierListOutput(output) {
|
|
@@ -782,7 +811,8 @@ async function runIncrementalLint({ agentLog, } = {}) {
|
|
|
782
811
|
}
|
|
783
812
|
}
|
|
784
813
|
/**
|
|
785
|
-
* Run changed tests using
|
|
814
|
+
* Run changed tests using configured test runner's incremental mode.
|
|
815
|
+
* WU-1356: Updated to use configured commands from gates-config.
|
|
786
816
|
* Falls back to full test suite if on main branch or if the run fails.
|
|
787
817
|
*
|
|
788
818
|
* @returns {{ ok: boolean, duration: number, isIncremental: boolean }}
|
|
@@ -797,13 +827,16 @@ async function runChangedTests({ agentLog, } = {}) {
|
|
|
797
827
|
}
|
|
798
828
|
writeSync(agentLog.logFd, `${line}\n`);
|
|
799
829
|
};
|
|
830
|
+
// WU-1356: Get configured commands
|
|
831
|
+
const gatesCommands = resolveGatesCommands(process.cwd());
|
|
832
|
+
const testRunner = resolveTestRunner(process.cwd());
|
|
800
833
|
try {
|
|
801
834
|
const git = getGitForCwd();
|
|
802
835
|
const currentBranch = await git.getCurrentBranch();
|
|
803
836
|
const isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
|
|
804
837
|
if (isMainBranch) {
|
|
805
838
|
logLine('📋 On main branch - running full test suite');
|
|
806
|
-
const result = run(
|
|
839
|
+
const result = run(gatesCommands.test_full, { agentLog });
|
|
807
840
|
return { ...result, isIncremental: false };
|
|
808
841
|
}
|
|
809
842
|
let changedFiles = [];
|
|
@@ -841,16 +874,29 @@ async function runChangedTests({ agentLog, } = {}) {
|
|
|
841
874
|
logLine('⚠️ Changed file list unavailable - running full test suite');
|
|
842
875
|
}
|
|
843
876
|
logLine('📋 Running full test suite to avoid missing coverage');
|
|
844
|
-
const result = run(
|
|
877
|
+
const result = run(gatesCommands.test_full, { agentLog });
|
|
845
878
|
return { ...result, duration: Date.now() - start, isIncremental: false };
|
|
846
879
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
880
|
+
// WU-1356: Use configured incremental test command
|
|
881
|
+
logLine(`\n> Running tests (${testRunner} --changed)\n`);
|
|
882
|
+
// If test_incremental is configured, use it directly
|
|
883
|
+
if (gatesCommands.test_incremental) {
|
|
884
|
+
const result = run(gatesCommands.test_incremental, { agentLog });
|
|
885
|
+
return { ...result, duration: Date.now() - start, isIncremental: true };
|
|
886
|
+
}
|
|
887
|
+
// Fallback: For vitest, use the built-in changed args helper
|
|
888
|
+
if (testRunner === 'vitest') {
|
|
889
|
+
const result = run(pnpmCmd('vitest', 'run', ...buildVitestChangedArgs({ baseBranch: 'origin/main' })), { agentLog });
|
|
890
|
+
return { ...result, duration: Date.now() - start, isIncremental: true };
|
|
891
|
+
}
|
|
892
|
+
// For other runners without configured incremental, fall back to full
|
|
893
|
+
logLine('⚠️ No incremental test command configured, running full suite');
|
|
894
|
+
const result = run(gatesCommands.test_full, { agentLog });
|
|
895
|
+
return { ...result, duration: Date.now() - start, isIncremental: false };
|
|
850
896
|
}
|
|
851
897
|
catch (error) {
|
|
852
898
|
console.error('⚠️ Changed tests failed, falling back to full suite:', error.message);
|
|
853
|
-
const result = run(
|
|
899
|
+
const result = run(gatesCommands.test_full, { agentLog });
|
|
854
900
|
return { ...result, isIncremental: false };
|
|
855
901
|
}
|
|
856
902
|
}
|
|
@@ -1055,6 +1101,8 @@ async function executeGates(opts) {
|
|
|
1055
1101
|
const testsRequired = resolvedTestPolicy.tests_required;
|
|
1056
1102
|
// WU-1191: Lane health gate mode (warn, error, or off)
|
|
1057
1103
|
const laneHealthMode = loadLaneHealthConfig(process.cwd());
|
|
1104
|
+
// WU-1356: Resolve configured gates commands for test execution
|
|
1105
|
+
const configuredGatesCommands = resolveGatesCommands(process.cwd());
|
|
1058
1106
|
if (useAgentMode) {
|
|
1059
1107
|
console.log(`🧾 gates (agent mode): output -> ${agentLog.logPath} (use --verbose for streaming)\n`);
|
|
1060
1108
|
}
|
|
@@ -1185,10 +1233,11 @@ async function executeGates(opts) {
|
|
|
1185
1233
|
// WU-1920: Use changed tests by default, full suite with --full-tests
|
|
1186
1234
|
// WU-2244: --full-coverage implies --full-tests for accurate coverage
|
|
1187
1235
|
// WU-1280: When tests_required=false (methodology.testing: none), failures only warn
|
|
1236
|
+
// WU-1356: Use configured test command instead of hard-coded turbo
|
|
1188
1237
|
{
|
|
1189
1238
|
name: GATE_NAMES.TEST,
|
|
1190
1239
|
cmd: isFullTests || isFullCoverage
|
|
1191
|
-
?
|
|
1240
|
+
? configuredGatesCommands.test_full
|
|
1192
1241
|
: GATE_COMMANDS.INCREMENTAL_TEST,
|
|
1193
1242
|
warnOnly: !testsRequired,
|
|
1194
1243
|
},
|
package/dist/init.js
CHANGED
|
@@ -1909,6 +1909,8 @@ export async function scaffoldProject(targetDir, options) {
|
|
|
1909
1909
|
// Create .lumenflow/agents directory with .gitkeep
|
|
1910
1910
|
await createDirectory(path.join(targetDir, LUMENFLOW_AGENTS_DIR), result, targetDir);
|
|
1911
1911
|
await createFile(path.join(targetDir, LUMENFLOW_AGENTS_DIR, '.gitkeep'), '', options.force ? 'force' : 'skip', result, targetDir);
|
|
1912
|
+
// WU-1342: Create .gitignore with required exclusions
|
|
1913
|
+
await scaffoldGitignore(targetDir, options, result);
|
|
1912
1914
|
// Optional: full docs scaffolding
|
|
1913
1915
|
if (options.full) {
|
|
1914
1916
|
await scaffoldFullDocs(targetDir, options, result, tokenDefaults);
|
|
@@ -1925,18 +1927,106 @@ export async function scaffoldProject(targetDir, options) {
|
|
|
1925
1927
|
}
|
|
1926
1928
|
return result;
|
|
1927
1929
|
}
|
|
1930
|
+
/**
|
|
1931
|
+
* WU-1342: .gitignore template with required exclusions
|
|
1932
|
+
* Includes node_modules, .lumenflow/state, and worktrees
|
|
1933
|
+
*/
|
|
1934
|
+
const GITIGNORE_TEMPLATE = `# Dependencies
|
|
1935
|
+
node_modules/
|
|
1936
|
+
|
|
1937
|
+
# LumenFlow state (local only, not shared)
|
|
1938
|
+
.lumenflow/state/
|
|
1939
|
+
|
|
1940
|
+
# Worktrees (isolated parallel work directories)
|
|
1941
|
+
worktrees/
|
|
1942
|
+
|
|
1943
|
+
# Build output
|
|
1944
|
+
dist/
|
|
1945
|
+
*.tsbuildinfo
|
|
1946
|
+
|
|
1947
|
+
# Environment files
|
|
1948
|
+
.env
|
|
1949
|
+
.env.local
|
|
1950
|
+
.env.*.local
|
|
1951
|
+
|
|
1952
|
+
# IDE
|
|
1953
|
+
.idea/
|
|
1954
|
+
.vscode/
|
|
1955
|
+
*.swp
|
|
1956
|
+
*.swo
|
|
1957
|
+
|
|
1958
|
+
# OS files
|
|
1959
|
+
.DS_Store
|
|
1960
|
+
Thumbs.db
|
|
1961
|
+
`;
|
|
1962
|
+
/** Gitignore file name constant to avoid duplicate string lint error */
|
|
1963
|
+
const GITIGNORE_FILE_NAME = '.gitignore';
|
|
1964
|
+
/**
|
|
1965
|
+
* WU-1342: Scaffold .gitignore file with LumenFlow exclusions
|
|
1966
|
+
* Supports merge mode to add exclusions to existing .gitignore
|
|
1967
|
+
*/
|
|
1968
|
+
async function scaffoldGitignore(targetDir, options, result) {
|
|
1969
|
+
const gitignorePath = path.join(targetDir, GITIGNORE_FILE_NAME);
|
|
1970
|
+
const fileMode = getFileMode(options);
|
|
1971
|
+
if (fileMode === 'merge' && fs.existsSync(gitignorePath)) {
|
|
1972
|
+
// Merge mode: append LumenFlow exclusions if not already present
|
|
1973
|
+
const existingContent = fs.readFileSync(gitignorePath, 'utf-8');
|
|
1974
|
+
const linesToAdd = [];
|
|
1975
|
+
// Check each required exclusion
|
|
1976
|
+
const requiredExclusions = [
|
|
1977
|
+
{ pattern: 'node_modules', line: 'node_modules/' },
|
|
1978
|
+
{ pattern: '.lumenflow/state', line: '.lumenflow/state/' },
|
|
1979
|
+
{ pattern: 'worktrees', line: 'worktrees/' },
|
|
1980
|
+
];
|
|
1981
|
+
for (const { pattern, line } of requiredExclusions) {
|
|
1982
|
+
if (!existingContent.includes(pattern)) {
|
|
1983
|
+
linesToAdd.push(line);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1986
|
+
if (linesToAdd.length > 0) {
|
|
1987
|
+
const separator = existingContent.endsWith('\n') ? '' : '\n';
|
|
1988
|
+
const lumenflowBlock = `${separator}
|
|
1989
|
+
# LumenFlow (auto-added)
|
|
1990
|
+
${linesToAdd.join('\n')}
|
|
1991
|
+
`;
|
|
1992
|
+
fs.writeFileSync(gitignorePath, existingContent + lumenflowBlock);
|
|
1993
|
+
result.merged?.push(GITIGNORE_FILE_NAME);
|
|
1994
|
+
}
|
|
1995
|
+
else {
|
|
1996
|
+
result.skipped.push(GITIGNORE_FILE_NAME);
|
|
1997
|
+
}
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
// Skip or force mode
|
|
2001
|
+
await createFile(gitignorePath, GITIGNORE_TEMPLATE, fileMode, result, targetDir);
|
|
2002
|
+
}
|
|
1928
2003
|
/**
|
|
1929
2004
|
* WU-1307: LumenFlow scripts to inject into package.json
|
|
2005
|
+
* WU-1342: Expanded to include all 17 essential commands
|
|
1930
2006
|
* Uses standalone binaries (wu-claim, wu-done, gates) that work in consumer projects
|
|
1931
2007
|
* after installing @lumenflow/cli.
|
|
1932
2008
|
*/
|
|
1933
2009
|
const LUMENFLOW_SCRIPTS = {
|
|
2010
|
+
// Core WU lifecycle
|
|
1934
2011
|
'wu:claim': 'wu-claim',
|
|
1935
2012
|
'wu:done': 'wu-done',
|
|
1936
2013
|
'wu:create': 'wu-create',
|
|
1937
2014
|
'wu:status': 'wu-status',
|
|
1938
2015
|
'wu:block': 'wu-block',
|
|
1939
2016
|
'wu:unblock': 'wu-unblock',
|
|
2017
|
+
// WU-1342: Additional critical commands
|
|
2018
|
+
'wu:prep': 'wu-prep',
|
|
2019
|
+
'wu:recover': 'wu-recover',
|
|
2020
|
+
'wu:spawn': 'wu-spawn',
|
|
2021
|
+
'wu:validate': 'wu-validate',
|
|
2022
|
+
'wu:infer-lane': 'wu-infer-lane',
|
|
2023
|
+
// Memory commands
|
|
2024
|
+
'mem:init': 'mem-init',
|
|
2025
|
+
'mem:checkpoint': 'mem-checkpoint',
|
|
2026
|
+
'mem:inbox': 'mem-inbox',
|
|
2027
|
+
// Lane commands
|
|
2028
|
+
'lane:suggest': 'lane-suggest',
|
|
2029
|
+
// Gates
|
|
1940
2030
|
gates: 'gates',
|
|
1941
2031
|
'gates:docs': 'gates --docs-only',
|
|
1942
2032
|
};
|
|
@@ -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
|
});
|
package/dist/sync-templates.js
CHANGED
|
@@ -8,9 +8,16 @@
|
|
|
8
8
|
* - Claude skills -> templates/vendors/claude/.claude/skills/
|
|
9
9
|
* - Core docs (LUMENFLOW.md, constraints.md) -> templates/core/
|
|
10
10
|
*/
|
|
11
|
+
/* eslint-disable no-console -- CLI tool requires console output */
|
|
12
|
+
/* eslint-disable security/detect-non-literal-fs-filename -- CLI tool syncs templates from known paths */
|
|
13
|
+
/* eslint-disable security/detect-non-literal-regexp -- Dynamic date pattern for template substitution */
|
|
11
14
|
import * as fs from 'node:fs';
|
|
12
15
|
import * as path from 'node:path';
|
|
13
16
|
import { createWUParser } from '@lumenflow/core';
|
|
17
|
+
// Directory name constants to avoid duplicate strings
|
|
18
|
+
const LUMENFLOW_DIR = '.lumenflow';
|
|
19
|
+
const CLAUDE_DIR = '.claude';
|
|
20
|
+
const SKILLS_DIR = 'skills';
|
|
14
21
|
// Template variable patterns
|
|
15
22
|
const DATE_PATTERN = /\d{4}-\d{2}-\d{2}/g;
|
|
16
23
|
/**
|
|
@@ -29,6 +36,12 @@ const SYNC_TEMPLATES_OPTIONS = {
|
|
|
29
36
|
description: 'Show detailed output',
|
|
30
37
|
default: false,
|
|
31
38
|
},
|
|
39
|
+
checkDrift: {
|
|
40
|
+
name: 'check-drift',
|
|
41
|
+
flags: '--check-drift',
|
|
42
|
+
description: 'Check for template drift without syncing (CI mode)',
|
|
43
|
+
default: false,
|
|
44
|
+
},
|
|
32
45
|
};
|
|
33
46
|
/**
|
|
34
47
|
* Parse sync-templates command options
|
|
@@ -42,6 +55,7 @@ export function parseSyncTemplatesOptions() {
|
|
|
42
55
|
return {
|
|
43
56
|
dryRun: opts['dry-run'] ?? false,
|
|
44
57
|
verbose: opts.verbose ?? false,
|
|
58
|
+
checkDrift: opts['check-drift'] ?? false,
|
|
45
59
|
};
|
|
46
60
|
}
|
|
47
61
|
/**
|
|
@@ -119,8 +133,8 @@ export async function syncOnboardingDocs(projectRoot, dryRun = false) {
|
|
|
119
133
|
*/
|
|
120
134
|
export async function syncSkillsToTemplates(projectRoot, dryRun = false) {
|
|
121
135
|
const result = { synced: [], errors: [] };
|
|
122
|
-
const sourceDir = path.join(projectRoot,
|
|
123
|
-
const targetDir = path.join(getTemplatesDir(projectRoot), 'vendors', 'claude',
|
|
136
|
+
const sourceDir = path.join(projectRoot, CLAUDE_DIR, SKILLS_DIR);
|
|
137
|
+
const targetDir = path.join(getTemplatesDir(projectRoot), 'vendors', 'claude', CLAUDE_DIR, SKILLS_DIR);
|
|
124
138
|
if (!fs.existsSync(sourceDir)) {
|
|
125
139
|
result.errors.push(`Skills source directory not found: ${sourceDir}`);
|
|
126
140
|
return result;
|
|
@@ -153,8 +167,8 @@ export async function syncCoreDocs(projectRoot, dryRun = false) {
|
|
|
153
167
|
const lumenflowTarget = path.join(templatesDir, 'core', 'LUMENFLOW.md.template');
|
|
154
168
|
syncFile(lumenflowSource, lumenflowTarget, projectRoot, result, dryRun);
|
|
155
169
|
// Sync constraints.md
|
|
156
|
-
const constraintsSource = path.join(projectRoot,
|
|
157
|
-
const constraintsTarget = path.join(templatesDir, 'core',
|
|
170
|
+
const constraintsSource = path.join(projectRoot, LUMENFLOW_DIR, 'constraints.md');
|
|
171
|
+
const constraintsTarget = path.join(templatesDir, 'core', LUMENFLOW_DIR, 'constraints.md.template');
|
|
158
172
|
syncFile(constraintsSource, constraintsTarget, projectRoot, result, dryRun);
|
|
159
173
|
return result;
|
|
160
174
|
}
|
|
@@ -167,12 +181,130 @@ export async function syncTemplates(projectRoot, dryRun = false) {
|
|
|
167
181
|
const core = await syncCoreDocs(projectRoot, dryRun);
|
|
168
182
|
return { onboarding, skills, core };
|
|
169
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* Compare source file content with template content (ignoring date placeholders)
|
|
186
|
+
*/
|
|
187
|
+
function compareContent(sourceContent, templateContent, projectRoot) {
|
|
188
|
+
// Convert source to template format for comparison
|
|
189
|
+
const convertedSource = convertToTemplate(sourceContent, projectRoot);
|
|
190
|
+
return convertedSource === templateContent;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Check if a single template file is in sync with its source
|
|
194
|
+
*/
|
|
195
|
+
function checkFileDrift(sourcePath, templatePath, projectRoot) {
|
|
196
|
+
const relativePath = path.relative(projectRoot, templatePath);
|
|
197
|
+
if (!fs.existsSync(sourcePath)) {
|
|
198
|
+
return { isDrifting: false, relativePath }; // Source doesn't exist, can't drift
|
|
199
|
+
}
|
|
200
|
+
if (!fs.existsSync(templatePath)) {
|
|
201
|
+
return { isDrifting: true, relativePath }; // Template missing, definitely drifting
|
|
202
|
+
}
|
|
203
|
+
const sourceContent = fs.readFileSync(sourcePath, 'utf-8');
|
|
204
|
+
const templateContent = fs.readFileSync(templatePath, 'utf-8');
|
|
205
|
+
const isDrifting = !compareContent(sourceContent, templateContent, projectRoot);
|
|
206
|
+
return { isDrifting, relativePath };
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Check for template drift - compares source docs with templates (WU-1353)
|
|
210
|
+
*
|
|
211
|
+
* This function compares source documents with their template counterparts
|
|
212
|
+
* to detect if templates have drifted out of sync. Used by CI to warn
|
|
213
|
+
* when templates need to be re-synced.
|
|
214
|
+
*/
|
|
215
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Multi-category drift check requires nested iteration
|
|
216
|
+
export async function checkTemplateDrift(projectRoot) {
|
|
217
|
+
const driftingFiles = [];
|
|
218
|
+
const checkedFiles = [];
|
|
219
|
+
const templatesDir = getTemplatesDir(projectRoot);
|
|
220
|
+
// Check core docs
|
|
221
|
+
const coreChecks = [
|
|
222
|
+
{
|
|
223
|
+
source: path.join(projectRoot, 'LUMENFLOW.md'),
|
|
224
|
+
template: path.join(templatesDir, 'core', 'LUMENFLOW.md.template'),
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
source: path.join(projectRoot, LUMENFLOW_DIR, 'constraints.md'),
|
|
228
|
+
template: path.join(templatesDir, 'core', LUMENFLOW_DIR, 'constraints.md.template'),
|
|
229
|
+
},
|
|
230
|
+
];
|
|
231
|
+
for (const check of coreChecks) {
|
|
232
|
+
const result = checkFileDrift(check.source, check.template, projectRoot);
|
|
233
|
+
checkedFiles.push(result.relativePath);
|
|
234
|
+
if (result.isDrifting) {
|
|
235
|
+
driftingFiles.push(result.relativePath);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Check onboarding docs
|
|
239
|
+
const onboardingSourceDir = path.join(projectRoot, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding');
|
|
240
|
+
const onboardingTargetDir = path.join(templatesDir, 'core', 'ai', 'onboarding');
|
|
241
|
+
if (fs.existsSync(onboardingSourceDir)) {
|
|
242
|
+
const files = fs.readdirSync(onboardingSourceDir).filter((f) => f.endsWith('.md'));
|
|
243
|
+
for (const file of files) {
|
|
244
|
+
const sourcePath = path.join(onboardingSourceDir, file);
|
|
245
|
+
const templatePath = path.join(onboardingTargetDir, `${file}.template`);
|
|
246
|
+
const result = checkFileDrift(sourcePath, templatePath, projectRoot);
|
|
247
|
+
checkedFiles.push(result.relativePath);
|
|
248
|
+
if (result.isDrifting) {
|
|
249
|
+
driftingFiles.push(result.relativePath);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Check skills
|
|
254
|
+
const skillsSourceDir = path.join(projectRoot, CLAUDE_DIR, SKILLS_DIR);
|
|
255
|
+
const skillsTargetDir = path.join(templatesDir, 'vendors', 'claude', CLAUDE_DIR, SKILLS_DIR);
|
|
256
|
+
if (fs.existsSync(skillsSourceDir)) {
|
|
257
|
+
const skillDirs = fs
|
|
258
|
+
.readdirSync(skillsSourceDir, { withFileTypes: true })
|
|
259
|
+
.filter((d) => d.isDirectory())
|
|
260
|
+
.map((d) => d.name);
|
|
261
|
+
for (const skillName of skillDirs) {
|
|
262
|
+
const skillFile = path.join(skillsSourceDir, skillName, 'SKILL.md');
|
|
263
|
+
const templatePath = path.join(skillsTargetDir, skillName, 'SKILL.md.template');
|
|
264
|
+
if (fs.existsSync(skillFile)) {
|
|
265
|
+
const result = checkFileDrift(skillFile, templatePath, projectRoot);
|
|
266
|
+
checkedFiles.push(result.relativePath);
|
|
267
|
+
if (result.isDrifting) {
|
|
268
|
+
driftingFiles.push(result.relativePath);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
hasDrift: driftingFiles.length > 0,
|
|
275
|
+
driftingFiles,
|
|
276
|
+
checkedFiles,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
170
279
|
/**
|
|
171
280
|
* CLI entry point
|
|
172
281
|
*/
|
|
282
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- CLI main() handles multiple modes and output formatting
|
|
173
283
|
export async function main() {
|
|
174
284
|
const opts = parseSyncTemplatesOptions();
|
|
175
285
|
const projectRoot = process.cwd();
|
|
286
|
+
// Check-drift mode: verify templates match source without syncing
|
|
287
|
+
if (opts.checkDrift) {
|
|
288
|
+
console.log('[sync-templates] Checking for template drift...');
|
|
289
|
+
const drift = await checkTemplateDrift(projectRoot);
|
|
290
|
+
if (opts.verbose) {
|
|
291
|
+
console.log(` Checked ${drift.checkedFiles.length} files`);
|
|
292
|
+
}
|
|
293
|
+
if (drift.hasDrift) {
|
|
294
|
+
console.log('\n[sync-templates] WARNING: Template drift detected!');
|
|
295
|
+
console.log(' The following templates are out of sync with their source:');
|
|
296
|
+
for (const file of drift.driftingFiles) {
|
|
297
|
+
console.log(` - ${file}`);
|
|
298
|
+
}
|
|
299
|
+
console.log('\n Run `pnpm sync:templates` to update templates.');
|
|
300
|
+
process.exitCode = 1;
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
console.log('[sync-templates] All templates are in sync.');
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
// Sync mode: update templates from source
|
|
176
308
|
console.log('[sync-templates] Syncing internal docs to CLI templates...');
|
|
177
309
|
if (opts.dryRun) {
|
|
178
310
|
console.log(' (dry-run mode - no files will be written)');
|
|
@@ -208,5 +340,5 @@ export async function main() {
|
|
|
208
340
|
// CLI entry point
|
|
209
341
|
import { runCLI } from './cli-entry-point.js';
|
|
210
342
|
if (import.meta.main) {
|
|
211
|
-
runCLI(main);
|
|
343
|
+
void runCLI(main);
|
|
212
344
|
}
|