@lumenflow/core 1.3.0 → 1.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.
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Spec Branch Helpers
3
+ *
4
+ * WU-1062: External plan storage and no-main-write mode
5
+ *
6
+ * Provides helpers for working with spec branches (spec/wu-XXXX).
7
+ * wu:create writes to spec branches by default; wu:claim merges them to main.
8
+ *
9
+ * @module
10
+ */
11
+ import { WU_PATHS } from './wu-paths.js';
12
+ /**
13
+ * Spec branch prefix
14
+ */
15
+ export const SPEC_BRANCH_PREFIX = 'spec/';
16
+ /**
17
+ * WU source location constants
18
+ */
19
+ export const WU_SOURCE = {
20
+ /** WU exists on main branch only */
21
+ MAIN: 'main',
22
+ /** WU exists on spec branch only */
23
+ SPEC_BRANCH: 'spec_branch',
24
+ /** WU exists on both main and spec branch */
25
+ BOTH: 'both',
26
+ /** WU not found anywhere */
27
+ NOT_FOUND: 'not_found',
28
+ };
29
+ /**
30
+ * Get the spec branch name for a WU
31
+ *
32
+ * @param {string} wuId - Work Unit ID (e.g., 'WU-1062')
33
+ * @returns {string} Spec branch name (e.g., 'spec/wu-1062')
34
+ *
35
+ * @example
36
+ * getSpecBranchName('WU-1062') // 'spec/wu-1062'
37
+ */
38
+ export function getSpecBranchName(wuId) {
39
+ return `${SPEC_BRANCH_PREFIX}${wuId.toLowerCase()}`;
40
+ }
41
+ /**
42
+ * Get the origin-qualified spec branch name
43
+ *
44
+ * @param {string} wuId - Work Unit ID
45
+ * @returns {string} Origin-qualified branch name (e.g., 'origin/spec/wu-1062')
46
+ */
47
+ export function getOriginSpecBranch(wuId) {
48
+ return `origin/${getSpecBranchName(wuId)}`;
49
+ }
50
+ /**
51
+ * Check if a spec branch exists on origin
52
+ *
53
+ * @param {string} wuId - Work Unit ID
54
+ * @param {SimpleGit} git - Git adapter instance
55
+ * @returns {Promise<boolean>} True if spec branch exists
56
+ *
57
+ * @example
58
+ * const exists = await specBranchExists('WU-1062', git);
59
+ */
60
+ export async function specBranchExists(wuId, git) {
61
+ try {
62
+ const originBranch = getOriginSpecBranch(wuId);
63
+ // Use branchExists if available, otherwise check with ls-remote
64
+ if ('branchExists' in git && typeof git.branchExists === 'function') {
65
+ return await git.branchExists(originBranch);
66
+ }
67
+ // Fallback: use ls-remote to check if branch exists
68
+ const result = await git.raw(['ls-remote', '--heads', 'origin', getSpecBranchName(wuId)]);
69
+ return result.trim().length > 0;
70
+ }
71
+ catch {
72
+ return false;
73
+ }
74
+ }
75
+ /**
76
+ * Check if a WU exists on main branch
77
+ *
78
+ * @param {string} wuId - Work Unit ID
79
+ * @param {SimpleGit} git - Git adapter instance
80
+ * @returns {Promise<boolean>} True if WU YAML exists on main
81
+ */
82
+ export async function isWUOnMain(wuId, git) {
83
+ try {
84
+ const wuPath = WU_PATHS.WU(wuId);
85
+ // Check if file exists on origin/main
86
+ await git.raw(['ls-tree', 'origin/main', '--', wuPath]);
87
+ return true;
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ /**
94
+ * Merge spec branch to main branch (fast-forward only)
95
+ *
96
+ * This is used by wu:claim when a WU exists only on a spec branch.
97
+ * The spec branch is merged to main before creating the worktree.
98
+ *
99
+ * @param {string} wuId - Work Unit ID
100
+ * @param {SimpleGit} git - Git adapter instance
101
+ * @throws {Error} If merge fails (e.g., due to conflicts)
102
+ *
103
+ * @example
104
+ * await mergeSpecBranchToMain('WU-1062', git);
105
+ */
106
+ export async function mergeSpecBranchToMain(wuId, git) {
107
+ const specBranch = getSpecBranchName(wuId);
108
+ const originSpecBranch = getOriginSpecBranch(wuId);
109
+ // Fetch the spec branch
110
+ await git.fetch('origin', specBranch);
111
+ // Merge with fast-forward only (safe merge)
112
+ await git.merge([originSpecBranch, '--ff-only']);
113
+ }
114
+ /**
115
+ * Delete spec branch after merge
116
+ *
117
+ * @param {string} wuId - Work Unit ID
118
+ * @param {SimpleGit} git - Git adapter instance
119
+ */
120
+ export async function deleteSpecBranch(wuId, git) {
121
+ const specBranch = getSpecBranchName(wuId);
122
+ try {
123
+ // Delete local branch if exists
124
+ await git.branch(['-d', specBranch]);
125
+ }
126
+ catch {
127
+ // Ignore if local branch doesn't exist
128
+ }
129
+ try {
130
+ // Delete remote branch
131
+ await git.push(['origin', '--delete', specBranch]);
132
+ }
133
+ catch {
134
+ // Ignore if remote branch doesn't exist
135
+ }
136
+ }
137
+ /**
138
+ * Determine the source of a WU (main, spec branch, both, or not found)
139
+ *
140
+ * Used by wu:claim to decide whether to merge spec branch before creating worktree.
141
+ *
142
+ * @param {string} wuId - Work Unit ID
143
+ * @param {SimpleGit} git - Git adapter instance
144
+ * @returns {Promise<WUSourceType>} Source location constant
145
+ *
146
+ * @example
147
+ * const source = await getWUSource('WU-1062', git);
148
+ * if (source === WU_SOURCE.SPEC_BRANCH) {
149
+ * await mergeSpecBranchToMain('WU-1062', git);
150
+ * }
151
+ */
152
+ export async function getWUSource(wuId, git) {
153
+ // Check both locations in parallel for efficiency
154
+ const [onMain, hasSpecBranch] = await Promise.all([
155
+ isWUOnMain(wuId, git),
156
+ specBranchExists(wuId, git),
157
+ ]);
158
+ if (onMain && hasSpecBranch) {
159
+ return WU_SOURCE.BOTH;
160
+ }
161
+ if (onMain) {
162
+ return WU_SOURCE.MAIN;
163
+ }
164
+ if (hasSpecBranch) {
165
+ return WU_SOURCE.SPEC_BRANCH;
166
+ }
167
+ return WU_SOURCE.NOT_FOUND;
168
+ }
169
+ /**
170
+ * Create a spec branch from current HEAD
171
+ *
172
+ * Used by wu:create in default mode (no --direct flag).
173
+ *
174
+ * @param {string} wuId - Work Unit ID
175
+ * @param {SimpleGit} git - Git adapter instance
176
+ */
177
+ export async function createSpecBranch(wuId, git) {
178
+ const specBranch = getSpecBranchName(wuId);
179
+ // Create local branch
180
+ await git.checkoutLocalBranch(specBranch);
181
+ }
182
+ /**
183
+ * Push spec branch to origin
184
+ *
185
+ * @param {string} wuId - Work Unit ID
186
+ * @param {SimpleGit} git - Git adapter instance
187
+ */
188
+ export async function pushSpecBranch(wuId, git) {
189
+ const specBranch = getSpecBranchName(wuId);
190
+ // Push to origin
191
+ await git.push(['origin', specBranch]);
192
+ }
@@ -30,6 +30,7 @@ export declare function checkWUConsistency(id: any, projectRoot?: string): Promi
30
30
  backlogInProgress?: undefined;
31
31
  statusInProgress?: undefined;
32
32
  hasWorktree?: undefined;
33
+ worktreePathExists?: undefined;
33
34
  };
34
35
  } | {
35
36
  valid: boolean;
@@ -41,6 +42,7 @@ export declare function checkWUConsistency(id: any, projectRoot?: string): Promi
41
42
  backlogInProgress: boolean;
42
43
  statusInProgress: boolean;
43
44
  hasWorktree: boolean;
45
+ worktreePathExists: boolean;
44
46
  wuExists?: undefined;
45
47
  };
