@lumenflow/cli 2.5.1 โ†’ 2.7.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.
@@ -0,0 +1,304 @@
1
+ /**
2
+ * @file gates-config.test.ts
3
+ * WU-1356: Tests for package manager and script name configuration.
4
+ *
5
+ * Tests configurable package_manager, gates.commands, test_runner, and build_command
6
+ * in .lumenflow.config.yaml for framework agnosticism.
7
+ */
8
+ /* eslint-disable sonarjs/no-duplicate-string -- Test files intentionally repeat string literals for readability */
9
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
10
+ import * as fs from 'node:fs';
11
+ import * as path from 'node:path';
12
+ import * as os from 'node:os';
13
+ import * as yaml from 'yaml';
14
+ // Import the schema and functions we're testing
15
+ import { PackageManagerSchema, TestRunnerSchema, GatesCommandsConfigSchema, parseConfig, } from '@lumenflow/core/dist/lumenflow-config-schema.js';
16
+ import { resolvePackageManager, resolveBuildCommand, resolveGatesCommands, resolveTestRunner, getIgnorePatterns, } from '@lumenflow/core/dist/gates-config.js';
17
+ describe('WU-1356: Package manager and script configuration', () => {
18
+ let tempDir;
19
+ beforeEach(() => {
20
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-test-'));
21
+ });
22
+ afterEach(() => {
23
+ fs.rmSync(tempDir, { recursive: true, force: true });
24
+ });
25
+ describe('PackageManagerSchema', () => {
26
+ it('accepts pnpm as default', () => {
27
+ const result = PackageManagerSchema.parse(undefined);
28
+ expect(result).toBe('pnpm');
29
+ });
30
+ it('accepts npm', () => {
31
+ const result = PackageManagerSchema.parse('npm');
32
+ expect(result).toBe('npm');
33
+ });
34
+ it('accepts yarn', () => {
35
+ const result = PackageManagerSchema.parse('yarn');
36
+ expect(result).toBe('yarn');
37
+ });
38
+ it('accepts bun', () => {
39
+ const result = PackageManagerSchema.parse('bun');
40
+ expect(result).toBe('bun');
41
+ });
42
+ it('rejects invalid package manager', () => {
43
+ expect(() => PackageManagerSchema.parse('invalid')).toThrow();
44
+ });
45
+ });
46
+ describe('TestRunnerSchema', () => {
47
+ it('accepts vitest as default', () => {
48
+ const result = TestRunnerSchema.parse(undefined);
49
+ expect(result).toBe('vitest');
50
+ });
51
+ it('accepts jest', () => {
52
+ const result = TestRunnerSchema.parse('jest');
53
+ expect(result).toBe('jest');
54
+ });
55
+ it('accepts mocha', () => {
56
+ const result = TestRunnerSchema.parse('mocha');
57
+ expect(result).toBe('mocha');
58
+ });
59
+ it('rejects invalid test runner', () => {
60
+ expect(() => TestRunnerSchema.parse('invalid')).toThrow();
61
+ });
62
+ });
63
+ describe('GatesCommandsConfigSchema', () => {
64
+ it('has sensible defaults for test commands', () => {
65
+ const result = GatesCommandsConfigSchema.parse({});
66
+ expect(result.test_full).toBeDefined();
67
+ expect(result.test_docs_only).toBeDefined();
68
+ expect(result.test_incremental).toBeDefined();
69
+ });
70
+ it('allows custom test commands', () => {
71
+ const config = {
72
+ test_full: 'npm test',
73
+ test_docs_only: 'npm test -- --grep docs',
74
+ test_incremental: 'npm test -- --changed',
75
+ };
76
+ const result = GatesCommandsConfigSchema.parse(config);
77
+ expect(result.test_full).toBe('npm test');
78
+ expect(result.test_docs_only).toBe('npm test -- --grep docs');
79
+ expect(result.test_incremental).toBe('npm test -- --changed');
80
+ });
81
+ });
82
+ describe('LumenFlowConfigSchema - package_manager field', () => {
83
+ it('includes package_manager with default pnpm', () => {
84
+ const config = parseConfig({});
85
+ expect(config.package_manager).toBe('pnpm');
86
+ });
87
+ it('accepts npm as package_manager', () => {
88
+ const config = parseConfig({ package_manager: 'npm' });
89
+ expect(config.package_manager).toBe('npm');
90
+ });
91
+ it('accepts yarn as package_manager', () => {
92
+ const config = parseConfig({ package_manager: 'yarn' });
93
+ expect(config.package_manager).toBe('yarn');
94
+ });
95
+ it('accepts bun as package_manager', () => {
96
+ const config = parseConfig({ package_manager: 'bun' });
97
+ expect(config.package_manager).toBe('bun');
98
+ });
99
+ });
100
+ describe('LumenFlowConfigSchema - test_runner field', () => {
101
+ it('includes test_runner with default vitest', () => {
102
+ const config = parseConfig({});
103
+ expect(config.test_runner).toBe('vitest');
104
+ });
105
+ it('accepts jest as test_runner', () => {
106
+ const config = parseConfig({ test_runner: 'jest' });
107
+ expect(config.test_runner).toBe('jest');
108
+ });
109
+ it('accepts mocha as test_runner', () => {
110
+ const config = parseConfig({ test_runner: 'mocha' });
111
+ expect(config.test_runner).toBe('mocha');
112
+ });
113
+ });
114
+ describe('LumenFlowConfigSchema - gates.commands section', () => {
115
+ it('includes gates.commands with defaults', () => {
116
+ const config = parseConfig({});
117
+ expect(config.gates.commands).toBeDefined();
118
+ expect(config.gates.commands.test_full).toBeDefined();
119
+ expect(config.gates.commands.test_docs_only).toBeDefined();
120
+ expect(config.gates.commands.test_incremental).toBeDefined();
121
+ });
122
+ it('allows custom gates commands configuration', () => {
123
+ const config = parseConfig({
124
+ gates: {
125
+ commands: {
126
+ test_full: 'npm test',
127
+ test_docs_only: 'npm test -- --docs',
128
+ test_incremental: 'npm test -- --changed',
129
+ },
130
+ },
131
+ });
132
+ expect(config.gates.commands.test_full).toBe('npm test');
133
+ expect(config.gates.commands.test_docs_only).toBe('npm test -- --docs');
134
+ expect(config.gates.commands.test_incremental).toBe('npm test -- --changed');
135
+ });
136
+ });
137
+ describe('LumenFlowConfigSchema - build_command field', () => {
138
+ it('includes build_command with default for pnpm', () => {
139
+ const config = parseConfig({});
140
+ expect(config.build_command).toBe('pnpm --filter @lumenflow/cli build');
141
+ });
142
+ it('allows custom build_command', () => {
143
+ const config = parseConfig({ build_command: 'npm run build' });
144
+ expect(config.build_command).toBe('npm run build');
145
+ });
146
+ });
147
+ describe('resolvePackageManager', () => {
148
+ it('returns pnpm when no config file exists', () => {
149
+ const result = resolvePackageManager(tempDir);
150
+ expect(result).toBe('pnpm');
151
+ });
152
+ it('returns configured package manager from config file', () => {
153
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
154
+ fs.writeFileSync(configPath, yaml.stringify({ package_manager: 'npm' }));
155
+ const result = resolvePackageManager(tempDir);
156
+ expect(result).toBe('npm');
157
+ });
158
+ it('returns yarn when configured', () => {
159
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
160
+ fs.writeFileSync(configPath, yaml.stringify({ package_manager: 'yarn' }));
161
+ const result = resolvePackageManager(tempDir);
162
+ expect(result).toBe('yarn');
163
+ });
164
+ });
165
+ describe('resolveBuildCommand', () => {
166
+ it('returns default build command when no config file exists', () => {
167
+ const result = resolveBuildCommand(tempDir);
168
+ expect(result).toBe('pnpm --filter @lumenflow/cli build');
169
+ });
170
+ it('returns configured build_command from config file', () => {
171
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
172
+ fs.writeFileSync(configPath, yaml.stringify({ build_command: 'npm run build' }));
173
+ const result = resolveBuildCommand(tempDir);
174
+ expect(result).toBe('npm run build');
175
+ });
176
+ it('adapts default build command for different package managers', () => {
177
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
178
+ fs.writeFileSync(configPath, yaml.stringify({ package_manager: 'npm' }));
179
+ const result = resolveBuildCommand(tempDir);
180
+ expect(result).toContain('npm');
181
+ });
182
+ });
183
+ describe('resolveGatesCommands', () => {
184
+ it('returns default commands when no config file exists', () => {
185
+ const commands = resolveGatesCommands(tempDir);
186
+ expect(commands.test_full).toBeDefined();
187
+ expect(commands.test_docs_only).toBeDefined();
188
+ expect(commands.test_incremental).toBeDefined();
189
+ });
190
+ it('returns configured commands from config file', () => {
191
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
192
+ fs.writeFileSync(configPath, yaml.stringify({
193
+ gates: {
194
+ commands: {
195
+ test_full: 'npm test',
196
+ test_docs_only: 'npm test -- --docs',
197
+ test_incremental: 'npm test -- --changed',
198
+ },
199
+ },
200
+ }));
201
+ const commands = resolveGatesCommands(tempDir);
202
+ expect(commands.test_full).toBe('npm test');
203
+ expect(commands.test_docs_only).toBe('npm test -- --docs');
204
+ expect(commands.test_incremental).toBe('npm test -- --changed');
205
+ });
206
+ });
207
+ describe('resolveTestRunner', () => {
208
+ it('returns vitest when no config file exists', () => {
209
+ const result = resolveTestRunner(tempDir);
210
+ expect(result).toBe('vitest');
211
+ });
212
+ it('returns jest when configured', () => {
213
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
214
+ fs.writeFileSync(configPath, yaml.stringify({ test_runner: 'jest' }));
215
+ const result = resolveTestRunner(tempDir);
216
+ expect(result).toBe('jest');
217
+ });
218
+ });
219
+ describe('getIgnorePatterns', () => {
220
+ it('returns .turbo pattern for vitest', () => {
221
+ const patterns = getIgnorePatterns('vitest');
222
+ expect(patterns).toContain('.turbo');
223
+ });
224
+ it('returns different pattern for jest', () => {
225
+ const patterns = getIgnorePatterns('jest');
226
+ expect(patterns).not.toContain('.turbo');
227
+ });
228
+ it('returns custom pattern from config', () => {
229
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
230
+ fs.writeFileSync(configPath, yaml.stringify({
231
+ gates: {
232
+ ignore_patterns: ['.nx', 'dist'],
233
+ },
234
+ }));
235
+ // This would be a config-aware version
236
+ const patterns = getIgnorePatterns('jest');
237
+ expect(Array.isArray(patterns)).toBe(true);
238
+ });
239
+ });
240
+ });
241
+ describe('WU-1356: npm+jest configuration', () => {
242
+ let tempDir;
243
+ beforeEach(() => {
244
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-npm-jest-'));
245
+ });
246
+ afterEach(() => {
247
+ fs.rmSync(tempDir, { recursive: true, force: true });
248
+ });
249
+ it('supports npm+jest configuration', () => {
250
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
251
+ fs.writeFileSync(configPath, yaml.stringify({
252
+ package_manager: 'npm',
253
+ test_runner: 'jest',
254
+ gates: {
255
+ commands: {
256
+ test_full: 'npm test',
257
+ test_docs_only: 'npm test -- --testPathPattern=docs',
258
+ test_incremental: 'npm test -- --onlyChanged',
259
+ },
260
+ },
261
+ build_command: 'npm run build',
262
+ }));
263
+ const pkgManager = resolvePackageManager(tempDir);
264
+ const testRunner = resolveTestRunner(tempDir);
265
+ const commands = resolveGatesCommands(tempDir);
266
+ const buildCmd = resolveBuildCommand(tempDir);
267
+ expect(pkgManager).toBe('npm');
268
+ expect(testRunner).toBe('jest');
269
+ expect(commands.test_full).toBe('npm test');
270
+ expect(commands.test_incremental).toBe('npm test -- --onlyChanged');
271
+ expect(buildCmd).toBe('npm run build');
272
+ });
273
+ });
274
+ describe('WU-1356: yarn+nx configuration', () => {
275
+ let tempDir;
276
+ beforeEach(() => {
277
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-yarn-nx-'));
278
+ });
279
+ afterEach(() => {
280
+ fs.rmSync(tempDir, { recursive: true, force: true });
281
+ });
282
+ it('supports yarn+nx configuration', () => {
283
+ const configPath = path.join(tempDir, '.lumenflow.config.yaml');
284
+ fs.writeFileSync(configPath, yaml.stringify({
285
+ package_manager: 'yarn',
286
+ test_runner: 'jest',
287
+ gates: {
288
+ commands: {
289
+ test_full: 'yarn nx run-many --target=test --all',
290
+ test_docs_only: 'yarn nx test docs',
291
+ test_incremental: 'yarn nx affected --target=test',
292
+ },
293
+ },
294
+ build_command: 'yarn nx build @lumenflow/cli',
295
+ }));
296
+ const pkgManager = resolvePackageManager(tempDir);
297
+ const commands = resolveGatesCommands(tempDir);
298
+ const buildCmd = resolveBuildCommand(tempDir);
299
+ expect(pkgManager).toBe('yarn');
300
+ expect(commands.test_full).toBe('yarn nx run-many --target=test --all');
301
+ expect(commands.test_incremental).toBe('yarn nx affected --target=test');
302
+ expect(buildCmd).toBe('yarn nx build @lumenflow/cli');
303
+ });
304
+ });
@@ -0,0 +1,97 @@
1
+ /**
2
+ * @file wu-proto.test.ts
3
+ * Test suite for wu:proto command (WU-1359)
4
+ *
5
+ * WU-1359: Add wu:proto convenience command for rapid prototyping
6
+ *
7
+ * Tests:
8
+ * - wu:proto creates WU with type: prototype
9
+ * - wu:proto has relaxed validation (no --acceptance required)
10
+ * - wu:proto immediately claims the WU
11
+ * - wu:proto prints cd command to worktree
12
+ */
13
+ import { describe, it, expect } from 'vitest';
14
+ // WU-1359: Import wu:proto validation and helpers
15
+ import { validateProtoSpec } from '../wu-proto.js';
16
+ /** Test constants to avoid duplicate string literals (sonarjs/no-duplicate-string) */
17
+ const TEST_WU_ID = 'WU-9999';
18
+ const TEST_LANE = 'Framework: CLI';
19
+ const TEST_TITLE = 'Quick prototype';
20
+ const TEST_DESCRIPTION = 'Context: testing.\nProblem: need to test.\nSolution: add tests.';
21
+ describe('wu:proto command (WU-1359)', () => {
22
+ describe('validateProtoSpec relaxed validation', () => {
23
+ it('should pass validation without --acceptance', () => {
24
+ const result = validateProtoSpec({
25
+ id: TEST_WU_ID,
26
+ lane: TEST_LANE,
27
+ title: TEST_TITLE,
28
+ opts: {
29
+ description: TEST_DESCRIPTION,
30
+ codePaths: ['packages/@lumenflow/cli/src/wu-proto.ts'],
31
+ },
32
+ });
33
+ expect(result.valid).toBe(true);
34
+ expect(result.errors).toHaveLength(0);
35
+ });
36
+ it('should pass validation without --exposure', () => {
37
+ const result = validateProtoSpec({
38
+ id: TEST_WU_ID,
39
+ lane: TEST_LANE,
40
+ title: TEST_TITLE,
41
+ opts: {
42
+ description: TEST_DESCRIPTION,
43
+ },
44
+ });
45
+ expect(result.valid).toBe(true);
46
+ expect(result.errors).toHaveLength(0);
47
+ });
48
+ it('should pass validation without --code-paths', () => {
49
+ const result = validateProtoSpec({
50
+ id: TEST_WU_ID,
51
+ lane: TEST_LANE,
52
+ title: TEST_TITLE,
53
+ opts: {
54
+ description: TEST_DESCRIPTION,
55
+ },
56
+ });
57
+ expect(result.valid).toBe(true);
58
+ expect(result.errors).toHaveLength(0);
59
+ });
60
+ it('should require lane', () => {
61
+ const result = validateProtoSpec({
62
+ id: TEST_WU_ID,
63
+ lane: '',
64
+ title: TEST_TITLE,
65
+ opts: {},
66
+ });
67
+ expect(result.valid).toBe(false);
68
+ expect(result.errors.some((e) => e.toLowerCase().includes('lane'))).toBe(true);
69
+ });
70
+ it('should require title', () => {
71
+ const result = validateProtoSpec({
72
+ id: TEST_WU_ID,
73
+ lane: TEST_LANE,
74
+ title: '',
75
+ opts: {},
76
+ });
77
+ expect(result.valid).toBe(false);
78
+ expect(result.errors.some((e) => e.toLowerCase().includes('title'))).toBe(true);
79
+ });
80
+ });
81
+ describe('prototype WU type', () => {
82
+ it('should set type to prototype', () => {
83
+ // The buildProtoWUContent function should return type: prototype
84
+ // We test this indirectly through the validateProtoSpec which checks content
85
+ const result = validateProtoSpec({
86
+ id: TEST_WU_ID,
87
+ lane: TEST_LANE,
88
+ title: TEST_TITLE,
89
+ opts: {
90
+ description: TEST_DESCRIPTION,
91
+ },
92
+ });
93
+ // Prototype WUs should always be valid with minimal input
94
+ expect(result.valid).toBe(true);
95
+ });
96
+ });
97
+ });
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
- import { loadLaneHealthConfig, resolveTestPolicy, } from '@lumenflow/core/dist/gates-config.js';
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
- const TEST_CONFIG_BASENAMES = new Set(['turbo.json', 'pnpm-lock.yaml', 'package.json']);
188
- const TEST_CONFIG_PATTERNS = [/^vitest\.config\.(ts|mts|js|mjs|cjs)$/i, /^tsconfig(\..+)?\.json$/i];
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 using turbo's --filter flag.
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
- // Build turbo filter args for each package
399
- // turbo supports --filter=@scope/package or --filter=package
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 result = run(pnpmCmd('turbo', 'run', 'test', ...filterArgs), { agentLog });
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 Vitest's --changed flag from the repo root.
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(pnpmCmd('turbo', 'run', 'test'), { agentLog });
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(pnpmCmd('turbo', 'run', 'test'), { agentLog });
877
+ const result = run(gatesCommands.test_full, { agentLog });
845
878
  return { ...result, duration: Date.now() - start, isIncremental: false };
