@lumenflow/cli 2.2.2 → 2.3.2
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 +147 -57
- package/dist/__tests__/agent-log-issue.test.js +56 -0
- package/dist/__tests__/cli-entry-point.test.js +66 -17
- package/dist/__tests__/cli-subprocess.test.js +25 -0
- package/dist/__tests__/init.test.js +298 -0
- package/dist/__tests__/initiative-plan.test.js +340 -0
- package/dist/__tests__/mem-cleanup-execution.test.js +19 -0
- package/dist/__tests__/merge-block.test.js +220 -0
- package/dist/__tests__/release.test.js +61 -0
- package/dist/__tests__/safe-git.test.js +191 -0
- package/dist/__tests__/state-doctor.test.js +274 -0
- package/dist/__tests__/wu-done.test.js +36 -0
- package/dist/__tests__/wu-edit.test.js +119 -0
- package/dist/__tests__/wu-prep.test.js +108 -0
- package/dist/agent-issues-query.js +4 -3
- package/dist/agent-log-issue.js +25 -4
- package/dist/backlog-prune.js +5 -4
- package/dist/cli-entry-point.js +11 -1
- package/dist/doctor.js +368 -0
- package/dist/flow-bottlenecks.js +6 -5
- package/dist/flow-report.js +4 -3
- package/dist/gates.js +356 -101
- package/dist/guard-locked.js +4 -3
- package/dist/guard-worktree-commit.js +4 -3
- package/dist/init.js +517 -86
- package/dist/initiative-add-wu.js +4 -3
- package/dist/initiative-bulk-assign-wus.js +8 -5
- package/dist/initiative-create.js +73 -37
- package/dist/initiative-edit.js +37 -21
- package/dist/initiative-list.js +4 -3
- package/dist/initiative-plan.js +337 -0
- package/dist/initiative-status.js +4 -3
- package/dist/lane-health.js +377 -0
- package/dist/lane-suggest.js +382 -0
- package/dist/mem-checkpoint.js +2 -2
- package/dist/mem-cleanup.js +2 -2
- package/dist/mem-context.js +306 -0
- package/dist/mem-create.js +2 -2
- package/dist/mem-delete.js +293 -0
- package/dist/mem-inbox.js +2 -2
- package/dist/mem-index.js +211 -0
- package/dist/mem-init.js +1 -1
- package/dist/mem-profile.js +207 -0
- package/dist/mem-promote.js +254 -0
- package/dist/mem-ready.js +2 -2
- package/dist/mem-signal.js +2 -2
- package/dist/mem-start.js +2 -2
- package/dist/mem-summarize.js +2 -2
- package/dist/mem-triage.js +2 -2
- package/dist/merge-block.js +222 -0
- package/dist/metrics-cli.js +7 -4
- package/dist/metrics-snapshot.js +4 -3
- package/dist/orchestrate-initiative.js +10 -4
- package/dist/orchestrate-monitor.js +379 -31
- package/dist/release.js +69 -29
- package/dist/signal-cleanup.js +296 -0
- package/dist/spawn-list.js +6 -5
- package/dist/state-bootstrap.js +5 -4
- package/dist/state-cleanup.js +360 -0
- package/dist/state-doctor-fix.js +196 -0
- package/dist/state-doctor.js +501 -0
- package/dist/validate-agent-skills.js +4 -3
- package/dist/validate-agent-sync.js +4 -3
- package/dist/validate-backlog-sync.js +4 -3
- package/dist/validate-skills-spec.js +4 -3
- package/dist/validate.js +4 -3
- package/dist/wu-block.js +3 -3
- package/dist/wu-claim.js +208 -98
- package/dist/wu-cleanup.js +5 -4
- package/dist/wu-create.js +71 -46
- package/dist/wu-delete.js +88 -60
- package/dist/wu-deps.js +6 -5
- package/dist/wu-done-check.js +34 -0
- package/dist/wu-done.js +39 -12
- package/dist/wu-edit.js +63 -28
- package/dist/wu-infer-lane.js +7 -6
- package/dist/wu-preflight.js +23 -81
- package/dist/wu-prep.js +125 -0
- package/dist/wu-prune.js +4 -3
- package/dist/wu-recover.js +88 -22
- package/dist/wu-repair.js +7 -6
- package/dist/wu-spawn.js +226 -270
- package/dist/wu-status.js +4 -3
- package/dist/wu-unblock.js +5 -5
- package/dist/wu-unlock-lane.js +4 -3
- package/dist/wu-validate.js +5 -4
- package/package.json +16 -7
- package/templates/core/.lumenflow/constraints.md.template +192 -0
- package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
- package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
- package/templates/core/AGENTS.md.template +60 -0
- package/templates/core/LUMENFLOW.md.template +255 -0
- package/templates/core/UPGRADING.md.template +121 -0
- package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
- package/templates/core/ai/onboarding/release-process.md.template +362 -0
- package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
- package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
- package/templates/vendors/aider/.aider.conf.yml.template +27 -0
- package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
- package/templates/vendors/claude/.claude/settings.json.template +49 -0
- package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
- package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
- package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
- package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
- package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
- package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
- package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
- package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
- package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
- package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
- package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
- package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
- package/templates/vendors/cline/.clinerules.template +53 -0
- package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
- package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
- package/templates/vendors/windsurf/.windsurf/rules/lumenflow.md.template +34 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Doctor CLI Tests (WU-1230)
|
|
3
|
+
*
|
|
4
|
+
* Tests for state:doctor --fix functionality:
|
|
5
|
+
* - Micro-worktree isolation for all tracked file changes
|
|
6
|
+
* - Removal of stale WU references from backlog.md and status.md
|
|
7
|
+
* - Changes pushed via merge, not direct file modification
|
|
8
|
+
*/
|
|
9
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
10
|
+
import { join } from 'node:path';
|
|
11
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
/**
|
|
14
|
+
* Mocked modules
|
|
15
|
+
*/
|
|
16
|
+
vi.mock('@lumenflow/core/dist/micro-worktree.js', () => ({
|
|
17
|
+
withMicroWorktree: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
vi.mock('@lumenflow/core/dist/git-adapter.js', () => ({
|
|
20
|
+
getGitForCwd: vi.fn(() => ({
|
|
21
|
+
fetch: vi.fn(),
|
|
22
|
+
merge: vi.fn(),
|
|
23
|
+
push: vi.fn(),
|
|
24
|
+
})),
|
|
25
|
+
createGitForPath: vi.fn(() => ({
|
|
26
|
+
add: vi.fn(),
|
|
27
|
+
commit: vi.fn(),
|
|
28
|
+
push: vi.fn(),
|
|
29
|
+
})),
|
|
30
|
+
}));
|
|
31
|
+
/**
|
|
32
|
+
* Import after mocks are set up
|
|
33
|
+
*/
|
|
34
|
+
import { withMicroWorktree } from '@lumenflow/core/dist/micro-worktree.js';
|
|
35
|
+
/**
|
|
36
|
+
* Constants for test paths
|
|
37
|
+
*/
|
|
38
|
+
const LUMENFLOW_DIR = '.lumenflow';
|
|
39
|
+
const STATE_DIR = 'state';
|
|
40
|
+
const STAMPS_DIR = 'stamps';
|
|
41
|
+
const MEMORY_DIR = 'memory';
|
|
42
|
+
const DOCS_TASKS_DIR = 'docs/04-operations/tasks';
|
|
43
|
+
const BACKLOG_PATH = `${DOCS_TASKS_DIR}/backlog.md`;
|
|
44
|
+
const STATUS_PATH = `${DOCS_TASKS_DIR}/status.md`;
|
|
45
|
+
/**
|
|
46
|
+
* Test directory path
|
|
47
|
+
*/
|
|
48
|
+
let testDir;
|
|
49
|
+
describe('state-doctor CLI (WU-1230)', () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
// Create temp directory for each test
|
|
53
|
+
testDir = mkdtempSync(join(tmpdir(), 'state-doctor-test-'));
|
|
54
|
+
});
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
// Cleanup temp directory
|
|
57
|
+
try {
|
|
58
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Ignore cleanup errors
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
describe('micro-worktree isolation', () => {
|
|
65
|
+
it('should use micro-worktree when --fix modifies tracked files', async () => {
|
|
66
|
+
// Setup: Create test state with broken events
|
|
67
|
+
setupTestState(testDir, {
|
|
68
|
+
wus: [],
|
|
69
|
+
events: [{ wuId: 'WU-999', type: 'claimed', timestamp: new Date().toISOString() }],
|
|
70
|
+
});
|
|
71
|
+
// Mock withMicroWorktree to track that it was called
|
|
72
|
+
const mockWithMicroWorktree = vi.mocked(withMicroWorktree);
|
|
73
|
+
mockWithMicroWorktree.mockImplementation(async (options) => {
|
|
74
|
+
// Execute the callback to simulate micro-worktree operations
|
|
75
|
+
const result = await options.execute({
|
|
76
|
+
worktreePath: testDir,
|
|
77
|
+
gitWorktree: {
|
|
78
|
+
add: vi.fn(),
|
|
79
|
+
addWithDeletions: vi.fn(),
|
|
80
|
+
commit: vi.fn(),
|
|
81
|
+
push: vi.fn(),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
return { ...result, ref: 'main' };
|
|
85
|
+
});
|
|
86
|
+
// Import and run the fix function
|
|
87
|
+
const { createStateDoctorFixDeps } = await import('../state-doctor-fix.js');
|
|
88
|
+
const deps = createStateDoctorFixDeps(testDir);
|
|
89
|
+
// When: Attempt to remove a broken event
|
|
90
|
+
await deps.removeEvent('WU-999');
|
|
91
|
+
// Then: micro-worktree should have been used
|
|
92
|
+
expect(mockWithMicroWorktree).toHaveBeenCalled();
|
|
93
|
+
expect(mockWithMicroWorktree).toHaveBeenCalledWith(expect.objectContaining({
|
|
94
|
+
operation: 'state-doctor',
|
|
95
|
+
pushOnly: true,
|
|
96
|
+
}));
|
|
97
|
+
});
|
|
98
|
+
it('should not directly modify files on main when --fix is used', async () => {
|
|
99
|
+
// Setup: Create test state with events file
|
|
100
|
+
const eventsPath = join(testDir, LUMENFLOW_DIR, STATE_DIR, 'wu-events.jsonl');
|
|
101
|
+
setupTestState(testDir, {
|
|
102
|
+
wus: [],
|
|
103
|
+
events: [{ wuId: 'WU-999', type: 'claimed', timestamp: new Date().toISOString() }],
|
|
104
|
+
});
|
|
105
|
+
const originalContent = readFileSync(eventsPath, 'utf-8');
|
|
106
|
+
// Mock withMicroWorktree to NOT actually modify files (simulating push-only mode)
|
|
107
|
+
const mockWithMicroWorktree = vi.mocked(withMicroWorktree);
|
|
108
|
+
mockWithMicroWorktree.mockResolvedValue({
|
|
109
|
+
commitMessage: 'fix: remove broken events',
|
|
110
|
+
files: ['.lumenflow/state/wu-events.jsonl'],
|
|
111
|
+
ref: 'main',
|
|
112
|
+
});
|
|
113
|
+
// Import and run the fix function
|
|
114
|
+
const { createStateDoctorFixDeps } = await import('../state-doctor-fix.js');
|
|
115
|
+
const deps = createStateDoctorFixDeps(testDir);
|
|
116
|
+
// When: Remove broken event
|
|
117
|
+
await deps.removeEvent('WU-999');
|
|
118
|
+
// Then: Original file on main should be unchanged
|
|
119
|
+
// (changes only happen in micro-worktree and pushed)
|
|
120
|
+
const currentContent = readFileSync(eventsPath, 'utf-8');
|
|
121
|
+
expect(currentContent).toBe(originalContent);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe('backlog.md and status.md cleanup', () => {
|
|
125
|
+
it('should remove stale WU references from backlog.md when removing broken events', async () => {
|
|
126
|
+
// Setup: Create backlog.md with reference to WU that will be removed
|
|
127
|
+
setupTestState(testDir, {
|
|
128
|
+
wus: [],
|
|
129
|
+
events: [{ wuId: 'WU-999', type: 'claimed', timestamp: new Date().toISOString() }],
|
|
130
|
+
backlog: `# Backlog
|
|
131
|
+
|
|
132
|
+
## In Progress
|
|
133
|
+
|
|
134
|
+
- WU-999: Some old WU that no longer exists
|
|
135
|
+
|
|
136
|
+
## Ready
|
|
137
|
+
|
|
138
|
+
- WU-100: Valid WU
|
|
139
|
+
`,
|
|
140
|
+
});
|
|
141
|
+
// Track the files that would be modified in micro-worktree
|
|
142
|
+
let capturedFiles = [];
|
|
143
|
+
const mockWithMicroWorktree = vi.mocked(withMicroWorktree);
|
|
144
|
+
mockWithMicroWorktree.mockImplementation(async (options) => {
|
|
145
|
+
const result = await options.execute({
|
|
146
|
+
worktreePath: testDir,
|
|
147
|
+
gitWorktree: {
|
|
148
|
+
add: vi.fn(),
|
|
149
|
+
addWithDeletions: vi.fn(),
|
|
150
|
+
commit: vi.fn(),
|
|
151
|
+
push: vi.fn(),
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
capturedFiles = result.files;
|
|
155
|
+
return { ...result, ref: 'main' };
|
|
156
|
+
});
|
|
157
|
+
// Import and run the fix function
|
|
158
|
+
const { createStateDoctorFixDeps } = await import('../state-doctor-fix.js');
|
|
159
|
+
const deps = createStateDoctorFixDeps(testDir);
|
|
160
|
+
// When: Remove broken event for WU-999
|
|
161
|
+
await deps.removeEvent('WU-999');
|
|
162
|
+
// Then: backlog.md should be in the list of modified files
|
|
163
|
+
expect(capturedFiles).toContain(BACKLOG_PATH);
|
|
164
|
+
});
|
|
165
|
+
it('should remove stale WU references from status.md when removing broken events', async () => {
|
|
166
|
+
// Setup: Create status.md with reference to WU that will be removed
|
|
167
|
+
setupTestState(testDir, {
|
|
168
|
+
wus: [],
|
|
169
|
+
events: [{ wuId: 'WU-999', type: 'claimed', timestamp: new Date().toISOString() }],
|
|
170
|
+
status: `# Status
|
|
171
|
+
|
|
172
|
+
## In Progress
|
|
173
|
+
|
|
174
|
+
| Lane | WU | Title |
|
|
175
|
+
|------|-----|-------|
|
|
176
|
+
| Framework: CLI | WU-999 | Old WU |
|
|
177
|
+
`,
|
|
178
|
+
});
|
|
179
|
+
// Track the files that would be modified
|
|
180
|
+
let capturedFiles = [];
|
|
181
|
+
const mockWithMicroWorktree = vi.mocked(withMicroWorktree);
|
|
182
|
+
mockWithMicroWorktree.mockImplementation(async (options) => {
|
|
183
|
+
const result = await options.execute({
|
|
184
|
+
worktreePath: testDir,
|
|
185
|
+
gitWorktree: {
|
|
186
|
+
add: vi.fn(),
|
|
187
|
+
addWithDeletions: vi.fn(),
|
|
188
|
+
commit: vi.fn(),
|
|
189
|
+
push: vi.fn(),
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
capturedFiles = result.files;
|
|
193
|
+
return { ...result, ref: 'main' };
|
|
194
|
+
});
|
|
195
|
+
// Import and run the fix function
|
|
196
|
+
const { createStateDoctorFixDeps } = await import('../state-doctor-fix.js');
|
|
197
|
+
const deps = createStateDoctorFixDeps(testDir);
|
|
198
|
+
// When: Remove broken event for WU-999
|
|
199
|
+
await deps.removeEvent('WU-999');
|
|
200
|
+
// Then: status.md should be in the list of modified files
|
|
201
|
+
expect(capturedFiles).toContain(STATUS_PATH);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
describe('commit and push behavior', () => {
|
|
205
|
+
it('should use pushOnly mode to avoid modifying local main', async () => {
|
|
206
|
+
setupTestState(testDir, {
|
|
207
|
+
wus: [],
|
|
208
|
+
events: [{ wuId: 'WU-999', type: 'claimed', timestamp: new Date().toISOString() }],
|
|
209
|
+
});
|
|
210
|
+
const mockWithMicroWorktree = vi.mocked(withMicroWorktree);
|
|
211
|
+
mockWithMicroWorktree.mockImplementation(async (options) => {
|
|
212
|
+
// Verify pushOnly is set
|
|
213
|
+
expect(options.pushOnly).toBe(true);
|
|
214
|
+
const result = await options.execute({
|
|
215
|
+
worktreePath: testDir,
|
|
216
|
+
gitWorktree: {
|
|
217
|
+
add: vi.fn(),
|
|
218
|
+
addWithDeletions: vi.fn(),
|
|
219
|
+
commit: vi.fn(),
|
|
220
|
+
push: vi.fn(),
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
return { ...result, ref: 'main' };
|
|
224
|
+
});
|
|
225
|
+
const { createStateDoctorFixDeps } = await import('../state-doctor-fix.js');
|
|
226
|
+
const deps = createStateDoctorFixDeps(testDir);
|
|
227
|
+
await deps.removeEvent('WU-999');
|
|
228
|
+
// The assertion is inside the mock - if we get here without error, pushOnly was true
|
|
229
|
+
expect(mockWithMicroWorktree).toHaveBeenCalled();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
function setupTestState(baseDir, state) {
|
|
234
|
+
// Create directories
|
|
235
|
+
const dirs = [
|
|
236
|
+
join(baseDir, LUMENFLOW_DIR, STATE_DIR),
|
|
237
|
+
join(baseDir, LUMENFLOW_DIR, STAMPS_DIR),
|
|
238
|
+
join(baseDir, LUMENFLOW_DIR, MEMORY_DIR),
|
|
239
|
+
join(baseDir, DOCS_TASKS_DIR, 'wu'),
|
|
240
|
+
];
|
|
241
|
+
for (const dir of dirs) {
|
|
242
|
+
mkdirSync(dir, { recursive: true });
|
|
243
|
+
}
|
|
244
|
+
// Create events file
|
|
245
|
+
if (state.events && state.events.length > 0) {
|
|
246
|
+
const eventsPath = join(baseDir, LUMENFLOW_DIR, STATE_DIR, 'wu-events.jsonl');
|
|
247
|
+
const content = state.events.map((e) => JSON.stringify(e)).join('\n') + '\n';
|
|
248
|
+
writeFileSync(eventsPath, content, 'utf-8');
|
|
249
|
+
}
|
|
250
|
+
// Create signals file
|
|
251
|
+
if (state.signals && state.signals.length > 0) {
|
|
252
|
+
const signalsPath = join(baseDir, LUMENFLOW_DIR, MEMORY_DIR, 'signals.jsonl');
|
|
253
|
+
const content = state.signals.map((s) => JSON.stringify(s)).join('\n') + '\n';
|
|
254
|
+
writeFileSync(signalsPath, content, 'utf-8');
|
|
255
|
+
}
|
|
256
|
+
// Create WU YAML files
|
|
257
|
+
if (state.wus) {
|
|
258
|
+
for (const wu of state.wus) {
|
|
259
|
+
const wuPath = join(baseDir, DOCS_TASKS_DIR, 'wu', `${wu.id}.yaml`);
|
|
260
|
+
const content = `id: ${wu.id}\nstatus: ${wu.status}\ntitle: ${wu.title || wu.id}\n`;
|
|
261
|
+
writeFileSync(wuPath, content, 'utf-8');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Create backlog.md
|
|
265
|
+
if (state.backlog) {
|
|
266
|
+
const backlogPath = join(baseDir, BACKLOG_PATH);
|
|
267
|
+
writeFileSync(backlogPath, state.backlog, 'utf-8');
|
|
268
|
+
}
|
|
269
|
+
// Create status.md
|
|
270
|
+
if (state.status) {
|
|
271
|
+
const statusPath = join(baseDir, STATUS_PATH);
|
|
272
|
+
writeFileSync(statusPath, state.status, 'utf-8');
|
|
273
|
+
}
|
|
274
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { ensureCleanWorktree } from '../wu-done-check.js';
|
|
3
|
+
import * as gitAdapter from '@lumenflow/core/git-adapter';
|
|
4
|
+
import * as errorHandler from '@lumenflow/core/error-handler';
|
|
5
|
+
// Mock dependencies
|
|
6
|
+
vi.mock('@lumenflow/core/git-adapter');
|
|
7
|
+
vi.mock('@lumenflow/core/error-handler');
|
|
8
|
+
describe('wu-done', () => {
|
|
9
|
+
describe('ensureCleanWorktree', () => {
|
|
10
|
+
let mockGit;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
vi.resetAllMocks();
|
|
13
|
+
mockGit = {
|
|
14
|
+
getStatus: vi.fn(),
|
|
15
|
+
};
|
|
16
|
+
vi.mocked(gitAdapter.createGitForPath).mockReturnValue(mockGit);
|
|
17
|
+
});
|
|
18
|
+
it('should pass if worktree is clean', async () => {
|
|
19
|
+
mockGit.getStatus.mockResolvedValue(''); // Clean status
|
|
20
|
+
await ensureCleanWorktree('/path/to/worktree');
|
|
21
|
+
expect(mockGit.getStatus).toHaveBeenCalled();
|
|
22
|
+
expect(errorHandler.die).not.toHaveBeenCalled();
|
|
23
|
+
});
|
|
24
|
+
it('should die if worktree has uncommitted changes', async () => {
|
|
25
|
+
mockGit.getStatus.mockResolvedValue('M file.ts\n?? new-file.ts'); // Dirty status
|
|
26
|
+
await ensureCleanWorktree('/path/to/worktree');
|
|
27
|
+
expect(mockGit.getStatus).toHaveBeenCalled();
|
|
28
|
+
expect(errorHandler.die).toHaveBeenCalledWith(expect.stringContaining('Worktree has uncommitted changes'));
|
|
29
|
+
});
|
|
30
|
+
it('should use the correct worktree path', async () => {
|
|
31
|
+
mockGit.getStatus.mockResolvedValue('');
|
|
32
|
+
await ensureCleanWorktree('/custom/worktree/path');
|
|
33
|
+
expect(gitAdapter.createGitForPath).toHaveBeenCalledWith('/custom/worktree/path');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU-1225: Tests for wu-edit append-by-default behavior
|
|
3
|
+
*
|
|
4
|
+
* Validates that array fields (code_paths, risks, acceptance, etc.)
|
|
5
|
+
* now append by default instead of replacing, making behavior consistent
|
|
6
|
+
* across all array options.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { applyEdits, mergeStringField } from '../wu-edit.js';
|
|
10
|
+
describe('wu-edit applyEdits', () => {
|
|
11
|
+
describe('WU-1225: code_paths append-by-default', () => {
|
|
12
|
+
const baseWU = {
|
|
13
|
+
id: 'WU-1225',
|
|
14
|
+
status: 'ready',
|
|
15
|
+
code_paths: ['existing/path.ts'],
|
|
16
|
+
};
|
|
17
|
+
it('appends code_paths by default (no flags)', () => {
|
|
18
|
+
const opts = { codePaths: ['new/path.ts'] };
|
|
19
|
+
const result = applyEdits(baseWU, opts);
|
|
20
|
+
expect(result.code_paths).toEqual(['existing/path.ts', 'new/path.ts']);
|
|
21
|
+
});
|
|
22
|
+
it('appends code_paths when --append is set (backwards compat)', () => {
|
|
23
|
+
const opts = { codePaths: ['new/path.ts'], append: true };
|
|
24
|
+
const result = applyEdits(baseWU, opts);
|
|
25
|
+
expect(result.code_paths).toEqual(['existing/path.ts', 'new/path.ts']);
|
|
26
|
+
});
|
|
27
|
+
it('replaces code_paths when --replace-code-paths is set', () => {
|
|
28
|
+
const opts = { codePaths: ['new/path.ts'], replaceCodePaths: true };
|
|
29
|
+
const result = applyEdits(baseWU, opts);
|
|
30
|
+
expect(result.code_paths).toEqual(['new/path.ts']);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
describe('WU-1225: risks append-by-default', () => {
|
|
34
|
+
const baseWU = {
|
|
35
|
+
id: 'WU-1225',
|
|
36
|
+
status: 'ready',
|
|
37
|
+
risks: ['existing risk'],
|
|
38
|
+
};
|
|
39
|
+
it('appends risks by default', () => {
|
|
40
|
+
const opts = { risks: ['new risk'] };
|
|
41
|
+
const result = applyEdits(baseWU, opts);
|
|
42
|
+
expect(result.risks).toEqual(['existing risk', 'new risk']);
|
|
43
|
+
});
|
|
44
|
+
it('replaces risks when --replace-risks is set', () => {
|
|
45
|
+
const opts = { risks: ['new risk'], replaceRisks: true };
|
|
46
|
+
const result = applyEdits(baseWU, opts);
|
|
47
|
+
expect(result.risks).toEqual(['new risk']);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('WU-1225: blocked_by append-by-default', () => {
|
|
51
|
+
const baseWU = {
|
|
52
|
+
id: 'WU-1225',
|
|
53
|
+
status: 'ready',
|
|
54
|
+
blocked_by: ['WU-100'],
|
|
55
|
+
};
|
|
56
|
+
it('appends blocked_by by default', () => {
|
|
57
|
+
const opts = { blockedBy: 'WU-200' };
|
|
58
|
+
const result = applyEdits(baseWU, opts);
|
|
59
|
+
expect(result.blocked_by).toEqual(['WU-100', 'WU-200']);
|
|
60
|
+
});
|
|
61
|
+
it('replaces blocked_by when --replace-blocked-by is set', () => {
|
|
62
|
+
const opts = { blockedBy: 'WU-200', replaceBlockedBy: true };
|
|
63
|
+
const result = applyEdits(baseWU, opts);
|
|
64
|
+
expect(result.blocked_by).toEqual(['WU-200']);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe('WU-1225: dependencies append-by-default', () => {
|
|
68
|
+
const baseWU = {
|
|
69
|
+
id: 'WU-1225',
|
|
70
|
+
status: 'ready',
|
|
71
|
+
dependencies: ['WU-50'],
|
|
72
|
+
};
|
|
73
|
+
it('appends dependencies by default', () => {
|
|
74
|
+
const opts = { addDep: 'WU-60' };
|
|
75
|
+
const result = applyEdits(baseWU, opts);
|
|
76
|
+
expect(result.dependencies).toEqual(['WU-50', 'WU-60']);
|
|
77
|
+
});
|
|
78
|
+
it('replaces dependencies when --replace-dependencies is set', () => {
|
|
79
|
+
const opts = { addDep: 'WU-60', replaceDependencies: true };
|
|
80
|
+
const result = applyEdits(baseWU, opts);
|
|
81
|
+
expect(result.dependencies).toEqual(['WU-60']);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('WU-1144: acceptance already appends by default', () => {
|
|
85
|
+
const baseWU = {
|
|
86
|
+
id: 'WU-1225',
|
|
87
|
+
status: 'ready',
|
|
88
|
+
acceptance: ['existing criterion'],
|
|
89
|
+
};
|
|
90
|
+
it('appends acceptance by default', () => {
|
|
91
|
+
const opts = { acceptance: ['new criterion'] };
|
|
92
|
+
const result = applyEdits(baseWU, opts);
|
|
93
|
+
expect(result.acceptance).toEqual(['existing criterion', 'new criterion']);
|
|
94
|
+
});
|
|
95
|
+
it('replaces acceptance when --replace-acceptance is set', () => {
|
|
96
|
+
const opts = { acceptance: ['new criterion'], replaceAcceptance: true };
|
|
97
|
+
const result = applyEdits(baseWU, opts);
|
|
98
|
+
expect(result.acceptance).toEqual(['new criterion']);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('wu-edit mergeStringField', () => {
|
|
103
|
+
it('appends by default', () => {
|
|
104
|
+
const result = mergeStringField('existing', 'new', false);
|
|
105
|
+
expect(result).toBe('existing\n\nnew');
|
|
106
|
+
});
|
|
107
|
+
it('replaces when shouldReplace is true', () => {
|
|
108
|
+
const result = mergeStringField('existing', 'new', true);
|
|
109
|
+
expect(result).toBe('new');
|
|
110
|
+
});
|
|
111
|
+
it('returns new value if existing is empty', () => {
|
|
112
|
+
const result = mergeStringField('', 'new', false);
|
|
113
|
+
expect(result).toBe('new');
|
|
114
|
+
});
|
|
115
|
+
it('returns new value if existing is undefined', () => {
|
|
116
|
+
const result = mergeStringField(undefined, 'new', false);
|
|
117
|
+
expect(result).toBe('new');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import * as locationResolver from '@lumenflow/core/dist/context/location-resolver.js';
|
|
3
|
+
import * as wuYaml from '@lumenflow/core/dist/wu-yaml.js';
|
|
4
|
+
import { CONTEXT_VALIDATION, WU_STATUS } from '@lumenflow/core/dist/wu-constants.js';
|
|
5
|
+
const { LOCATION_TYPES } = CONTEXT_VALIDATION;
|
|
6
|
+
// Mock dependencies
|
|
7
|
+
vi.mock('@lumenflow/core/dist/context/location-resolver.js');
|
|
8
|
+
vi.mock('@lumenflow/core/dist/error-handler.js');
|
|
9
|
+
vi.mock('@lumenflow/core/dist/wu-yaml.js');
|
|
10
|
+
vi.mock('../gates.js', () => ({
|
|
11
|
+
runGates: vi.fn().mockResolvedValue(true),
|
|
12
|
+
}));
|
|
13
|
+
describe('wu-prep (WU-1223)', () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
vi.resetAllMocks();
|
|
16
|
+
});
|
|
17
|
+
describe('location validation', () => {
|
|
18
|
+
it('should error when run from main checkout', async () => {
|
|
19
|
+
// Mock location as main checkout
|
|
20
|
+
vi.mocked(locationResolver.resolveLocation).mockResolvedValue({
|
|
21
|
+
type: LOCATION_TYPES.MAIN,
|
|
22
|
+
cwd: '/repo',
|
|
23
|
+
gitRoot: '/repo',
|
|
24
|
+
mainCheckout: '/repo',
|
|
25
|
+
worktreeName: null,
|
|
26
|
+
worktreeWuId: null,
|
|
27
|
+
});
|
|
28
|
+
// Import after mocks are set up
|
|
29
|
+
const { resolveLocation } = await import('@lumenflow/core/dist/context/location-resolver.js');
|
|
30
|
+
const location = await resolveLocation();
|
|
31
|
+
// Verify the mock returns main
|
|
32
|
+
expect(location.type).toBe(LOCATION_TYPES.MAIN);
|
|
33
|
+
});
|
|
34
|
+
it('should proceed when run from worktree', async () => {
|
|
35
|
+
// Mock location as worktree
|
|
36
|
+
vi.mocked(locationResolver.resolveLocation).mockResolvedValue({
|
|
37
|
+
type: LOCATION_TYPES.WORKTREE,
|
|
38
|
+
cwd: '/repo/worktrees/framework-cli-wu-1223',
|
|
39
|
+
gitRoot: '/repo/worktrees/framework-cli-wu-1223',
|
|
40
|
+
mainCheckout: '/repo',
|
|
41
|
+
worktreeName: 'framework-cli-wu-1223',
|
|
42
|
+
worktreeWuId: 'WU-1223',
|
|
43
|
+
});
|
|
44
|
+
const { resolveLocation } = await import('@lumenflow/core/dist/context/location-resolver.js');
|
|
45
|
+
const location = await resolveLocation();
|
|
46
|
+
// Verify the mock returns worktree
|
|
47
|
+
expect(location.type).toBe(LOCATION_TYPES.WORKTREE);
|
|
48
|
+
expect(location.mainCheckout).toBe('/repo');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe('WU status validation', () => {
|
|
52
|
+
it('should only allow in_progress WUs', async () => {
|
|
53
|
+
// Mock WU YAML with wrong status
|
|
54
|
+
const mockDoc = {
|
|
55
|
+
id: 'WU-1223',
|
|
56
|
+
status: WU_STATUS.DONE,
|
|
57
|
+
title: 'Test WU',
|
|
58
|
+
};
|
|
59
|
+
vi.mocked(wuYaml.readWU).mockReturnValue(mockDoc);
|
|
60
|
+
const { readWU } = await import('@lumenflow/core/dist/wu-yaml.js');
|
|
61
|
+
const doc = readWU('path/to/wu.yaml', 'WU-1223');
|
|
62
|
+
expect(doc.status).toBe(WU_STATUS.DONE);
|
|
63
|
+
expect(doc.status).not.toBe(WU_STATUS.IN_PROGRESS);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
describe('success message', () => {
|
|
67
|
+
it('should include copy-paste instruction with main path', async () => {
|
|
68
|
+
// The success message should include:
|
|
69
|
+
// 1. Main checkout path
|
|
70
|
+
// 2. WU ID
|
|
71
|
+
// 3. Copy-paste command: cd <main> && pnpm wu:done --id <WU-ID>
|
|
72
|
+
const mainCheckout = '/repo';
|
|
73
|
+
const wuId = 'WU-1223';
|
|
74
|
+
// Build expected command that would be in the success message
|
|
75
|
+
const expectedCommand = `cd ${mainCheckout} && pnpm wu:done --id ${wuId}`;
|
|
76
|
+
expect(expectedCommand).toBe('cd /repo && pnpm wu:done --id WU-1223');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('wu:done worktree check (WU-1223)', () => {
|
|
81
|
+
beforeEach(() => {
|
|
82
|
+
vi.resetAllMocks();
|
|
83
|
+
});
|
|
84
|
+
it('should error when run from worktree with guidance to use wu:prep', async () => {
|
|
85
|
+
// Mock location as worktree
|
|
86
|
+
vi.mocked(locationResolver.resolveLocation).mockResolvedValue({
|
|
87
|
+
type: LOCATION_TYPES.WORKTREE,
|
|
88
|
+
cwd: '/repo/worktrees/framework-cli-wu-1223',
|
|
89
|
+
gitRoot: '/repo/worktrees/framework-cli-wu-1223',
|
|
90
|
+
mainCheckout: '/repo',
|
|
91
|
+
worktreeName: 'framework-cli-wu-1223',
|
|
92
|
+
worktreeWuId: 'WU-1223',
|
|
93
|
+
});
|
|
94
|
+
const { resolveLocation } = await import('@lumenflow/core/dist/context/location-resolver.js');
|
|
95
|
+
const location = await resolveLocation();
|
|
96
|
+
// The error message should guide user to wu:prep workflow
|
|
97
|
+
expect(location.type).toBe(LOCATION_TYPES.WORKTREE);
|
|
98
|
+
// Error message should contain:
|
|
99
|
+
const errorShouldContain = [
|
|
100
|
+
'wu:prep', // Mention the new command
|
|
101
|
+
'main checkout', // Explain where wu:done should run
|
|
102
|
+
'/repo', // Main checkout path
|
|
103
|
+
];
|
|
104
|
+
// Build the expected error content
|
|
105
|
+
const expectedGuidance = `pnpm wu:prep --id WU-1223`;
|
|
106
|
+
expect(expectedGuidance).toContain('wu:prep');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -243,9 +243,10 @@ async function main() {
|
|
|
243
243
|
const issues = await readIssues(baseDir, sinceDate, opts.category, opts.severity);
|
|
244
244
|
displaySummary(issues, opts.since);
|
|
245
245
|
}
|
|
246
|
-
//
|
|
247
|
-
|
|
248
|
-
|
|
246
|
+
// WU-1181: Use import.meta.main instead of process.argv[1] comparison
|
|
247
|
+
// The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
|
|
248
|
+
// path but import.meta.url resolves to the real path - they never match
|
|
249
|
+
if (import.meta.main) {
|
|
249
250
|
main().catch((err) => {
|
|
250
251
|
die(`Issues query failed: ${err.message}`);
|
|
251
252
|
});
|
package/dist/agent-log-issue.js
CHANGED
|
@@ -6,11 +6,32 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Usage:
|
|
8
8
|
* pnpm agent:log-issue --category workflow --severity minor --title "..." --description "..."
|
|
9
|
+
* pnpm agent:log-issue --category tooling --severity major --title "..." --description "..." \
|
|
10
|
+
* --tag worktree --tag gates --file src/main.ts --file src/utils.ts
|
|
11
|
+
*
|
|
12
|
+
* WU-1182: Uses Commander.js repeatable options pattern for --tag and --file.
|
|
13
|
+
* Use --tag multiple times instead of comma-separated --tags.
|
|
9
14
|
*/
|
|
10
15
|
import { Command } from 'commander';
|
|
11
16
|
import { logIncident, getCurrentSession } from '@lumenflow/agent';
|
|
12
17
|
import { EXIT_CODES, INCIDENT_SEVERITY, LUMENFLOW_PATHS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
13
18
|
import chalk from 'chalk';
|
|
19
|
+
/**
|
|
20
|
+
* WU-1182: Collector function for Commander.js repeatable options.
|
|
21
|
+
* Accumulates multiple flag values into an array.
|
|
22
|
+
*
|
|
23
|
+
* Usage: --tag a --tag b → ['a', 'b']
|
|
24
|
+
*
|
|
25
|
+
* This follows Commander.js best practices - use repeatable pattern for
|
|
26
|
+
* multi-value options instead of comma-separated splits.
|
|
27
|
+
*
|
|
28
|
+
* @param value - New value from CLI
|
|
29
|
+
* @param previous - Previously accumulated values
|
|
30
|
+
* @returns Updated array with new value appended
|
|
31
|
+
*/
|
|
32
|
+
function collectRepeatable(value, previous) {
|
|
33
|
+
return previous.concat([value]);
|
|
34
|
+
}
|
|
14
35
|
const program = new Command()
|
|
15
36
|
.name('agent:log-issue')
|
|
16
37
|
.description('Log a workflow issue or incident')
|
|
@@ -19,9 +40,9 @@ const program = new Command()
|
|
|
19
40
|
.requiredOption('--title <title>', 'Short description (5-100 chars)')
|
|
20
41
|
.requiredOption('--description <desc>', 'Detailed context (10-2000 chars)')
|
|
21
42
|
.option('--resolution <res>', 'How the issue was resolved')
|
|
22
|
-
.option('--
|
|
43
|
+
.option('--tag <tag>', 'Tag for categorization (repeatable)', collectRepeatable, [])
|
|
23
44
|
.option('--step <step>', 'Current workflow step (e.g., wu:done, gates)')
|
|
24
|
-
.option('--
|
|
45
|
+
.option('--file <file>', 'Related file path (repeatable)', collectRepeatable, [])
|
|
25
46
|
.action(async (opts) => {
|
|
26
47
|
try {
|
|
27
48
|
const session = await getCurrentSession();
|
|
@@ -36,10 +57,10 @@ const program = new Command()
|
|
|
36
57
|
title: opts.title,
|
|
37
58
|
description: opts.description,
|
|
38
59
|
resolution: opts.resolution,
|
|
39
|
-
tags: opts.
|
|
60
|
+
tags: opts.tag,
|
|
40
61
|
context: {
|
|
41
62
|
current_step: opts.step,
|
|
42
|
-
related_files: opts.
|
|
63
|
+
related_files: opts.file,
|
|
43
64
|
},
|
|
44
65
|
};
|
|
45
66
|
await logIncident(incident);
|
package/dist/backlog-prune.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Auto-tagging stale WUs (in_progress/ready too long without activity)
|
|
7
7
|
* - Archiving old completed WUs (done for > N days)
|
|
8
8
|
*
|
|
9
|
-
* WU-1106: INIT-003 Phase 3b - Migrate from PatientPath tools/backlog-prune.
|
|
9
|
+
* WU-1106: INIT-003 Phase 3b - Migrate from PatientPath tools/backlog-prune.ts
|
|
10
10
|
*
|
|
11
11
|
* Usage:
|
|
12
12
|
* pnpm backlog:prune # Dry-run mode (shows what would be done)
|
|
@@ -291,9 +291,10 @@ async function main() {
|
|
|
291
291
|
}
|
|
292
292
|
process.exit(EXIT_CODES.SUCCESS);
|
|
293
293
|
}
|
|
294
|
-
//
|
|
295
|
-
|
|
294
|
+
// WU-1181: Use import.meta.main instead of process.argv[1] comparison
|
|
295
|
+
// The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
|
|
296
|
+
// path but import.meta.url resolves to the real path - they never match
|
|
296
297
|
import { runCLI } from './cli-entry-point.js';
|
|
297
|
-
if (
|
|
298
|
+
if (import.meta.main) {
|
|
298
299
|
runCLI(main);
|
|
299
300
|
}
|
package/dist/cli-entry-point.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable no-console -- CLI entry point uses console for error output */
|
|
1
2
|
/**
|
|
2
3
|
* Shared CLI entry point wrapper
|
|
3
4
|
*
|
|
@@ -12,6 +13,10 @@
|
|
|
12
13
|
*
|
|
13
14
|
* WU-1085: Initializes color support respecting NO_COLOR/FORCE_COLOR/--no-color
|
|
14
15
|
*
|
|
16
|
+
* WU-1233: Adds EPIPE protection for pipe resilience. When CLI output is piped
|
|
17
|
+
* through head/tail, the pipe may close before all output is written. Without
|
|
18
|
+
* this protection, Node.js throws unhandled EPIPE errors crashing the process.
|
|
19
|
+
*
|
|
15
20
|
* @example
|
|
16
21
|
* ```typescript
|
|
17
22
|
* // At the bottom of each CLI file:
|
|
@@ -23,15 +28,20 @@
|
|
|
23
28
|
* ```
|
|
24
29
|
*/
|
|
25
30
|
import { EXIT_CODES } from '@lumenflow/core/dist/wu-constants.js';
|
|
26
|
-
import { initColorSupport } from '@lumenflow/core';
|
|
31
|
+
import { initColorSupport, StreamErrorHandler } from '@lumenflow/core';
|
|
27
32
|
/**
|
|
28
33
|
* Wraps an async main function with proper error handling.
|
|
29
34
|
* WU-1085: Also initializes color support based on NO_COLOR/FORCE_COLOR/--no-color
|
|
35
|
+
* WU-1233: Attaches EPIPE handler for graceful pipe closure
|
|
30
36
|
*
|
|
31
37
|
* @param main - The async main function to execute
|
|
32
38
|
* @returns Promise that resolves when main completes (or after error handling)
|
|
33
39
|
*/
|
|
34
40
|
export async function runCLI(main) {
|
|
41
|
+
// WU-1233: Attach EPIPE handler before running command
|
|
42
|
+
// This must be done early to catch any EPIPE errors during execution
|
|
43
|
+
const streamErrorHandler = StreamErrorHandler.createWithDefaults();
|
|
44
|
+
streamErrorHandler.attach();
|
|
35
45
|
// WU-1085: Initialize color support before running command
|
|
36
46
|
initColorSupport();
|
|
37
47
|
try {
|