46
48
  }>;
@@ -17,7 +17,7 @@ import { constants } from 'node:fs';
17
17
  import path from 'node:path';
18
18
  import { parseYAML, stringifyYAML } from './wu-yaml.js';
19
19
  import { WU_PATHS } from './wu-paths.js';
20
- import { CONSISTENCY_TYPES, LOG_PREFIX, REMOTES, STRING_LITERALS, toKebab, WU_STATUS, YAML_OPTIONS, } from './wu-constants.js';
20
+ import { CONSISTENCY_TYPES, CONSISTENCY_MESSAGES, LOG_PREFIX, REMOTES, STRING_LITERALS, toKebab, WU_STATUS, YAML_OPTIONS, } from './wu-constants.js';
21
21
  import { todayISO } from './date-utils.js';
22
22
  import { createGitForPath } from './git-adapter.js';
23
23
  /**
@@ -45,6 +45,7 @@ export async function checkWUConsistency(id, projectRoot = process.cwd()) {
45
45
  const yamlStatus = wuDoc?.status || 'unknown';
46
46
  const lane = wuDoc?.lane || '';
47
47
  const title = wuDoc?.title || '';
48
+ const worktreePathFromYaml = wuDoc?.worktree_path || '';
48
49
  // Check stamp existence
49
50
  let hasStamp = false;
50
51
  try {
@@ -74,6 +75,7 @@ export async function checkWUConsistency(id, projectRoot = process.cwd()) {
74
75
  const { inProgress: statusInProgress } = parseStatusSections(statusContent, id);
75
76
  // Check for worktree
76
77
  const hasWorktree = await checkWorktreeExists(id, projectRoot);
78
+ const worktreePathExists = await checkWorktreePathExists(worktreePathFromYaml);
77
79
  // Detection logic
78
80
  // 1. YAML done but in status.md In Progress
79
81
  if (yamlStatus === WU_STATUS.DONE && statusInProgress) {
@@ -129,6 +131,19 @@ export async function checkWUConsistency(id, projectRoot = process.cwd()) {
129
131
  canAutoRepair: true,
130
132
  });
131
133
  }
134
+ // 6. Claimed WU missing worktree directory
135
+ if (worktreePathFromYaml &&
136
+ !worktreePathExists &&
137
+ (yamlStatus === WU_STATUS.IN_PROGRESS || yamlStatus === WU_STATUS.BLOCKED)) {
138
+ errors.push({
139
+ type: CONSISTENCY_TYPES.MISSING_WORKTREE_CLAIMED,
140
+ wuId: id,
141
+ title,
142
+ description: CONSISTENCY_MESSAGES.MISSING_WORKTREE_CLAIMED(id, yamlStatus, worktreePathFromYaml),
143
+ repairAction: CONSISTENCY_MESSAGES.MISSING_WORKTREE_CLAIMED_REPAIR,
144
+ canAutoRepair: false,
145
+ });
146
+ }
132
147
  return {
133
148
  valid: errors.length === 0,
134
149
  errors,
@@ -139,6 +154,7 @@ export async function checkWUConsistency(id, projectRoot = process.cwd()) {
139
154
  backlogInProgress,
140
155
  statusInProgress,
141
156
  hasWorktree,
157
+ worktreePathExists,
142
158
  },
143
159
  };
144
160
  }
@@ -565,3 +581,21 @@ async function checkWorktreeExists(id, projectRoot) {
565
581
  return false;
566
582
  }
567
583
  }
584
+ /**
585
+ * Check whether a worktree path exists on disk
586
+ *
587
+ * @param {string} worktreePath - Worktree path from WU YAML
588
+ * @returns {Promise<boolean>} True if path exists
589
+ */
590
+ async function checkWorktreePathExists(worktreePath) {
591
+ if (!worktreePath) {
592
+ return false;
593
+ }
594
+ try {
595
+ await access(worktreePath, constants.R_OK);
596
+ return true;
597
+ }
598
+ catch {
599
+ return false;
600
+ }
601
+ }
@@ -40,6 +40,10 @@ export declare const GIT_REFS: {
40
40
  ORIGIN_MAIN: string;
41
41
  /** Current HEAD ref */
42
42
  HEAD: string;
43
+ /** Upstream ref */
44
+ UPSTREAM: string;
45
+ /** Range of upstream..HEAD */
46
+ UPSTREAM_RANGE: string;
43
47
  /** Fetch head ref */
44
48
  FETCH_HEAD: string;
45
49
  };
