@lumenflow/core 2.1.2 → 2.2.1

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.
@@ -11,6 +11,9 @@
11
11
  *
12
12
  * @module system-map-validator
13
13
  */
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import fg from 'fast-glob';
16
+ import { parseYAML } from './wu-yaml.js';
14
17
  /**
15
18
  * Canonical list of valid audience tags as defined in SYSTEM-MAP.yaml header
16
19
  * @type {string[]}
@@ -270,3 +273,50 @@ export async function validateSystemMap(systemMap, deps) {
270
273
  classificationErrors,
271
274
  };
272
275
  }
276
+ const DEFAULT_SYSTEM_MAP_PATH = 'SYSTEM-MAP.yaml';
277
+ function emitErrors(label, errors) {
278
+ if (!errors || errors.length === 0)
279
+ return;
280
+ console.error(`\n${label}:`);
281
+ for (const error of errors) {
282
+ console.error(` - ${error}`);
283
+ }
284
+ }
285
+ async function runCLI() {
286
+ const systemMapPath = process.env.SYSTEM_MAP_PATH || DEFAULT_SYSTEM_MAP_PATH;
287
+ if (!existsSync(systemMapPath)) {
288
+ console.warn(`[system-map] ${systemMapPath} not found; skipping validation.`);
289
+ process.exit(0);
290
+ }
291
+ let systemMap;
292
+ try {
293
+ const raw = readFileSync(systemMapPath, 'utf-8');
294
+ systemMap = parseYAML(raw);
295
+ }
296
+ catch (error) {
297
+ const message = error instanceof Error ? error.message : String(error);
298
+ console.error(`[system-map] Failed to read or parse ${systemMapPath}: ${message}`);
299
+ process.exit(1);
300
+ }
301
+ const result = await validateSystemMap(systemMap, {
302
+ exists: (path) => existsSync(path),
303
+ glob: (pattern) => fg(pattern, { dot: false }),
304
+ });
305
+ if (!result.valid) {
306
+ console.error('\n[system-map] Validation failed');
307
+ emitErrors('Missing paths', result.pathErrors);
308
+ emitErrors('Orphan docs', result.orphanDocs);
309
+ emitErrors('Invalid audiences', result.audienceErrors);
310
+ emitErrors('Invalid quick queries', result.queryErrors);
311
+ emitErrors('Classification routing violations', result.classificationErrors);
312
+ process.exit(1);
313
+ }
314
+ console.log('[system-map] Validation passed');
315
+ process.exit(0);
316
+ }
317
+ if (import.meta.main) {
318
+ runCLI().catch((error) => {
319
+ console.error('[system-map] Validation failed:', error);
320
+ process.exit(1);
321
+ });
322
+ }
@@ -1186,6 +1186,8 @@ export declare const AUDIT_ARGS: {
1186
1186
  * Centralized paths to validation scripts.
1187
1187
  */
1188
1188
  export declare const SCRIPT_PATHS: {
1189
+ /** Gates runner */
1190
+ GATES: string;
1189
1191
  /** WU YAML validation */
1190
1192
  VALIDATE: string;
1191
1193
  /** Prompt registry validation */
@@ -12,6 +12,8 @@
12
12
  * @see {@link tools/lib/wu-schema.mjs} - PLACEHOLDER_SENTINEL (already centralized)
13
13
  */
14
14
  import path from 'node:path';
15
+ import { existsSync } from 'node:fs';
16
+ import { createRequire } from 'node:module';
15
17
  import { kebabCase } from 'change-case';
16
18
  /**
17
19
  * Git branch names
@@ -1000,16 +1002,63 @@ export const GATE_COMMANDS = {
1000
1002
  /** WU-2062: Triggers tiered test execution based on risk */
1001
1003
  TIERED_TEST: 'tiered-test',
1002
1004
  };