846
879
  }
847
- logLine('\n> Running tests (vitest --changed)\n');
848
- const result = run(pnpmCmd('vitest', 'run', ...buildVitestChangedArgs({ baseBranch: 'origin/main' })), { agentLog });
849
- return { ...result, duration: Date.now() - start, isIncremental: true };
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(pnpmCmd('turbo', 'run', 'test'), { agentLog });
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
- ? pnpmCmd('turbo', 'run', 'test')
1240
+ ? configuredGatesCommands.test_full
1192
1241
  : GATE_COMMANDS.INCREMENTAL_TEST,
1193
1242
  warnOnly: !testsRequired,
1194
1243
  },
package/dist/init.js CHANGED
@@ -2011,6 +2011,7 @@ const LUMENFLOW_SCRIPTS = {
2011
2011
  'wu:claim': 'wu-claim',
2012
2012
  'wu:done': 'wu-done',
2013
2013
  'wu:create': 'wu-create',
2014
+ 'wu:proto': 'wu-proto', // WU-1359: Prototype WU with relaxed validation
2014
2015
  'wu:status': 'wu-status',
2015
2016
  'wu:block': 'wu-block',
2016
2017
  'wu:unblock': 'wu-unblock',
@@ -2361,10 +2362,21 @@ export async function main() {
2361
2362
  console.log('\nWarnings:');
2362
2363
  result.warnings.forEach((w) => console.log(` โš  ${w}`));
2363
2364
  }
2365
+ // WU-1359: Show complete lifecycle with auto-ID (no --id flag required)
2364
2366
  console.log('\n[lumenflow init] Done! Next steps:');
2365
2367
  console.log(' 1. Review AGENTS.md and LUMENFLOW.md for workflow documentation');
2366
2368
  console.log(` 2. Edit ${CONFIG_FILE_NAME} to match your project structure`);
2367
- console.log(' 3. Run: pnpm wu:create --id WU-0001 --lane <lane> --title "First WU"');
2369
+ console.log(' 3. Start your first WU:');
2370
+ console.log('');
2371
+ console.log(' # Create (auto-generates ID):');
2372
+ console.log(' pnpm wu:create --lane <lane> --title "First WU" \\');
2373
+ console.log(' --description "Context: ... Problem: ... Solution: ..." \\');
2374
+ console.log(' --acceptance "Criterion 1" --code-paths "src/..." --exposure backend-only');
2375
+ console.log('');
2376
+ console.log(' # Or for rapid prototyping (minimal validation):');
2377
+ console.log(' pnpm wu:proto --lane <lane> --title "Quick experiment"');
2378
+ console.log('');
2379
+ console.log(' 4. Full lifecycle: wu:create -> wu:claim -> wu:prep -> wu:done');
2368
2380
  /* eslint-enable no-console */
2369
2381
  }
2370
2382
  // WU-1297: Use import.meta.main instead of exporting main() without calling it