@@ -258,6 +262,22 @@ export declare const CONSISTENCY_TYPES: {
258
262
  ORPHAN_WORKTREE_DONE: string;
259
263
  /** Stamp file exists but WU YAML status is not 'done' (partial wu:done failure) */
260
264
  STAMP_EXISTS_YAML_NOT_DONE: string;
265
+ /** WU is claimed but its worktree directory is missing */
266
+ MISSING_WORKTREE_CLAIMED: string;
267
+ };
268
+ /**
269
+ * Consistency check messages
270
+ */
271
+ export declare const CONSISTENCY_MESSAGES: {
272
+ MISSING_WORKTREE_CLAIMED: (id: any, status: any, worktreePath: any) => string;
273
+ MISSING_WORKTREE_CLAIMED_REPAIR: string;
274
+ };
275
+ /**
276
+ * Worktree warning messages
277
+ */
278
+ export declare const WORKTREE_WARNINGS: {
279
+ MISSING_TRACKED_HEADER: string;
280
+ MISSING_TRACKED_LINE: (worktreePath: any) => string;
261
281
  };
262
282
  /**
263
283
  * File system constants
@@ -404,6 +424,59 @@ export declare const BOX: {
404
424
  /** Side border for content lines */
405
425
  SIDE: string;
406
426
  };