1005
+ const require = createRequire(import.meta.url);
1006
+ const NOOP_NODE_COMMAND = 'node -e "process.exit(0)"';
1007
+ function resolveNodeModulePath(modulePath) {
1008
+ try {
1009
+ return require.resolve(modulePath);
1010
+ }
1011
+ catch {
1012
+ return null;
1013
+ }
1014
+ }
1015
+ function resolveRepoPath(relativePath, requireExists = false) {
1016
+ const candidate = path.join(process.cwd(), relativePath);
1017
+ if (requireExists && !existsSync(candidate)) {
1018
+ return null;
1019
+ }
1020
+ return candidate;
1021
+ }
1022
+ function buildNodeCommand({ modulePath, repoPath, allowMissing = false, repoPathRequiresExistence = false, }) {
1023
+ const resolved = modulePath ? resolveNodeModulePath(modulePath) : null;
1024
+ if (resolved) {
1025
+ return `node ${resolved}`;
1026
+ }
1027
+ const fallback = repoPath ? resolveRepoPath(repoPath, repoPathRequiresExistence) : null;
1028
+ if (fallback) {
1029
+ return `node ${fallback}`;
1030
+ }
1031
+ if (allowMissing) {
1032
+ return NOOP_NODE_COMMAND;
1033
+ }
1034
+ if (repoPath) {
1035
+ return `node ${resolveRepoPath(repoPath)}`;
1036
+ }
1037
+ if (modulePath) {
1038
+ return `node ${modulePath}`;
1039
+ }
1040
+ return NOOP_NODE_COMMAND;
1041
+ }
1003
1042
  /**
1004
1043
  * Tool paths for scripts
1005
1044
  *
1006
1045
  * Centralized paths to tool scripts.
1007
1046
  */
1008
1047
  export const TOOL_PATHS = {
1009
- VALIDATE_BACKLOG_SYNC: 'node tools/validate-backlog-sync.js',
1010
- SUPABASE_DOCS_LINTER: 'node packages/linters/supabase-docs-linter.js',
1048
+ VALIDATE_BACKLOG_SYNC: buildNodeCommand({
1049
+ modulePath: '@lumenflow/cli/dist/validate-backlog-sync.js',
1050
+ repoPath: 'packages/@lumenflow/cli/dist/validate-backlog-sync.js',
1051
+ }),
1052
+ SUPABASE_DOCS_LINTER: buildNodeCommand({
1053
+ repoPath: 'packages/linters/supabase-docs-linter.js',
1054
+ allowMissing: true,
1055
+ repoPathRequiresExistence: true,
1056
+ }),
1011
1057
  /** WU-2315: System map validator script */
1012
- SYSTEM_MAP_VALIDATE: 'node tools/system-map-validate.js',
1058
+ SYSTEM_MAP_VALIDATE: buildNodeCommand({
1059
+ modulePath: '@lumenflow/core/dist/system-map-validator.js',
1060
+ repoPath: 'packages/@lumenflow/core/dist/system-map-validator.js',
1061
+ }),
1013
1062
  };
1014
1063
  /**
1015
1064
  * CLI mode flags
@@ -1229,8 +1278,16 @@ export const AUDIT_ARGS = {
1229
1278
  * Centralized paths to validation scripts.
1230
1279
  */
1231
1280
  export const SCRIPT_PATHS = {
1281
+ /** Gates runner */
1282
+ GATES: buildNodeCommand({
1283
+ modulePath: '@lumenflow/cli/dist/gates.js',
1284
+ repoPath: 'packages/@lumenflow/cli/dist/gates.js',
1285
+ }),
1232
1286
  /** WU YAML validation */
1233
- VALIDATE: 'tools/validate.js',
1287
+ VALIDATE: buildNodeCommand({
1288
+ modulePath: '@lumenflow/cli/dist/validate.js',
1289
+ repoPath: 'packages/@lumenflow/cli/dist/validate.js',
1290
+ }),
1234
1291
  /** Prompt registry validation */
1235
1292
  VALIDATE_PROMPT_REGISTRY: 'tools/validate-prompt-registry.js',
1236
1293
  };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * WU-1145: Concurrent backlog merge utilities
