@lumenflow/cli 2.6.0 → 2.8.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 +120 -105
- package/dist/__tests__/agent-spawn-coordination.test.js +451 -0
- package/dist/__tests__/commands/integrate.test.js +165 -0
- package/dist/__tests__/gates-config.test.js +0 -1
- package/dist/__tests__/hooks/enforcement.test.js +279 -0
- package/dist/__tests__/init-greenfield.test.js +247 -0
- package/dist/__tests__/init-quick-ref.test.js +0 -1
- package/dist/__tests__/init-template-portability.test.js +0 -1
- package/dist/__tests__/init.test.js +27 -0
- package/dist/__tests__/initiative-e2e.test.js +442 -0
- package/dist/__tests__/initiative-plan-replacement.test.js +0 -1
- package/dist/__tests__/memory-integration.test.js +333 -0
- package/dist/__tests__/release.test.js +1 -1
- package/dist/__tests__/safe-git.test.js +0 -1
- package/dist/__tests__/state-doctor.test.js +54 -0
- package/dist/__tests__/sync-templates.test.js +255 -0
- package/dist/__tests__/wu-create-required-fields.test.js +121 -0
- package/dist/__tests__/wu-done-auto-cleanup.test.js +135 -0
- package/dist/__tests__/wu-lifecycle-integration.test.js +388 -0
- package/dist/__tests__/wu-proto.test.js +97 -0
- package/dist/backlog-prune.js +0 -1
- package/dist/cli-entry-point.js +0 -1
- package/dist/commands/integrate.js +229 -0
- package/dist/docs-sync.js +46 -0
- package/dist/doctor.js +0 -2
- package/dist/gates.js +0 -7
- package/dist/hooks/enforcement-checks.js +209 -0
- package/dist/hooks/enforcement-generator.js +365 -0
- package/dist/hooks/enforcement-sync.js +243 -0
- package/dist/hooks/index.js +7 -0
- package/dist/init.js +266 -11
- package/dist/initiative-add-wu.js +0 -2
- package/dist/initiative-create.js +0 -3
- package/dist/initiative-edit.js +0 -5
- package/dist/initiative-plan.js +0 -1
- package/dist/initiative-remove-wu.js +0 -2
- package/dist/lane-health.js +0 -2
- package/dist/lane-suggest.js +0 -1
- package/dist/mem-checkpoint.js +0 -2
- package/dist/mem-cleanup.js +0 -2
- package/dist/mem-context.js +0 -3
- package/dist/mem-create.js +0 -2
- package/dist/mem-delete.js +0 -3
- package/dist/mem-inbox.js +0 -2
- package/dist/mem-index.js +0 -1
- package/dist/mem-init.js +0 -2
- package/dist/mem-profile.js +0 -1
- package/dist/mem-promote.js +0 -1
- package/dist/mem-ready.js +0 -2
- package/dist/mem-signal.js +0 -2
- package/dist/mem-start.js +0 -2
- package/dist/mem-summarize.js +0 -2
- package/dist/metrics-cli.js +1 -1
- package/dist/metrics-snapshot.js +1 -1
- package/dist/onboarding-smoke-test.js +0 -5
- package/dist/orchestrate-init-status.js +0 -1
- package/dist/orchestrate-initiative.js +0 -1
- package/dist/orchestrate-monitor.js +0 -1
- package/dist/plan-create.js +0 -2
- package/dist/plan-edit.js +0 -2
- package/dist/plan-link.js +0 -2
- package/dist/plan-promote.js +0 -2
- package/dist/signal-cleanup.js +0 -4
- package/dist/state-bootstrap.js +0 -1
- package/dist/state-cleanup.js +0 -4
- package/dist/state-doctor-fix.js +5 -8
- package/dist/state-doctor.js +0 -11
- package/dist/sync-templates.js +188 -34
- package/dist/wu-block.js +100 -48
- package/dist/wu-claim.js +1 -22
- package/dist/wu-cleanup.js +0 -1
- package/dist/wu-create.js +0 -2
- package/dist/wu-done-auto-cleanup.js +139 -0
- package/dist/wu-done.js +11 -4
- package/dist/wu-edit.js +0 -12
- package/dist/wu-preflight.js +0 -1
- package/dist/wu-prep.js +0 -1
- package/dist/wu-proto.js +329 -0
- package/dist/wu-spawn.js +0 -3
- package/dist/wu-unblock.js +0 -2
- package/dist/wu-validate.js +0 -1
- package/package.json +9 -7
- package/templates/core/.husky/pre-commit.template +93 -0
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +27 -0
- package/templates/core/ai/onboarding/rapid-prototyping.md +143 -0
- package/templates/core/ai/onboarding/starting-prompt.md.template +3 -3
- package/templates/vendors/claude/.claude/CLAUDE.md.template +25 -0
- package/templates/vendors/claude/.claude/hooks/enforce-worktree.sh +135 -0
|
@@ -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
|
});
|
|
@@ -491,4 +491,31 @@ describe('lumenflow init', () => {
|
|
|
491
491
|
});
|
|
492
492
|
});
|
|
493
493
|
});
|
|
494
|
+
// WU-1362: Branch guard tests for init.ts
|
|
495
|
+
describe('WU-1362: branch guard for tracked file writes', () => {
|
|
496
|
+
it('should block scaffold when on main branch and targeting main checkout', async () => {
|
|
497
|
+
// This test verifies that scaffoldProject checks branch before writing
|
|
498
|
+
// Note: This test uses a temp directory (not on main), so it should pass
|
|
499
|
+
// The actual blocking only applies when targeting main checkout on main branch
|
|
500
|
+
const options = {
|
|
501
|
+
force: false,
|
|
502
|
+
full: false,
|
|
503
|
+
};
|
|
504
|
+
// Since we're in a temp dir, not on main branch, this should work
|
|
505
|
+
const result = await scaffoldProject(tempDir, options);
|
|
506
|
+
expect(result.created.length).toBeGreaterThan(0);
|
|
507
|
+
});
|
|
508
|
+
it('should allow scaffold in worktree directory', async () => {
|
|
509
|
+
// Simulate worktree-like path by creating directory structure
|
|
510
|
+
const worktreePath = path.join(tempDir, 'worktrees', 'operations-wu-999');
|
|
511
|
+
fs.mkdirSync(worktreePath, { recursive: true });
|
|
512
|
+
const options = {
|
|
513
|
+
force: false,
|
|
514
|
+
full: false,
|
|
515
|
+
};
|
|
516
|
+
// Should succeed when in worktree-like path
|
|
517
|
+
const result = await scaffoldProject(worktreePath, options);
|
|
518
|
+
expect(result.created.length).toBeGreaterThan(0);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
494
521
|
});
|