427
+ /**
428
+ * Cleanup guard constants
429
+ */
430
+ export declare const CLEANUP_GUARD: {
431
+ REASONS: {
432
+ UNCOMMITTED_CHANGES: string;
433
+ UNPUSHED_COMMITS: string;
434
+ STATUS_NOT_DONE: string;
435
+ MISSING_STAMP: string;
436
+ PR_NOT_MERGED: string;
437
+ };
438
+ TITLES: {
439
+ BLOCKED: string;
440
+ NEXT_STEPS: string;
441
+ };
442
+ MESSAGES: {
443
+ UNCOMMITTED_CHANGES: string;
444
+ UNPUSHED_COMMITS: string;
445
+ STATUS_NOT_DONE: string;
446
+ MISSING_STAMP: string;
447
+ PR_NOT_MERGED: string;
448
+ };
449
+ NEXT_STEPS: {
450
+ DEFAULT: {
451
+ text: string;
452
+ appendId: boolean;
453
+ }[];
454
+ UNCOMMITTED_CHANGES: {
455
+ text: string;
456
+ appendId: boolean;
457
+ }[];
458
+ UNPUSHED_COMMITS: {
459
+ text: string;
460
+ appendId: boolean;
461
+ }[];
462
+ STATUS_NOT_DONE: {
463
+ text: string;
464
+ appendId: boolean;
465
+ }[];
466
+ MISSING_STAMP: {
467
+ text: string;
468
+ appendId: boolean;
469
+ }[];
470
+ PR_NOT_MERGED: {
471
+ text: string;
472
+ appendId: boolean;
473
+ }[];
474
+ };
475
+ PR_CHECK: {
476
+ START: string;
477
+ RESULT: string;
478
+ };
479
+ };
407
480
  /**
408
481
  * Git display constants
409
482
  *
@@ -471,6 +544,8 @@ export declare const GIT_FLAGS: {
471
544
  NO_VERIFY: string;
472
545
  /** No GPG sign flag (skip commit signing) */
