@lumenflow/cli 2.7.0 → 2.9.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.
Files changed (84) hide show
  1. package/README.md +121 -105
  2. package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
  3. package/dist/__tests__/commands/integrate.test.js +165 -0
  4. package/dist/__tests__/commands.test.js +75 -0
  5. package/dist/__tests__/doctor.test.js +510 -0
  6. package/dist/__tests__/gates-config.test.js +0 -1
  7. package/dist/__tests__/hooks/enforcement.test.js +279 -0
  8. package/dist/__tests__/init-greenfield.test.js +247 -0
  9. package/dist/__tests__/init-quick-ref.test.js +0 -1
  10. package/dist/__tests__/init-template-portability.test.js +0 -1
  11. package/dist/__tests__/init.test.js +249 -0
  12. package/dist/__tests__/initiative-e2e.test.js +442 -0
  13. package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
  14. package/dist/__tests__/memory-integration.test.js +333 -0
  15. package/dist/__tests__/release.test.js +1 -1
  16. package/dist/__tests__/safe-git.test.js +0 -1
  17. package/dist/__tests__/state-doctor.test.js +54 -0
  18. package/dist/__tests__/sync-templates.test.js +255 -0
  19. package/dist/__tests__/wu-create-required-fields.test.js +121 -0
  20. package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
  21. package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
  22. package/dist/backlog-prune.js +0 -1
  23. package/dist/cli-entry-point.js +0 -1
  24. package/dist/commands/integrate.js +229 -0
  25. package/dist/commands.js +171 -0
  26. package/dist/docs-sync.js +46 -0
  27. package/dist/doctor.js +479 -10
  28. package/dist/gates.js +0 -7
  29. package/dist/hooks/enforcement-checks.js +209 -0
  30. package/dist/hooks/enforcement-generator.js +365 -0
  31. package/dist/hooks/enforcement-sync.js +243 -0
  32. package/dist/hooks/index.js +7 -0
  33. package/dist/init.js +502 -17
  34. package/dist/initiative-add-wu.js +0 -2
  35. package/dist/initiative-create.js +0 -3
  36. package/dist/initiative-edit.js +0 -5
  37. package/dist/initiative-plan.js +0 -1
  38. package/dist/initiative-remove-wu.js +0 -2
  39. package/dist/lane-health.js +0 -2
  40. package/dist/lane-suggest.js +0 -1
  41. package/dist/mem-checkpoint.js +0 -2
  42. package/dist/mem-cleanup.js +0 -2
  43. package/dist/mem-context.js +0 -3
  44. package/dist/mem-create.js +0 -2
  45. package/dist/mem-delete.js +0 -3
  46. package/dist/mem-inbox.js +0 -2
  47. package/dist/mem-index.js +0 -1
  48. package/dist/mem-init.js +0 -2
  49. package/dist/mem-profile.js +0 -1
  50. package/dist/mem-promote.js +0 -1
  51. package/dist/mem-ready.js +0 -2
  52. package/dist/mem-signal.js +0 -2
  53. package/dist/mem-start.js +0 -2
  54. package/dist/mem-summarize.js +0 -2
  55. package/dist/metrics-cli.js +1 -1
  56. package/dist/metrics-snapshot.js +1 -1
  57. package/dist/onboarding-smoke-test.js +0 -5
  58. package/dist/orchestrate-init-status.js +0 -1
  59. package/dist/orchestrate-initiative.js +0 -1
  60. package/dist/orchestrate-monitor.js +0 -1
  61. package/dist/plan-create.js +0 -2
  62. package/dist/plan-edit.js +0 -2
  63. package/dist/plan-link.js +0 -2
  64. package/dist/plan-promote.js +0 -2
  65. package/dist/signal-cleanup.js +0 -4
  66. package/dist/state-bootstrap.js +0 -1
  67. package/dist/state-cleanup.js +0 -4
  68. package/dist/state-doctor-fix.js +5 -8
  69. package/dist/state-doctor.js +0 -11
  70. package/dist/sync-templates.js +188 -34
  71. package/dist/wu-block.js +100 -48
  72. package/dist/wu-claim.js +1 -22
  73. package/dist/wu-cleanup.js +0 -1
  74. package/dist/wu-create.js +0 -2
  75. package/dist/wu-done-auto-cleanup.js +139 -0
  76. package/dist/wu-done.js +11 -4
  77. package/dist/wu-edit.js +0 -12
  78. package/dist/wu-preflight.js +0 -1
  79. package/dist/wu-prep.js +0 -1
  80. package/dist/wu-proto.js +0 -1
  81. package/dist/wu-spawn.js +0 -3
  82. package/dist/wu-unblock.js +0 -2
  83. package/dist/wu-validate.js +0 -1
  84. package/package.json +9 -7