3
+ *
4
+ * This module provides utilities for merging state stores from worktree
5
+ * and main branches to prevent loss of concurrent WU completions.
6
+ *
7
+ * Problem: When wu:done regenerates backlog.md, it only uses the worktree's
8
+ * state store, losing any WUs that were completed on main since the worktree
9
+ * was created.
10
+ *
11
+ * Solution: Before regenerating backlog.md, merge events from both state
12
+ * stores using event deduplication by identity (type, wuId, timestamp).
13
+ */
14
+ import { WUStateStore } from './wu-state-store.js';
15
+ /**
16
+ * Fetch wu-events.jsonl content from origin/main using git show.
17
+ *
18
+ * This allows us to read the state from main without switching branches
19
+ * or having access to the main checkout directory.
20
+ *
21
+ * @returns Events content from origin/main, or null if not available
22
+ */
23
+ export declare function fetchMainEventsContent(): Promise<string | null>;
24
+ /**
25
+ * Merge state stores from worktree and main.
26
+ *
27
+ * This creates a new WUStateStore instance that contains the merged
28
+ * state from both sources, preserving all concurrent modifications.
29
+ *
30
+ * @param worktreeStateDir - Path to worktree's .lumenflow/state directory
31
+ * @param mainStateDir - Path to main's .lumenflow/state directory
32
+ * @returns A WUStateStore with merged state
33
+ */
34
+ export declare function mergeStateStores(worktreeStateDir: string, mainStateDir: string): Promise<WUStateStore>;
35
+ /**
36
+ * Compute backlog content with merged state from worktree and main.
37
+ *
38
+ * This is a drop-in replacement for computeBacklogContent that handles
39
+ * concurrent modifications by merging state stores before generation.
40
+ *
41
+ * @param backlogPath - Path to backlog.md in the worktree
42
+ * @param wuId - WU ID being completed
43
+ * @param title - WU title
44
+ * @param mainStateDir - Path to main's .lumenflow/state directory
45
+ * @returns Merged backlog.md content
46
+ */
47
+ export declare function computeBacklogContentWithMerge(backlogPath: string, wuId: string, title: string, mainStateDir: string): Promise<string>;
48
+ /**
49
+ * Get the merged events content for wu-events.jsonl
50
+ *
51
+ * This returns the content that should be written to wu-events.jsonl
52
+ * after merging worktree and main state stores.
53
+ *
54
+ * @param worktreeStateDir - Path to worktree's .lumenflow/state directory
55
+ * @param mainStateDir - Path to main's .lumenflow/state directory
56
+ * @param wuId - WU ID being completed (to add complete event)
57
+ * @returns JSONL content for the merged events file
58
+ */
59
+ export declare function getMergedEventsContent(worktreeStateDir: string, mainStateDir: string, wuId: string): Promise<string>;
60
+ /**
61
+ * Merge worktree state with origin/main state using git show.
62
+ *
63
+ * This function:
64
+ * 1. Fetches the wu-events.jsonl content from origin/main using git show
65
+ * 2. Parses events from both worktree and main
66
+ * 3. Merges them with deduplication
67
+ * 4. Returns a store with the merged state
68
+ *
69
+ * This is the integration point for wu:done to preserve concurrent changes.
70
+ *
71
+ * @param worktreeStateDir - Path to worktree's .lumenflow/state directory
72
+ * @returns A WUStateStore with merged state, or just worktree state if main unavailable
73
+ */
74
+ export declare function mergeWithMainState(worktreeStateDir: string): Promise<WUStateStore>;
75
+ /**
76
+ * Compute backlog content with merged state from origin/main.
77
+ *
78
+ * This is the main integration function for wu:done. It:
79
+ * 1. Loads the worktree's state
80
+ * 2. Fetches and merges state from origin/main
81
+ * 3. Applies the complete event for the WU being done
82
+ * 4. Generates backlog from the merged state
83
+ *
84
+ * @param backlogPath - Path to backlog.md in the worktree
85
+ * @param wuId - WU ID being completed
86
+ * @returns Merged backlog.md content
87
+ */
88
+ export declare function computeBacklogContentWithMainMerge(backlogPath: string, wuId: string): Promise<string>;
89
+ /**
90
+ * Compute wu-events.jsonl content with merged state from origin/main.
91
+ *
92
+ * Returns the JSONL content that should be written to wu-events.jsonl,
93
+ * containing merged events from both worktree and main, plus the completion event.
94
+ *
95
+ * @param backlogPath - Path to backlog.md in the worktree
96
+ * @param wuId - WU ID being completed
97
+ * @returns Object with events path and merged content, or null if no update needed
98
+ */
99
+ export declare function computeWUEventsContentWithMainMerge(backlogPath: string, wuId: string): Promise<{
100
+ eventsPath: string;
101
+ content: string;
102
+ } | null>;
@@ -0,0 +1,330 @@
1
+ /**
2
+ * WU-1145: Concurrent backlog merge utilities
3
+ *
4
+ * This module provides utilities for merging state stores from worktree
5
+ * and main branches to prevent loss of concurrent WU completions.
6
+ *
7
+ * Problem: When wu:done regenerates backlog.md, it only uses the worktree's
8
+ * state store, losing any WUs that were completed on main since the worktree
9
+ * was created.
10
+ *
11
+ * Solution: Before regenerating backlog.md, merge events from both state
12
+ * stores using event deduplication by identity (type, wuId, timestamp).
13
+ */
14
+ import { existsSync, readFileSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ import { WUStateStore, WU_EVENTS_FILE_NAME } from './wu-state-store.js';
17
+ import { validateWUEvent } from './wu-state-schema.js';
18
+ import { generateBacklog } from './backlog-generator.js';
19
+ import { getStateStoreDirFromBacklog } from './wu-paths.js';
20
+ import { getGitForCwd } from './git-adapter.js';
21
+ import { REMOTES, BRANCHES, BEACON_PATHS } from './wu-constants.js';
22
+ /**
23
+ * Creates a unique key for an event to detect duplicates.
24
+ * Events are considered identical if they have the same type, wuId, and timestamp.
25
+ */
26
+ function getEventKey(event) {
27
+ return `${event.type}:${event.wuId}:${event.timestamp}`;
28
+ }
29
+ /**
30
+ * Fetch wu-events.jsonl content from origin/main using git show.
31
+ *
32
+ * This allows us to read the state from main without switching branches
33
+ * or having access to the main checkout directory.
34
+ *
35
+ * @returns Events content from origin/main, or null if not available
36
+ */
37
+ export async function fetchMainEventsContent() {
38
+ try {
39
+ const git = getGitForCwd();
40
+ // First, fetch to ensure we have the latest main
41
+ try {
42
+ await git.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
43
+ }
44
+ catch {
45
+ // If fetch fails (e.g., offline), continue with cached version
46
+ console.warn('[wu-done] Warning: Could not fetch latest main, using cached version');
47
+ }
48
+ // Try to read wu-events.jsonl from origin/main
49
+ const eventsPath = `${BEACON_PATHS.STATE_DIR}/${WU_EVENTS_FILE_NAME}`;
50
+ const content = await git.raw(['show', `${REMOTES.ORIGIN}/${BRANCHES.MAIN}:${eventsPath}`]);
51
+ return content;
52
+ }
53
+ catch (error) {
54
+ // File may not exist on main (e.g., new repo or first WU)
55
+ // Or we may not be in a git repo
56
+ return null;
57
+ }
58
+ }
59
+ /**
60
+ * Parse wu-events.jsonl content into validated events
61
+ */
62
+ function parseEventsFile(content, sourceLabel) {
63
+ const events = [];
64
+ const lines = content
65
+ .split('\n')
66
+ .map((line) => line.trim())
67
+ .filter(Boolean);
68
+ for (let i = 0; i < lines.length; i++) {
69
+ const line = lines[i];
70
+ let parsed;
71
+ try {
72
+ parsed = JSON.parse(line);
73
+ }
74
+ catch (error) {
75
+ console.warn(`[wu-done] Warning: Malformed JSON in ${sourceLabel} line ${i + 1}, skipping`);
76
+ continue;
77
+ }
78
+ const validation = validateWUEvent(parsed);
79
+ if (!validation.success) {
80
+ console.warn(`[wu-done] Warning: Invalid event in ${sourceLabel} line ${i + 1}, skipping`);
81
+ continue;
82
+ }
83
+ events.push(validation.data);
84
+ }
85
+ return events;
86
+ }
87
+ /**
88
+ * Load events from a state directory
89
+ */
90
+ function loadEventsFromDir(stateDir, label) {
91
+ const eventsPath = join(stateDir, WU_EVENTS_FILE_NAME);
92
+ if (!existsSync(eventsPath)) {
93
+ return [];
94
+ }
95
+ const content = readFileSync(eventsPath, { encoding: 'utf-8' });
96
+ return parseEventsFile(content, label);
97
+ }
98
+ /**
99
+ * Merge events from two sources, preserving order and deduplicating.
100
+ *
101
+ * The merge strategy:
102
+ * 1. Start with all events from main (the "base" timeline)
103
+ * 2. Add any events from worktree that aren't in main
104
+ *
105
+ * This ensures:
106
+ * - Concurrent completions on main are preserved
107
+ * - The worktree's claim/in_progress events are included
108
+ * - No duplicate events
109
+ */
110
+ function mergeEvents(mainEvents, worktreeEvents) {
111
+ const seen = new Set();
112
+ const merged = [];
113
+ // First, add all main events
114
+ for (const event of mainEvents) {
115
+ const key = getEventKey(event);
116
+ if (!seen.has(key)) {
117
+ seen.add(key);
118
+ merged.push(event);
119
+ }
120
+ }
121
+ // Then add worktree events that aren't in main
122
+ for (const event of worktreeEvents) {
123
+ const key = getEventKey(event);
124
+ if (!seen.has(key)) {
125
+ seen.add(key);
126
+ merged.push(event);
127
+ }
128
+ }
129
+ // Sort by timestamp to ensure chronological order
130
+ merged.sort((a, b) => {
131
+ const timeA = new Date(a.timestamp).getTime();
132
+ const timeB = new Date(b.timestamp).getTime();
133
+ return timeA - timeB;
134
+ });
135
+ return merged;
136
+ }
137
+ /**
138
+ * Merge state stores from worktree and main.
139
+ *
140
+ * This creates a new WUStateStore instance that contains the merged
141
+ * state from both sources, preserving all concurrent modifications.
142
+ *
143
+ * @param worktreeStateDir - Path to worktree's .lumenflow/state directory
144
+ * @param mainStateDir - Path to main's .lumenflow/state directory
145
+ * @returns A WUStateStore with merged state
146
+ */
147
+ export async function mergeStateStores(worktreeStateDir, mainStateDir) {
148
+ // Load events from both sources
149
+ const worktreeEvents = loadEventsFromDir(worktreeStateDir, 'worktree');
150
+ const mainEvents = loadEventsFromDir(mainStateDir, 'main');
151
+ // Merge events
152
+ const mergedEvents = mergeEvents(mainEvents, worktreeEvents);
153
+ // Create a new store and replay merged events
154
+ const store = new WUStateStore(worktreeStateDir);
155
+ for (const event of mergedEvents) {
156
+ store.applyEvent(event);
157
+ }
158
+ return store;
159
+ }
160
+ /**
161
+ * Compute backlog content with merged state from worktree and main.
162
+ *
163
+ * This is a drop-in replacement for computeBacklogContent that handles
164
+ * concurrent modifications by merging state stores before generation.
165
+ *
166
+ * @param backlogPath - Path to backlog.md in the worktree
167
+ * @param wuId - WU ID being completed
168
+ * @param title - WU title
169
+ * @param mainStateDir - Path to main's .lumenflow/state directory
170
+ * @returns Merged backlog.md content
171
+ */
172
+ export async function computeBacklogContentWithMerge(backlogPath, wuId, title, mainStateDir) {
173
+ const worktreeStateDir = getStateStoreDirFromBacklog(backlogPath);
174
+ // Merge state stores
175
+ const mergedStore = await mergeStateStores(worktreeStateDir, mainStateDir);
176
+ // Check if the WU is already done in the merged state
177
+ const currentState = mergedStore.getWUState(wuId);
178
+ if (!currentState) {
179
+ throw new Error(`WU ${wuId} not found in merged state store`);
180
+ }
181
+ // If not already done, create and apply the complete event
182
+ if (currentState.status !== 'done') {
183
+ const completeEvent = mergedStore.createCompleteEvent(wuId);
184
+ mergedStore.applyEvent(completeEvent);
185
+ }
186
+ // Generate backlog from merged state
187
+ return generateBacklog(mergedStore);
188
+ }
189
+ /**
190
+ * Get the merged events content for wu-events.jsonl
191
+ *
192
+ * This returns the content that should be written to wu-events.jsonl
193
+ * after merging worktree and main state stores.
194
+ *
195
+ * @param worktreeStateDir - Path to worktree's .lumenflow/state directory
196
+ * @param mainStateDir - Path to main's .lumenflow/state directory
197
+ * @param wuId - WU ID being completed (to add complete event)
198
+ * @returns JSONL content for the merged events file
199
+ */
200
+ export async function getMergedEventsContent(worktreeStateDir, mainStateDir, wuId) {
201
+ // Load events from both sources
202
+ const worktreeEvents = loadEventsFromDir(worktreeStateDir, 'worktree');
203
+ const mainEvents = loadEventsFromDir(mainStateDir, 'main');
204
+ // Merge events
205
+ const mergedEvents = mergeEvents(mainEvents, worktreeEvents);
206
+ // Check if we need to add a complete event
207
+ const lastEventForWU = [...mergedEvents].reverse().find((e) => e.wuId === wuId);
208
+ if (!lastEventForWU || lastEventForWU.type !== 'complete') {
209
+ // Create a temporary store to generate the complete event
210
+ const tempStore = new WUStateStore(worktreeStateDir);
211
+ for (const event of mergedEvents) {
212
+ tempStore.applyEvent(event);
213
+ }
214
+ const currentState = tempStore.getWUState(wuId);
215
+ if (currentState && currentState.status === 'in_progress') {
216
+ const completeEvent = tempStore.createCompleteEvent(wuId);
217
+ mergedEvents.push(completeEvent);
218
+ }
219
+ }
220
+ // Convert to JSONL
221
+ return mergedEvents.map((event) => JSON.stringify(event)).join('\n') + '\n';
222
+ }
223
+ /**
224
+ * Merge worktree state with origin/main state using git show.
225
+ *
226
+ * This function:
227
+ * 1. Fetches the wu-events.jsonl content from origin/main using git show
228
+ * 2. Parses events from both worktree and main
229
+ * 3. Merges them with deduplication
230
+ * 4. Returns a store with the merged state
231
+ *
232
+ * This is the integration point for wu:done to preserve concurrent changes.
233
+ *
234
+ * @param worktreeStateDir - Path to worktree's .lumenflow/state directory
235
+ * @returns A WUStateStore with merged state, or just worktree state if main unavailable
236
+ */
237
+ export async function mergeWithMainState(worktreeStateDir) {
238
+ // Load worktree events
239
+ const worktreeEvents = loadEventsFromDir(worktreeStateDir, 'worktree');
240
+ // Try to fetch main events via git show
241
+ const mainContent = await fetchMainEventsContent();
242
+ const mainEvents = mainContent ? parseEventsFile(mainContent, 'origin/main') : [];
243
+ if (mainEvents.length > 0) {
244
+ console.log(`[wu-done] Merging state: ${worktreeEvents.length} worktree events + ${mainEvents.length} main events`);
245
+ }
246
+ // Merge events
247
+ const mergedEvents = mergeEvents(mainEvents, worktreeEvents);
248
+ // Create a new store and replay merged events
249
+ const store = new WUStateStore(worktreeStateDir);
250
+ for (const event of mergedEvents) {
251
+ store.applyEvent(event);
252
+ }
253
+ return store;
254
+ }
255
+ /**
256
+ * Compute backlog content with merged state from origin/main.
257
+ *
258
+ * This is the main integration function for wu:done. It:
259
+ * 1. Loads the worktree's state
260
+ * 2. Fetches and merges state from origin/main
261
+ * 3. Applies the complete event for the WU being done
262
+ * 4. Generates backlog from the merged state
263
+ *
264
+ * @param backlogPath - Path to backlog.md in the worktree
265
+ * @param wuId - WU ID being completed
266
+ * @returns Merged backlog.md content
267
+ */
268
+ export async function computeBacklogContentWithMainMerge(backlogPath, wuId) {
269
+ const worktreeStateDir = getStateStoreDirFromBacklog(backlogPath);
270
+ // Merge with main state
271
+ const mergedStore = await mergeWithMainState(worktreeStateDir);
272
+ // Check if the WU exists in the merged state
273
+ const currentState = mergedStore.getWUState(wuId);
274
+ if (!currentState) {
275
+ throw new Error(`WU ${wuId} not found in merged state store. ` +
276
+ `This may indicate the WU was never properly claimed.`);
277
+ }
278
+ // If not already done, create and apply the complete event
279
+ if (currentState.status !== 'done') {
280
+ if (currentState.status !== 'in_progress') {
281
+ throw new Error(`WU ${wuId} is in status "${currentState.status}", expected "in_progress"`);
282
+ }
283
+ const completeEvent = mergedStore.createCompleteEvent(wuId);
284
+ mergedStore.applyEvent(completeEvent);
285
+ }
286
+ // Generate backlog from merged state
287
+ return generateBacklog(mergedStore);
288
+ }
289
+ /**
290
+ * Compute wu-events.jsonl content with merged state from origin/main.
291
+ *
292
+ * Returns the JSONL content that should be written to wu-events.jsonl,
293
+ * containing merged events from both worktree and main, plus the completion event.
294
+ *
295
+ * @param backlogPath - Path to backlog.md in the worktree
296
+ * @param wuId - WU ID being completed
297
+ * @returns Object with events path and merged content, or null if no update needed
298
+ */
299
+ export async function computeWUEventsContentWithMainMerge(backlogPath, wuId) {
300
+ const worktreeStateDir = getStateStoreDirFromBacklog(backlogPath);
301
+ // Load worktree events
302
+ const worktreeEvents = loadEventsFromDir(worktreeStateDir, 'worktree');
303
+ // Try to fetch main events via git show
304
+ const mainContent = await fetchMainEventsContent();
305
+ const mainEvents = mainContent ? parseEventsFile(mainContent, 'origin/main') : [];
306
+ // Merge events
307
+ const mergedEvents = mergeEvents(mainEvents, worktreeEvents);
308
+ // Check if WU is already done
309
+ const tempStore = new WUStateStore(worktreeStateDir);
310
+ for (const event of mergedEvents) {
311
+ tempStore.applyEvent(event);
312
+ }
313
+ const currentState = tempStore.getWUState(wuId);
314
+ if (!currentState) {
315
+ throw new Error(`WU ${wuId} not found in merged state store`);
316
+ }
317
+ if (currentState.status === 'done') {
318
+ // Already done, no update needed
319
+ return null;
320
+ }
321
+ if (currentState.status !== 'in_progress') {
322
+ throw new Error(`WU ${wuId} is in status "${currentState.status}", expected "in_progress"`);
323
+ }
324
+ // Add complete event
325
+ const completeEvent = tempStore.createCompleteEvent(wuId);
326
+ mergedEvents.push(completeEvent);
327
+ const eventsPath = join(worktreeStateDir, WU_EVENTS_FILE_NAME);
328
+ const content = mergedEvents.map((event) => JSON.stringify(event)).join('\n') + '\n';
329
+ return { eventsPath, content };
330
+ }
@@ -42,6 +42,13 @@ export declare function stageDocOutputs(): Promise<void>;
42
42
  * @returns void
43
43
  */
44
44
  export declare function runDocsGenerate(repoRoot: string): void;
45
+ /**
46
+ * Format generated doc output files with prettier.
47
+ *
48
+ * @param repoRoot - Repository root directory for running prettier
49
+ * @returns void
50
+ */
51
+ export declare function formatDocOutputs(repoRoot: string): void;
45
52
  /**
46
53
  * Result of the docs regeneration check.
47
54
  */
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { execSync } from 'node:child_process';
11
11
  import { getGitForCwd } from './git-adapter.js';
12
- import { STDIO } from './wu-constants.js';
12
+ import { STDIO, PKG_MANAGER, SCRIPTS, PRETTIER_FLAGS } from './wu-constants.js';
13
13
  /**
14
14
  * Pathspecs for files that affect generated documentation.
15
15
  * When any of these files change, docs:generate should be run.
@@ -80,6 +80,21 @@ export function runDocsGenerate(repoRoot) {
80
80
  encoding: 'utf-8',
81
81
  });
82
82
  }
83
+ /**
84
+ * Format generated doc output files with prettier.
85
+ *
86
+ * @param repoRoot - Repository root directory for running prettier
87
+ * @returns void
88
+ */
89
+ export function formatDocOutputs(repoRoot) {
90
+ const files = DOC_OUTPUT_FILES.map((file) => `"${file}"`).join(' ');
91
+ const prettierCmd = `${PKG_MANAGER} ${SCRIPTS.PRETTIER} ${PRETTIER_FLAGS.WRITE} ${files}`;
92
+ execSync(prettierCmd, {
93
+ cwd: repoRoot,
94
+ stdio: STDIO.INHERIT,
95
+ encoding: 'utf-8',
96
+ });
97
+ }
83
98
  /**
84
99
  * Detect doc-source changes and regenerate docs if needed.
85
100
  * This is the main integration point for wu:done.
@@ -101,6 +116,8 @@ export async function maybeRegenerateAndStageDocs(options) {
101
116
  console.log('[wu:done] Doc-source changes detected, running turbo docs:generate...');
102
117
  // Run turbo docs:generate (Turbo handles caching and dependencies)
103
118
  runDocsGenerate(repoRoot);
119
+ // Format the generated doc outputs before staging
120
+ formatDocOutputs(repoRoot);
104
121
  // Stage the doc output files
105
122
  await stageDocOutputs();
106
123
  console.log('[wu:done] Documentation regenerated and staged');
@@ -2,9 +2,8 @@
2
2
  * Preflight validation helpers for wu:done.
3
3
  */
4
4
  import { execSync as execSyncImport } from 'node:child_process';
5
- import { existsSync } from 'node:fs';
6
5
  import { validatePreflight } from './wu-preflight-validators.js';
7
- import { LOG_PREFIX, EMOJI, STDIO } from './wu-constants.js';
6
+ import { LOG_PREFIX, EMOJI, STDIO, SCRIPT_PATHS } from './wu-constants.js';
8
7
  /**
9
8
  * WU-1781: Build preflight error message with actionable guidance
10
9
  */
@@ -115,7 +114,7 @@ export function runPreflightTasksValidation(id, options = {}) {
115
114
  console.log(`\n${LOG_PREFIX.DONE} 🔍 Preflight: running tasks:validate...`);
116
115
  try {
117
116
  // Run tasks:validate with WU_ID context (single-WU validation mode)
118
- execSyncFn('node tools/validate.js', {
117
+ execSyncFn(SCRIPT_PATHS.VALIDATE, {
119
118
  stdio: STDIO.PIPE,
120
119
  encoding: 'utf-8',
121
120
  env: { ...process.env, WU_ID: id },
@@ -165,14 +164,8 @@ export function validateAllPreCommitHooks(id, worktreePath = null, options = {})
165
164
  if (worktreePath) {
166
165
  execOptions.cwd = worktreePath;
167
166
  }
168
- // WU-1086: Check for .mjs extension first, fall back to .js for backwards compatibility
169
- const basePath = worktreePath || '.';
170
- const mjsPath = `${basePath}/tools/gates-pre-commit.mjs`;
171
- const gateScript = existsSync(mjsPath)
172
- ? 'tools/gates-pre-commit.mjs'
173
- : 'tools/gates-pre-commit.js';
174
- // Run the gates-pre-commit script that contains all validation gates
175
- execSyncFn(`node ${gateScript}`, execOptions);
167
+ // WU-1139: Run CLI gates directly (removes stub scripts)
168
+ execSyncFn(SCRIPT_PATHS.GATES, execOptions);
176
169
  console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} All pre-commit hooks passed`);
177
170
  return { valid: true, errors: [] };
178
171
  }