473
546
  NO_GPG_SIGN: string;
547
+ /** One-line log format */
548
+ ONELINE: string;
474
549
  };
475
550
  /**
476
551
  * Git commands
@@ -487,6 +562,8 @@ export declare const GIT_COMMANDS: {
487
562
  LS_TREE: string;
488
563
  /** Git diff command */
489
564
  DIFF: string;
565
+ /** Git log command */
566
+ LOG: string;
490
567
  /** Git merge-base command */
491
568
  MERGE_BASE: string;
492
569
  /** Git rev-parse command */
@@ -42,6 +42,10 @@ export const GIT_REFS = {
42
42
  ORIGIN_MAIN: 'origin/main',
43
43
  /** Current HEAD ref */
44
44
  HEAD: 'HEAD',
45
+ /** Upstream ref */
46
+ UPSTREAM: '@{u}',
47
+ /** Range of upstream..HEAD */
48
+ UPSTREAM_RANGE: '@{u}..HEAD',
45
49
  /** Fetch head ref */
46
50
  FETCH_HEAD: 'FETCH_HEAD',
47
51
  };
@@ -273,6 +277,22 @@ export const CONSISTENCY_TYPES = {
273
277
  ORPHAN_WORKTREE_DONE: 'ORPHAN_WORKTREE_DONE',
274
278
  /** Stamp file exists but WU YAML status is not 'done' (partial wu:done failure) */
275
279
  STAMP_EXISTS_YAML_NOT_DONE: 'STAMP_EXISTS_YAML_NOT_DONE',
280
+ /** WU is claimed but its worktree directory is missing */
281
+ MISSING_WORKTREE_CLAIMED: 'MISSING_WORKTREE_CLAIMED',
282
+ };
283
+ /**
284
+ * Consistency check messages
285
+ */
286
+ export const CONSISTENCY_MESSAGES = {
287
+ MISSING_WORKTREE_CLAIMED: (id, status, worktreePath) => `WU ${id} is '${status}' but worktree path is missing (${worktreePath})`,
288
+ MISSING_WORKTREE_CLAIMED_REPAIR: 'Recover worktree or re-claim WU',
289
+ };
290
+ /**
291
+ * Worktree warning messages
292
+ */
293
+ export const WORKTREE_WARNINGS = {
294
+ MISSING_TRACKED_HEADER: 'Tracked worktrees missing on disk (possible manual deletion):',
295
+ MISSING_TRACKED_LINE: (worktreePath) => `Missing: ${worktreePath}`,
276
296
  };
277
297
  /**
278
298
  * File system constants
@@ -426,6 +446,62 @@ export const BOX = {
426
446
  /** Side border for content lines */
427
447
  SIDE: '║',
428
448
  };