@@ -0,0 +1,279 @@
1
+ /**
2
+ * @file enforcement.test.ts
3
+ * Tests for Claude Code enforcement hooks (WU-1367)
4
+ *
5
+ * TDD: Write failing tests first, then implement.
6
+ */
7
+ // Test file lint exceptions
8
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
9
+ import * as fs from 'node:fs';
10
+ // Mock fs before importing module under test
11
+ vi.mock('node:fs');
12
+ vi.mock('node:child_process');
13
+ const TEST_PROJECT_DIR = '/test/project';
14
+ const CONFIG_FILE_NAME = '.lumenflow.config.yaml';
15
+ describe('WU-1367: Enforcement Hooks Config Schema', () => {
16
+ describe('ClientConfigSchema enforcement block', () => {
17
+ it('should accept enforcement block under agents.clients.claude-code', async () => {
18
+ // Import dynamically to allow mocking
19
+ const { ClientConfigSchema } = await import('@lumenflow/core/dist/lumenflow-config-schema.js');
20
+ const config = {
21
+ preamble: 'CLAUDE.md',
22
+ skillsDir: '.claude/skills',
23
+ enforcement: {
24
+ hooks: true,
25
+ block_outside_worktree: true,
26
+ require_wu_for_edits: true,
27
+ warn_on_stop_without_wu_done: true,
28
+ },
29
+ };
30
+ const result = ClientConfigSchema.safeParse(config);
31
+ expect(result.success).toBe(true);
32
+ if (result.success) {
33
+ expect(result.data.enforcement).toEqual({
34
+ hooks: true,
35
+ block_outside_worktree: true,
36
+ require_wu_for_edits: true,
37
+ warn_on_stop_without_wu_done: true,
38
+ });
39
+ }
40
+ });
41
+ it('should default enforcement values to false when not specified', async () => {
42
+ const { ClientConfigSchema } = await import('@lumenflow/core/dist/lumenflow-config-schema.js');
43
+ const config = {
44
+ preamble: 'CLAUDE.md',
45
+ enforcement: {},
46
+ };
47
+ const result = ClientConfigSchema.safeParse(config);
48
+ expect(result.success).toBe(true);
49
+ if (result.success) {
50
+ expect(result.data.enforcement?.hooks).toBe(false);
51
+ expect(result.data.enforcement?.block_outside_worktree).toBe(false);
52
+ expect(result.data.enforcement?.require_wu_for_edits).toBe(false);
53
+ expect(result.data.enforcement?.warn_on_stop_without_wu_done).toBe(false);
54
+ }
55
+ });
56
+ it('should allow enforcement to be undefined', async () => {
57
+ const { ClientConfigSchema } = await import('@lumenflow/core/dist/lumenflow-config-schema.js');
58
+ const config = {
59
+ preamble: 'CLAUDE.md',
60
+ };
61
+ const result = ClientConfigSchema.safeParse(config);
62
+ expect(result.success).toBe(true);
63
+ if (result.success) {
64
+ expect(result.data.enforcement).toBeUndefined();
65
+ }
66
+ });
67
+ });
68
+ });
69
+ describe('WU-1367: Hook Generation', () => {
70
+ beforeEach(() => {
71
+ vi.resetAllMocks();
72
+ });
73
+ describe('generateEnforcementHooks', () => {
74
+ it('should generate PreToolUse hook for Write/Edit blocking when block_outside_worktree=true', async () => {
75
+ const { generateEnforcementHooks } = await import('../../hooks/enforcement-generator.js');
76
+ const config = {
77
+ block_outside_worktree: true,
78
+ require_wu_for_edits: false,
79
+ warn_on_stop_without_wu_done: false,
80
+ };
81
+ const hooks = generateEnforcementHooks(config);
82
+ expect(hooks.preToolUse).toBeDefined();
83
+ expect(hooks.preToolUse?.length).toBeGreaterThan(0);
84
+ expect(hooks.preToolUse?.[0].matcher).toBe('Write|Edit');
85
+ });
86
+ it('should generate PreToolUse hook for WU requirement when require_wu_for_edits=true', async () => {
87
+ const { generateEnforcementHooks } = await import('../../hooks/enforcement-generator.js');
88
+ const config = {
89
+ block_outside_worktree: false,
90
+ require_wu_for_edits: true,
91
+ warn_on_stop_without_wu_done: false,
92
+ };
93
+ const hooks = generateEnforcementHooks(config);
94
+ expect(hooks.preToolUse).toBeDefined();
95
+ expect(hooks.preToolUse?.some((h) => h.matcher === 'Write|Edit')).toBe(true);
96
+ });
97
+ it('should generate Stop hook when warn_on_stop_without_wu_done=true', async () => {
98
+ const { generateEnforcementHooks } = await import('../../hooks/enforcement-generator.js');
99
+ const config = {
100
+ block_outside_worktree: false,
101
+ require_wu_for_edits: false,
102
+ warn_on_stop_without_wu_done: true,
103
+ };
104
+ const hooks = generateEnforcementHooks(config);
105
+ expect(hooks.stop).toBeDefined();
106
+ expect(hooks.stop?.length).toBeGreaterThan(0);
107
+ });
108
+ it('should return empty hooks when all enforcement options are false', async () => {
109
+ const { generateEnforcementHooks } = await import('../../hooks/enforcement-generator.js');
110
+ const config = {
111
+ block_outside_worktree: false,
112
+ require_wu_for_edits: false,
113
+ warn_on_stop_without_wu_done: false,
114
+ };
115
+ const hooks = generateEnforcementHooks(config);
116
+ expect(hooks.preToolUse).toBeUndefined();
117
+ expect(hooks.stop).toBeUndefined();
118
+ });
119
+ });
120
+ });
121
+ describe('WU-1367: Integrate Command', () => {
122
+ beforeEach(() => {
123
+ vi.resetAllMocks();
124
+ });
125
+ describe('integrateClaudeCode', () => {
126
+ it('should create .claude/hooks directory when enforcement.hooks=true', async () => {
127
+ const mockMkdirSync = vi.mocked(fs.mkdirSync);
128
+ vi.mocked(fs.writeFileSync);
129
+ vi.mocked(fs.existsSync).mockReturnValue(false);
130
+ const { integrateClaudeCode } = await import('../../commands/integrate.js');
131
+ const config = {
132
+ enforcement: {
133
+ hooks: true,
134
+ block_outside_worktree: true,
135
+ require_wu_for_edits: false,
136
+ warn_on_stop_without_wu_done: false,
137
+ },
138
+ };
139
+ await integrateClaudeCode(TEST_PROJECT_DIR, config);
140
+ expect(mockMkdirSync).toHaveBeenCalledWith(expect.stringContaining('.claude/hooks'), expect.any(Object));
141
+ });
142
+ it('should generate enforce-worktree.sh hook when block_outside_worktree=true', async () => {
143
+ const mockWriteFileSync = vi.mocked(fs.writeFileSync);
144
+ vi.mocked(fs.existsSync).mockReturnValue(false);
145
+ vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
146
+ const { integrateClaudeCode } = await import('../../commands/integrate.js');
147
+ const config = {
148
+ enforcement: {
149
+ hooks: true,
150
+ block_outside_worktree: true,
151
+ require_wu_for_edits: false,
152
+ warn_on_stop_without_wu_done: false,
153
+ },
154
+ };
155
+ await integrateClaudeCode(TEST_PROJECT_DIR, config);
156
+ expect(mockWriteFileSync).toHaveBeenCalledWith(expect.stringContaining('enforce-worktree.sh'), expect.any(String), expect.any(Object));
157
+ });
158
+ it('should update settings.json with hook configuration', async () => {
159
+ const mockWriteFileSync = vi.mocked(fs.writeFileSync);
160
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
161
+ permissions: { allow: ['Bash'] },
162
+ }));
163
+ vi.mocked(fs.existsSync).mockReturnValue(true);
164
+ vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
165
+ const { integrateClaudeCode } = await import('../../commands/integrate.js');
166
+ const config = {
167
+ enforcement: {
168
+ hooks: true,
169
+ block_outside_worktree: true,
170
+ require_wu_for_edits: false,
171
+ warn_on_stop_without_wu_done: false,
172
+ },
173
+ };
174
+ await integrateClaudeCode(TEST_PROJECT_DIR, config);
175
+ // Should write updated settings.json with hooks config
176
+ const settingsCall = mockWriteFileSync.mock.calls.find((call) => String(call[0]).includes('settings.json'));
177
+ expect(settingsCall).toBeDefined();
178
+ const settingsContent = JSON.parse(settingsCall[1]);
179
+ expect(settingsContent.hooks).toBeDefined();
180
+ expect(settingsContent.hooks.PreToolUse).toBeDefined();
181
+ });
182
+ });
183
+ });
184
+ describe('WU-1367: Hook Graceful Degradation', () => {
185
+ it('should allow operation when LumenFlow state cannot be determined', async () => {
186
+ // The hook should fail-open if it cannot determine LumenFlow state
187
+ // This prevents blocking legitimate work due to infrastructure issues
188
+ const { checkWorktreeEnforcement } = await import('../../hooks/enforcement-checks.js');
189
+ // Simulate missing .lumenflow directory
190
+ vi.mocked(fs.existsSync).mockReturnValue(false);
191
+ const result = await checkWorktreeEnforcement({
192
+ file_path: '/some/path/file.ts',
193
+ tool_name: 'Write',
194
+ });
195
+ // Should not block - graceful degradation
196
+ expect(result.allowed).toBe(true);
197
+ expect(result.reason).toContain('graceful');
198
+ });
199
+ it('should allow operation when worktree detection fails', async () => {
200
+ const { checkWorktreeEnforcement } = await import('../../hooks/enforcement-checks.js');
201
+ // Simulate git command failure
202
+ vi.mocked(fs.existsSync).mockReturnValue(true);
203
+ const mockExecFileSync = vi.fn().mockImplementation(() => {
204
+ throw new Error('git command failed');
205
+ });
206
+ vi.doMock('node:child_process', () => ({
207
+ execFileSync: mockExecFileSync,
208
+ }));
209
+ const result = await checkWorktreeEnforcement({
210
+ file_path: '/some/path/file.ts',
211
+ tool_name: 'Write',
212
+ });
213
+ // Should not block - graceful degradation
214
+ expect(result.allowed).toBe(true);
215
+ });
216
+ });
217
+ describe('WU-1367: Setup Hook Sync', () => {
218
+ it('should sync hooks when enforcement.hooks=true in config', async () => {
219
+ // This tests that pnpm setup syncs hooks appropriately
220
+ const mockWriteFileSync = vi.mocked(fs.writeFileSync);
221
+ vi.mocked(fs.mkdirSync).mockReturnValue(undefined);
222
+ // Mock existsSync to return false for most paths (so dirs get created)
223
+ // but return true for the config file
224
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
225
+ const pathStr = String(p);
226
+ return pathStr.endsWith(CONFIG_FILE_NAME);
227
+ });
228
+ // Config file is YAML, not JSON
229
+ vi.mocked(fs.readFileSync).mockImplementation((p) => {
230
+ const pathStr = String(p);
231
+ if (pathStr.endsWith(CONFIG_FILE_NAME)) {
232
+ return `
233
+ agents:
234
+ clients:
235
+ claude-code:
236
+ enforcement:
237
+ hooks: true
238
+ block_outside_worktree: true
239
+ `;
240
+ }
241
+ // Return empty JSON for settings.json
242
+ return '{}';
243
+ });
244
+ // Clear any previous calls
245
+ mockWriteFileSync.mockClear();
246
+ const { syncEnforcementHooks } = await import('../../hooks/enforcement-sync.js');
247
+ const result = await syncEnforcementHooks(TEST_PROJECT_DIR);
248
+ // Should have written hook files
249
+ expect(result).toBe(true);
250
+ expect(mockWriteFileSync).toHaveBeenCalled();
251
+ });
252
+ it('should skip hook sync when enforcement.hooks=false', async () => {
253
+ const mockWriteFileSync = vi.mocked(fs.writeFileSync);
254
+ vi.mocked(fs.existsSync).mockImplementation((p) => {
255
+ const pathStr = String(p);
256
+ return pathStr.endsWith(CONFIG_FILE_NAME);
257
+ });
258
+ vi.mocked(fs.readFileSync).mockImplementation((p) => {
259
+ const pathStr = String(p);
260
+ if (pathStr.endsWith(CONFIG_FILE_NAME)) {
261
+ return `
262
+ agents:
263
+ clients:
264
+ claude-code:
265
+ enforcement:
266
+ hooks: false
267
+ `;
268
+ }
269
+ return '{}';
270
+ });
271
+ // Clear any previous calls
272
+ mockWriteFileSync.mockClear();
273
+ const { syncEnforcementHooks } = await import('../../hooks/enforcement-sync.js');
274
+ const result = await syncEnforcementHooks(TEST_PROJECT_DIR);
275
+ // Should NOT have written hook files
276
+ expect(result).toBe(false);
277
+ expect(mockWriteFileSync).not.toHaveBeenCalled();
278
+ });
279
+ });
@@ -0,0 +1,247 @@
1
+ /**
2
+ * @file init-greenfield.test.ts
3
+ * Tests for greenfield onboarding with initiative-first workflow (WU-1364)
4
+ *
5
+ * Verifies:
6
+ * - Init output includes initiative-first workflow guidance
7
+ * - starting-prompt.md has 'When Starting From Product Vision' section
8
+ * - Init auto-creates initial commit when git repo has no commits
9
+ * - Init auto-sets git.requireRemote=false when no remote configured
10
+ * - Default lane-inference template includes Core and Feature as parent lanes
11
+ * - LUMENFLOW.md mentions initiatives and when to use them
12
+ */
13
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
14
+ import * as fs from 'node:fs';
15
+ import * as path from 'node:path';
16
+ import * as os from 'node:os';
17
+ import { execFileSync } from 'node:child_process';
18
+ import { scaffoldProject } from '../init.js';
19
+ // Constants to avoid duplicate strings
20
+ const ARC42_DOCS_STRUCTURE = 'arc42';
21
+ const STARTING_PROMPT_FILE = 'starting-prompt.md';
22
+ const LUMENFLOW_CONFIG_FILE = '.lumenflow.config.yaml';
23
+ const LANE_INFERENCE_FILE = '.lumenflow.lane-inference.yaml';
24
+ describe('greenfield onboarding (WU-1364)', () => {
25
+ let tempDir;
26
+ beforeEach(() => {
27
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lumenflow-greenfield-test-'));
28
+ });
29
+ afterEach(() => {
30
+ fs.rmSync(tempDir, { recursive: true, force: true });
31
+ });
32
+ function getOnboardingDir() {
33
+ return path.join(tempDir, 'docs', '04-operations', '_frameworks', 'lumenflow', 'agent', 'onboarding');
34
+ }
35
+ function getArc42Options() {
36
+ return {
37
+ force: false,
38
+ full: true,
39
+ docsStructure: ARC42_DOCS_STRUCTURE,
40
+ };
41
+ }
42
+ /**
43
+ * Initialize a git repo without commits (empty repo state)
44
+ * Uses execFileSync for safety (no shell injection)
45
+ */
46
+ function initEmptyGitRepo() {
47
+ execFileSync('git', ['init'], { cwd: tempDir, stdio: 'pipe' });
48
+ // Configure git user for commit (required in some environments)
49
+ execFileSync('git', ['config', 'user.email', 'test@example.com'], {
50
+ cwd: tempDir,
51
+ stdio: 'pipe',
52
+ });
53
+ execFileSync('git', ['config', 'user.name', 'Test User'], { cwd: tempDir, stdio: 'pipe' });
54
+ }
55
+ /**
56
+ * Initialize a git repo with an initial commit
57
+ */
58
+ function initGitRepoWithCommit() {
59
+ initEmptyGitRepo();
60
+ fs.writeFileSync(path.join(tempDir, '.gitkeep'), '');
61
+ execFileSync('git', ['add', '.gitkeep'], { cwd: tempDir, stdio: 'pipe' });
62
+ execFileSync('git', ['commit', '-m', 'Initial commit'], { cwd: tempDir, stdio: 'pipe' });
63
+ }
64
+ describe('AC: starting-prompt.md has initiative-first workflow section', () => {
65
+ it('should include "When Starting From Product Vision" section in starting-prompt.md', async () => {
66
+ await scaffoldProject(tempDir, getArc42Options());
67
+ const startingPromptPath = path.join(getOnboardingDir(), STARTING_PROMPT_FILE);
68
+ expect(fs.existsSync(startingPromptPath)).toBe(true);
69
+ const content = fs.readFileSync(startingPromptPath, 'utf-8');
70
+ expect(content).toContain('When Starting From Product Vision');
71
+ });
72
+ it('should describe 4-step initiative workflow in product vision section', async () => {
73
+ await scaffoldProject(tempDir, getArc42Options());
74
+ const startingPromptPath = path.join(getOnboardingDir(), STARTING_PROMPT_FILE);
75
+ const content = fs.readFileSync(startingPromptPath, 'utf-8');
76
+ // Should mention initiative creation
77
+ expect(content).toContain('initiative:create');
78
+ // Should mention phased work
79
+ expect(content).toMatch(/phase|INIT-/i);
80
+ // Should mention WU organization under initiatives
81
+ expect(content).toMatch(/initiative.*WU|WU.*initiative/i);
82
+ });
83
+ it('should warn against creating orphan WUs without initiative structure', async () => {
84
+ await scaffoldProject(tempDir, getArc42Options());
85
+ const startingPromptPath = path.join(getOnboardingDir(), STARTING_PROMPT_FILE);
86
+ const content = fs.readFileSync(startingPromptPath, 'utf-8');
87
+ // Should have guidance about when NOT to create standalone WUs
88
+ expect(content).toMatch(/don't|avoid|instead.*initiative/i);
89
+ });
90
+ });
91
+ describe('AC: Init auto-creates initial commit when git repo has no commits', () => {
92
+ it('should create initial commit in empty git repo', async () => {
93
+ initEmptyGitRepo();
94
+ // Verify no commits exist
95
+ try {
96
+ execFileSync('git', ['rev-parse', 'HEAD'], { cwd: tempDir, stdio: 'pipe' });
97
+ throw new Error('Expected HEAD to not exist');
98
+ }
99
+ catch {
100
+ // Expected: fatal: ambiguous argument 'HEAD'
101
+ }
102
+ await scaffoldProject(tempDir, getArc42Options());
103
+ // Now HEAD should exist
104
+ const result = execFileSync('git', ['rev-parse', 'HEAD'], {
105
+ cwd: tempDir,
106
+ encoding: 'utf-8',
107
+ stdio: 'pipe',
108
+ });
109
+ expect(result.trim()).toMatch(/^[a-f0-9]{40}$/);
110
+ });
111
+ it('should not create extra commit if repo already has commits', async () => {
112
+ initGitRepoWithCommit();
113
+ // Get initial commit count
114
+ const beforeCount = execFileSync('git', ['rev-list', '--count', 'HEAD'], {
115
+ cwd: tempDir,
116
+ encoding: 'utf-8',
117
+ stdio: 'pipe',
118
+ }).trim();
119
+ await scaffoldProject(tempDir, getArc42Options());
120
+ // Commit count should be the same (init doesn't auto-commit if commits exist)
121
+ const afterCount = execFileSync('git', ['rev-list', '--count', 'HEAD'], {
122
+ cwd: tempDir,
123
+ encoding: 'utf-8',
124
+ stdio: 'pipe',
125
+ }).trim();
126
+ expect(afterCount).toBe(beforeCount);
127
+ });
128
+ it('should skip auto-commit if not in a git repo', async () => {
129
+ // Not a git repo - just a plain directory
130
+ await scaffoldProject(tempDir, getArc42Options());
131
+ // Should not fail, just skip the git operations
132
+ expect(fs.existsSync(path.join(tempDir, LUMENFLOW_CONFIG_FILE))).toBe(true);
133
+ });
134
+ });
135
+ describe('AC: Init auto-sets git.requireRemote=false when no remote configured', () => {
136
+ it('should set requireRemote=false in config when no origin remote', async () => {
137
+ initGitRepoWithCommit();
138
+ // No remote added
139
+ await scaffoldProject(tempDir, getArc42Options());
140
+ const configPath = path.join(tempDir, LUMENFLOW_CONFIG_FILE);
141
+ const content = fs.readFileSync(configPath, 'utf-8');
142
+ expect(content).toContain('requireRemote: false');
143
+ });
144
+ it('should not set requireRemote=false if origin remote exists', async () => {
145
+ initGitRepoWithCommit();
146
+ // Add a remote
147
+ execFileSync('git', ['remote', 'add', 'origin', 'https://github.com/test/repo.git'], {
148
+ cwd: tempDir,
149
+ stdio: 'pipe',
150
+ });
151
+ await scaffoldProject(tempDir, getArc42Options());
152
+ const configPath = path.join(tempDir, LUMENFLOW_CONFIG_FILE);
153
+ const content = fs.readFileSync(configPath, 'utf-8');
154
+ // Should not have requireRemote: false (remote exists)
155
+ expect(content).not.toContain('requireRemote: false');
156
+ });
157
+ it('should skip remote check if not in a git repo', async () => {
158
+ // Not a git repo
159
+ await scaffoldProject(tempDir, getArc42Options());
160
+ const configPath = path.join(tempDir, LUMENFLOW_CONFIG_FILE);
161
+ const content = fs.readFileSync(configPath, 'utf-8');
162
+ // When not in a git repo, should default to requireRemote: false for safety
163
+ expect(content).toContain('requireRemote: false');
164
+ });
165
+ });
166
+ describe('AC: Default lane-inference template includes Core and Feature parent lanes', () => {
167
+ it('should include Core as a parent lane', async () => {
168
+ await scaffoldProject(tempDir, getArc42Options());
169
+ const laneInferencePath = path.join(tempDir, LANE_INFERENCE_FILE);
170
+ expect(fs.existsSync(laneInferencePath)).toBe(true);
171
+ const content = fs.readFileSync(laneInferencePath, 'utf-8');
172
+ // Should have Core as a top-level parent lane (not just Framework: Core)
173
+ expect(content).toMatch(/^Core:/m);
174
+ });
175
+ it('should include Feature as a parent lane', async () => {
176
+ await scaffoldProject(tempDir, getArc42Options());
177
+ const laneInferencePath = path.join(tempDir, LANE_INFERENCE_FILE);
178
+ const content = fs.readFileSync(laneInferencePath, 'utf-8');
179
+ // Should have Feature as a top-level parent lane
180
+ expect(content).toMatch(/^Feature:/m);
181
+ });
182
+ it('should support intuitive lane names like "Core: Platform"', async () => {
183
+ await scaffoldProject(tempDir, getArc42Options());
184
+ const laneInferencePath = path.join(tempDir, LANE_INFERENCE_FILE);
185
+ const content = fs.readFileSync(laneInferencePath, 'utf-8');
186
+ // Should have sublanes under Core and Feature
187
+ // e.g., Core: followed by sublanes like Platform, Library, etc.
188
+ expect(content).toMatch(/Core:\n\s+\w+:/m);
189
+ expect(content).toMatch(/Feature:\n\s+\w+:/m);
190
+ });
191
+ });
192
+ describe('AC: LUMENFLOW.md mentions initiatives and when to use them', () => {
193
+ it('should mention initiatives in generated LUMENFLOW.md', async () => {
194
+ await scaffoldProject(tempDir, getArc42Options());
195
+ const lumenflowPath = path.join(tempDir, 'LUMENFLOW.md');
196
+ const content = fs.readFileSync(lumenflowPath, 'utf-8');
197
+ expect(content).toMatch(/initiative/i);
198
+ });
199
+ it('should explain when to use initiatives vs standalone WUs', async () => {
200
+ await scaffoldProject(tempDir, getArc42Options());
201
+ const lumenflowPath = path.join(tempDir, 'LUMENFLOW.md');
202
+ const content = fs.readFileSync(lumenflowPath, 'utf-8');
203
+ // Should mention when to use initiatives
204
+ expect(content).toMatch(/multi-phase|product vision|larger|complex/i);
205
+ });
206
+ it('should reference initiative:create command', async () => {
207
+ await scaffoldProject(tempDir, getArc42Options());
208
+ const lumenflowPath = path.join(tempDir, 'LUMENFLOW.md');
209
+ const content = fs.readFileSync(lumenflowPath, 'utf-8');
210
+ expect(content).toContain('initiative:create');
211
+ });
212
+ });
213
+ describe('AC: Init output includes initiative-first workflow guidance', () => {
214
+ // This test verifies the console output, which requires capturing stdout
215
+ // We'll mock console.log to capture the output
216
+ it('should print initiative-first guidance in init output', async () => {
217
+ const consoleLogs = [];
218
+ const originalLog = console.log;
219
+ console.log = (...args) => {
220
+ consoleLogs.push(args.join(' '));
221
+ };
222
+ try {
223
+ // Import and run main() to capture console output
224
+ const { main } = await import('../init.js');
225
+ // Change to temp directory and run init
226
+ const originalCwd = process.cwd();
227
+ process.chdir(tempDir);
228
+ // Mock process.argv for parseInitOptions
229
+ const originalArgv = process.argv;
230
+ process.argv = ['node', 'init', '--full'];
231
+ try {
232
+ await main();
233
+ }
234
+ finally {
235
+ process.argv = originalArgv;
236
+ process.chdir(originalCwd);
237
+ }
238
+ const output = consoleLogs.join('\n');
239
+ // Should mention initiatives in the "Next steps" or guidance section
240
+ expect(output).toMatch(/initiative|product vision|INIT-/i);
241
+ }
242
+ finally {
243
+ console.log = originalLog;
244
+ }
245
+ });
246
+ });
247
+ });
@@ -105,7 +105,6 @@ describe('quick-ref commands', () => {
105
105
  const quickRefPath = getQuickRefPath();
106
106
  const content = fs.readFileSync(quickRefPath, 'utf-8');
107
107
  // Should have a project setup section
108
- // eslint-disable-next-line sonarjs/slow-regex -- Simple alternation, no backtracking risk
109
108
  expect(content).toMatch(/##.*?Setup|##.*?Project/i);
110
109
  expect(content).toContain('lumenflow init');
111
110
  });
@@ -75,7 +75,6 @@ describe('template portability', () => {
75
75
  if (fs.existsSync(startingPromptPath)) {
76
76
  const content = fs.readFileSync(startingPromptPath, 'utf-8');
77
77
  // Should use relative paths like ../../../../../../LUMENFLOW.md
78
- // eslint-disable-next-line sonarjs/slow-regex -- Simple path pattern, no backtracking risk
79
78
  expect(content).toMatch(/\[.*?\]\([./]+.*?\.md\)/);
80
79
  }
81
80
  });