449
+ /**
450
+ * Cleanup guard constants
451
+ */
452
+ export const CLEANUP_GUARD = {
453
+ REASONS: {
454
+ UNCOMMITTED_CHANGES: 'UNCOMMITTED_CHANGES',
455
+ UNPUSHED_COMMITS: 'UNPUSHED_COMMITS',
456
+ STATUS_NOT_DONE: 'STATUS_NOT_DONE',
457
+ MISSING_STAMP: 'MISSING_STAMP',
458
+ PR_NOT_MERGED: 'PR_NOT_MERGED',
459
+ },
460
+ TITLES: {
461
+ BLOCKED: 'CLEANUP BLOCKED',
462
+ NEXT_STEPS: 'Next steps:',
463
+ },
464
+ MESSAGES: {
465
+ UNCOMMITTED_CHANGES: 'Worktree has uncommitted changes. Refusing to delete.',
466
+ UNPUSHED_COMMITS: 'Worktree has unpushed commits. Refusing to delete.',
467
+ STATUS_NOT_DONE: 'WU YAML status is not done. Refusing to delete.',
468
+ MISSING_STAMP: 'WU stamp is missing. Refusing to delete.',
469
+ PR_NOT_MERGED: 'PR is not merged (or cannot be verified). Refusing to delete.',
470
+ },
471
+ NEXT_STEPS: {
472
+ DEFAULT: [
473
+ { text: '1. Resolve the issue above', appendId: false },
474
+ { text: '2. Re-run: pnpm wu:cleanup --id', appendId: true },
475
+ ],
476
+ UNCOMMITTED_CHANGES: [
477
+ { text: '1. Commit or stash changes in the worktree', appendId: false },
478
+ { text: '2. Re-run: pnpm wu:cleanup --id', appendId: true },
479
+ ],
480
+ UNPUSHED_COMMITS: [
481
+ { text: '1. Push the lane branch to origin', appendId: false },
482
+ { text: '2. Re-run: pnpm wu:cleanup --id', appendId: true },
483
+ ],
484
+ STATUS_NOT_DONE: [
485
+ {
486
+ text: `1. Complete the WU with ${LOG_PREFIX.DONE} (creates stamp + done status)`,
487
+ appendId: false,
488
+ },
489
+ { text: '2. Re-run: pnpm wu:cleanup --id', appendId: true },
490
+ ],
491
+ MISSING_STAMP: [
492
+ { text: '1. Run wu:done to create the stamp file', appendId: false },
493
+ { text: '2. Re-run: pnpm wu:cleanup --id', appendId: true },
494
+ ],
495
+ PR_NOT_MERGED: [
496
+ { text: '1. Merge the PR in GitHub', appendId: false },
497
+ { text: '2. Re-run: pnpm wu:cleanup --id', appendId: true },
498
+ ],
499
+ },
500
+ PR_CHECK: {
501
+ START: 'Verifying PR merge status...',
502
+ RESULT: 'PR merge verification via',
503
+ },
504
+ };
429
505
  /**
430
506
  * Git display constants
431
507
  *
@@ -493,6 +569,8 @@ export const GIT_FLAGS = {
493
569
  NO_VERIFY: '--no-verify',
494
570
  /** No GPG sign flag (skip commit signing) */
495
571
  NO_GPG_SIGN: '--no-gpg-sign',
572
+ /** One-line log format */
573
+ ONELINE: '--oneline',
496
574
  };
497
575
  /**
498
576
  * Git commands
@@ -509,6 +587,8 @@ export const GIT_COMMANDS = {
509
587
  LS_TREE: 'ls-tree',
510
588
  /** Git diff command */
511
589
  DIFF: 'diff',
590
+ /** Git log command */
591
+ LOG: 'log',
512
592
  /** Git merge-base command */
513
593
  MERGE_BASE: 'merge-base',
514
594
  /** Git rev-parse command */
@@ -1,11 +1,16 @@
1
1
  /**
2
- * WU Create Validators (WU-2107)
2
+ * WU Create Validators (WU-2107, WU-1062)
3
3
  *
4
- * Validation helpers for wu:create, including lane inference surfacing.
4
+ * Validation helpers for wu:create, including:
5
+ * - Lane inference surfacing (WU-2107)
6
+ * - External spec_refs validation (WU-1062)
5
7
  *
6
8
  * When agents create WUs, this module helps surface lane inference suggestions
7
9
  * to guide better lane selection and improve parallelization.
8
10
  *
11
+ * WU-1062: Validates spec_refs paths, accepting both repo-relative paths
12
+ * and external paths (lumenflow://, ~/.lumenflow/, $LUMENFLOW_HOME/).
13
+ *
9
14
  * NOTE: This is domain-specific WU workflow code, not a general utility.
10
15
  * No external library exists for LumenFlow lane inference validation.
11
16
  */
@@ -40,3 +45,36 @@ export declare function validateLaneWithInference(providedLane: any, codePaths:
40
45
  inferredLane: any;
41
46
  confidence: any;
42
47
  };
48
+ /**
49
+ * WU-1062: Validate spec_refs paths
50
+ *
51
+ * Accepts:
52
+ * - Repo-relative paths: docs/04-operations/plans/WU-XXX-plan.md
53
+ * - External paths: lumenflow://plans/WU-XXX-plan.md
54
+ * - Tilde paths: ~/.lumenflow/plans/WU-XXX-plan.md
55
+ * - Env var paths: $LUMENFLOW_HOME/plans/WU-XXX-plan.md
56
+ *
57
+ * @param {string[]} specRefs - Array of spec reference paths
58
+ * @returns {{ valid: boolean, errors: string[], warnings: string[] }} Validation result
59
+ */
60
+ export declare function validateSpecRefs(specRefs: string[]): {
61
+ valid: boolean;
62
+ errors: string[];
63
+ warnings: string[];
64
+ };
65
+ /**
66
+ * WU-1062: Check if spec_refs contains external paths
67
+ *
68
+ * @param {string[]} specRefs - Array of spec reference paths
69
+ * @returns {boolean} True if any spec_ref is an external path
70
+ */
71
+ export declare function hasExternalSpecRefs(specRefs: string[]): boolean;
72
+ /**
73
+ * WU-1062: Normalize all spec_refs paths
74
+ *
75
+ * Expands external paths to absolute paths while keeping repo-relative paths unchanged.
76
+ *
77
+ * @param {string[]} specRefs - Array of spec reference paths
78
+ * @returns {string[]} Normalized paths
79
+ */
80
+ export declare function normalizeSpecRefs(specRefs: string[]): string[];
@@ -1,14 +1,20 @@
1
1
  /**
2
- * WU Create Validators (WU-2107)
2
+ * WU Create Validators (WU-2107, WU-1062)
3
3
  *
4
- * Validation helpers for wu:create, including lane inference surfacing.
4
+ * Validation helpers for wu:create, including:
5
+ * - Lane inference surfacing (WU-2107)
6
+ * - External spec_refs validation (WU-1062)
5
7
  *
6
8
  * When agents create WUs, this module helps surface lane inference suggestions
7
9
  * to guide better lane selection and improve parallelization.
8
10
  *
11
+ * WU-1062: Validates spec_refs paths, accepting both repo-relative paths
12
+ * and external paths (lumenflow://, ~/.lumenflow/, $LUMENFLOW_HOME/).
13
+ *
9
14
  * NOTE: This is domain-specific WU workflow code, not a general utility.
10
15
  * No external library exists for LumenFlow lane inference validation.
11
16
  */
17
+ import { isExternalPath, normalizeSpecRef } from './lumenflow-home.js';
12
18
  /** Confidence threshold for showing suggestion (percentage) */
13
19
  const CONFIDENCE_THRESHOLD_LOW = 30;
14
20
  /**
@@ -91,3 +97,71 @@ export function validateLaneWithInference(providedLane, codePaths, description,
91
97
  return { shouldWarn: false, warning: '' };
92
98
  }
93
99
  }
100
+ /**
101
+ * WU-1062: Validate spec_refs paths
102
+ *
103
+ * Accepts:
104
+ * - Repo-relative paths: docs/04-operations/plans/WU-XXX-plan.md
105
+ * - External paths: lumenflow://plans/WU-XXX-plan.md
106
+ * - Tilde paths: ~/.lumenflow/plans/WU-XXX-plan.md
107
+ * - Env var paths: $LUMENFLOW_HOME/plans/WU-XXX-plan.md
108
+ *
109
+ * @param {string[]} specRefs - Array of spec reference paths
110
+ * @returns {{ valid: boolean, errors: string[], warnings: string[] }} Validation result
111
+ */
112
+ export function validateSpecRefs(specRefs) {
113
+ const errors = [];
114
+ const warnings = [];
115
+ if (!specRefs || specRefs.length === 0) {
116
+ return { valid: true, errors, warnings };
117
+ }
118
+ for (const ref of specRefs) {
119
+ // Check for empty refs
120
+ if (!ref || ref.trim().length === 0) {
121
+ errors.push('Empty spec_ref detected');
122
+ continue;
123
+ }
124
+ // External paths are valid (will be resolved at runtime)
125
+ if (isExternalPath(ref)) {
126
+ // Add informational warning about external paths
127
+ warnings.push(`External spec_ref: "${ref}" - ensure plan exists at ${normalizeSpecRef(ref)}`);
128
+ continue;
129
+ }
130
+ // Repo-relative paths should follow conventions
131
+ const isValidRepoPath = ref.startsWith('docs/') || ref.startsWith('./docs/') || ref.endsWith('.md');
132
+ if (!isValidRepoPath) {
133
+ warnings.push(`Unconventional spec_ref path: "${ref}" - consider using docs/04-operations/plans/ or lumenflow://plans/`);
134
+ }
135
+ }
136
+ return {
137
+ valid: errors.length === 0,
138
+ errors,
139
+ warnings,
140
+ };
141
+ }
142
+ /**
143
+ * WU-1062: Check if spec_refs contains external paths
144
+ *
145
+ * @param {string[]} specRefs - Array of spec reference paths
146
+ * @returns {boolean} True if any spec_ref is an external path
147
+ */
148
+ export function hasExternalSpecRefs(specRefs) {
149
+ if (!specRefs || specRefs.length === 0) {
150
+ return false;
151
+ }
152
+ return specRefs.some((ref) => isExternalPath(ref));
153
+ }
154
+ /**
155
+ * WU-1062: Normalize all spec_refs paths
156
+ *
157
+ * Expands external paths to absolute paths while keeping repo-relative paths unchanged.
158
+ *
159
+ * @param {string[]} specRefs - Array of spec reference paths
160
+ * @returns {string[]} Normalized paths
161
+ */
162
+ export function normalizeSpecRefs(specRefs) {
163
+ if (!specRefs || specRefs.length === 0) {
164
+ return [];
165
+ }
166
+ return specRefs.map((ref) => normalizeSpecRef(ref));
167
+ }
@@ -23,6 +23,8 @@ import { die, createError, ErrorCodes } from './error-handler.js';
23
23
  import { validateWU, validateDoneWU } from './wu-schema.js';
24
24
  import { assertTransition } from './state-machine.js';
25
25
  import { detectZombieState, recoverZombieState } from './wu-recovery.js';
26
+ // WU-1061: Import docs regeneration utilities
27
+ import { maybeRegenerateAndStageDocs } from './wu-done-docs-generate.js';
26
28
  /**
27
29
  * @typedef {Object} BranchOnlyContext
28
30
  * @property {string} id - WU ID (e.g., "WU-1215")
@@ -119,6 +121,13 @@ export async function executeBranchOnlyCompletion(context) {
119
121
  statusPath: metadataStatusPath,
120
122
  backlogPath: metadataBacklogPath,
121
123
  });
124
+ // WU-1061: Regenerate docs if doc-source files changed
125
+ // This runs BEFORE stageAndFormatMetadata to include doc outputs
126
+ // in the single atomic commit
127
+ await maybeRegenerateAndStageDocs({
128
+ baseBranch: BRANCHES.MAIN,
129
+ repoRoot: metadataBasePath,
130
+ });
122
131
  // Step 7: Stage and format files
123
132
  await stageAndFormatMetadata({
124
133
  id,