@lumenflow/core 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.
Files changed (213) hide show
  1. package/dist/active-wu-detector.d.ts +1 -1
  2. package/dist/active-wu-detector.js +1 -1
  3. package/dist/arg-parser.js +51 -18
  4. package/dist/backlog-generator.d.ts +4 -4
  5. package/dist/backlog-generator.js +4 -4
  6. package/dist/backlog-sync-validator.js +1 -1
  7. package/dist/cleanup-lock.d.ts +9 -2
  8. package/dist/cleanup-lock.js +17 -7
  9. package/dist/code-path-validator.d.ts +3 -3
  10. package/dist/code-path-validator.js +3 -3
  11. package/dist/compliance-parser.d.ts +1 -1
  12. package/dist/compliance-parser.js +1 -1
  13. package/dist/constants/backlog-patterns.d.ts +1 -1
  14. package/dist/constants/backlog-patterns.js +1 -1
  15. package/dist/constants/dora-constants.d.ts +1 -1
  16. package/dist/constants/dora-constants.js +1 -1
  17. package/dist/constants/gate-constants.d.ts +1 -1
  18. package/dist/constants/gate-constants.js +1 -1
  19. package/dist/constants/linter-constants.d.ts +1 -1
  20. package/dist/constants/linter-constants.js +1 -1
  21. package/dist/constants/tokenizer-constants.d.ts +1 -1
  22. package/dist/constants/tokenizer-constants.js +1 -1
  23. package/dist/context/location-resolver.js +2 -1
  24. package/dist/context-validation-integration.d.ts +1 -0
  25. package/dist/core/scope-checker.d.ts +3 -3
  26. package/dist/core/scope-checker.js +3 -3
  27. package/dist/core/tool-runner.d.ts +5 -5
  28. package/dist/core/tool-runner.js +5 -5
  29. package/dist/core/tool.constants.d.ts +1 -1
  30. package/dist/core/tool.constants.js +1 -1
  31. package/dist/core/tool.schemas.d.ts +2 -2
  32. package/dist/core/tool.schemas.js +1 -1
  33. package/dist/core/worktree-guard.d.ts +1 -1
  34. package/dist/core/worktree-guard.js +1 -1
  35. package/dist/coverage-gate.d.ts +12 -3
  36. package/dist/coverage-gate.js +15 -8
  37. package/dist/date-utils.d.ts +4 -4
  38. package/dist/date-utils.js +4 -4
  39. package/dist/dependency-graph.d.ts +6 -0
  40. package/dist/dependency-graph.js +43 -2
  41. package/dist/dependency-guard.d.ts +2 -2
  42. package/dist/dependency-guard.js +3 -3
  43. package/dist/dependency-validator.d.ts +4 -4
  44. package/dist/dependency-validator.js +4 -7
  45. package/dist/domain/orchestration.constants.d.ts +31 -10
  46. package/dist/domain/orchestration.constants.js +45 -16
  47. package/dist/domain/orchestration.schemas.d.ts +54 -28
  48. package/dist/domain/orchestration.schemas.js +2 -2
  49. package/dist/domain/orchestration.types.d.ts +2 -2
  50. package/dist/domain/orchestration.types.js +2 -2
  51. package/dist/error-handler.d.ts +10 -10
  52. package/dist/error-handler.js +10 -10
  53. package/dist/file-classifiers.d.ts +6 -6
  54. package/dist/file-classifiers.js +6 -6
  55. package/dist/gates-config.d.ts +74 -0
  56. package/dist/gates-config.js +209 -2
  57. package/dist/git-adapter.d.ts +11 -11
  58. package/dist/git-adapter.js +11 -11
  59. package/dist/git-context-extractor.d.ts +112 -0
  60. package/dist/git-context-extractor.js +559 -0
  61. package/dist/hardcoded-strings.d.ts +1 -1
  62. package/dist/hardcoded-strings.js +1 -1
  63. package/dist/incremental-lint.d.ts +1 -1
  64. package/dist/incremental-lint.js +2 -2
  65. package/dist/incremental-test.d.ts +1 -1
  66. package/dist/incremental-test.js +1 -1
  67. package/dist/index.d.ts +13 -0
  68. package/dist/index.js +25 -0
  69. package/dist/invariants/check-automated-tests.d.ts +2 -2
  70. package/dist/invariants/check-automated-tests.js +3 -3
  71. package/dist/lane-checker.d.ts +28 -7
  72. package/dist/lane-checker.js +316 -159
  73. package/dist/lane-suggest-prompt.d.ts +108 -0
  74. package/dist/lane-suggest-prompt.js +359 -0
  75. package/dist/lane-validator.d.ts +3 -3
  76. package/dist/lane-validator.js +3 -3
  77. package/dist/logs-lib.d.ts +1 -1
  78. package/dist/logs-lib.js +1 -1
  79. package/dist/lumenflow-config-schema.d.ts +162 -0
  80. package/dist/lumenflow-config-schema.js +180 -0
  81. package/dist/manual-test-validator.d.ts +2 -2
  82. package/dist/manual-test-validator.js +3 -3
  83. package/dist/merge-lock.d.ts +8 -1
  84. package/dist/merge-lock.js +16 -7
  85. package/dist/micro-worktree.d.ts +81 -13
  86. package/dist/micro-worktree.js +98 -17
  87. package/dist/migration-deployer.d.ts +1 -1
  88. package/dist/migration-deployer.js +1 -1
  89. package/dist/orchestration-advisory-loader.d.ts +2 -2
  90. package/dist/orchestration-advisory-loader.js +10 -6
  91. package/dist/orchestration-advisory.d.ts +3 -3
  92. package/dist/orchestration-advisory.js +4 -4
  93. package/dist/orchestration-di.d.ts +4 -4
  94. package/dist/orchestration-di.js +4 -4
  95. package/dist/orchestration-rules.d.ts +4 -4
  96. package/dist/orchestration-rules.js +18 -10
  97. package/dist/orphan-detector.d.ts +3 -3
  98. package/dist/orphan-detector.js +3 -3
  99. package/dist/patrol-loop.d.ts +170 -0
  100. package/dist/patrol-loop.js +186 -0
  101. package/dist/process-detector.d.ts +5 -5
  102. package/dist/process-detector.js +5 -5
  103. package/dist/rebase-artifact-cleanup.d.ts +3 -3
  104. package/dist/rebase-artifact-cleanup.js +3 -3
  105. package/dist/resolve-policy.d.ts +195 -0
  106. package/dist/resolve-policy.js +203 -0
  107. package/dist/risk-detector.d.ts +2 -2
  108. package/dist/risk-detector.js +2 -2
  109. package/dist/rollback-utils.d.ts +1 -1
  110. package/dist/rollback-utils.js +1 -1
  111. package/dist/section-headings.d.ts +1 -1
  112. package/dist/section-headings.js +1 -1
  113. package/dist/spawn-escalation.d.ts +4 -4
  114. package/dist/spawn-escalation.js +3 -3
  115. package/dist/spawn-monitor.d.ts +4 -4
  116. package/dist/spawn-monitor.js +4 -4
  117. package/dist/spawn-recovery.d.ts +3 -3
  118. package/dist/spawn-recovery.js +3 -3
  119. package/dist/spawn-registry-schema.d.ts +2 -2
  120. package/dist/spawn-registry-schema.js +2 -2
  121. package/dist/spawn-registry-store.d.ts +2 -2
  122. package/dist/spawn-registry-store.js +2 -2
  123. package/dist/spawn-strategy.d.ts +17 -11
  124. package/dist/spawn-strategy.js +47 -44
  125. package/dist/spawn-tree.d.ts +3 -3
  126. package/dist/spawn-tree.js +3 -3
  127. package/dist/state-cleanup-core.d.ts +205 -0
  128. package/dist/state-cleanup-core.js +240 -0
  129. package/dist/state-doctor-core.d.ts +168 -0
  130. package/dist/state-doctor-core.js +251 -0
  131. package/dist/stream-error-handler.d.ts +67 -0
  132. package/dist/stream-error-handler.js +94 -0
  133. package/dist/telemetry.d.ts +1 -1
  134. package/dist/telemetry.js +1 -1
  135. package/dist/template-loader.d.ts +162 -0
  136. package/dist/template-loader.js +372 -0
  137. package/dist/test-baseline.d.ts +176 -0
  138. package/dist/test-baseline.js +282 -0
  139. package/dist/usecases/get-suggestions.usecase.d.ts +1 -1
  140. package/dist/validation/command-registry.js +37 -0
  141. package/dist/validators/backlog-sync.js +4 -2
  142. package/dist/worktree-scanner.d.ts +1 -1
  143. package/dist/worktree-scanner.js +1 -1
  144. package/dist/worktree-symlink.d.ts +3 -3
  145. package/dist/worktree-symlink.js +3 -3
  146. package/dist/wu-backlog-updater.d.ts +1 -1
  147. package/dist/wu-backlog-updater.js +1 -1
  148. package/dist/wu-claim-helpers.d.ts +1 -1
  149. package/dist/wu-claim-helpers.js +1 -1
  150. package/dist/wu-claim-resume.d.ts +1 -1
  151. package/dist/wu-claim-resume.js +1 -1
  152. package/dist/wu-consistency-checker.d.ts +1 -1
  153. package/dist/wu-consistency-checker.js +17 -11
  154. package/dist/wu-constants.d.ts +73 -21
  155. package/dist/wu-constants.js +65 -22
  156. package/dist/wu-done-branch-only.d.ts +1 -1
  157. package/dist/wu-done-branch-only.js +1 -1
  158. package/dist/wu-done-docs-generate.d.ts +1 -1
  159. package/dist/wu-done-docs-generate.js +1 -1
  160. package/dist/wu-done-messages.d.ts +2 -2
  161. package/dist/wu-done-messages.js +2 -2
  162. package/dist/wu-done-metadata.d.ts +3 -3
  163. package/dist/wu-done-metadata.js +3 -3
  164. package/dist/wu-done-pr.d.ts +1 -1
  165. package/dist/wu-done-pr.js +4 -2
  166. package/dist/wu-done-preflight.d.ts +8 -0
  167. package/dist/wu-done-preflight.js +18 -2
  168. package/dist/wu-done-ui.d.ts +3 -3
  169. package/dist/wu-done-ui.js +3 -3
  170. package/dist/wu-done-validation.d.ts +30 -0
  171. package/dist/wu-done-validation.js +106 -1
  172. package/dist/wu-done-worktree.d.ts +1 -1
  173. package/dist/wu-done-worktree.js +11 -1
  174. package/dist/wu-events-cleanup.d.ts +148 -0
  175. package/dist/wu-events-cleanup.js +401 -0
  176. package/dist/wu-helpers.d.ts +2 -2
  177. package/dist/wu-helpers.js +2 -2
  178. package/dist/wu-id-generator.d.ts +58 -0
  179. package/dist/wu-id-generator.js +103 -0
  180. package/dist/wu-lint.js +1 -1
  181. package/dist/wu-preflight-validators.d.ts +13 -1
  182. package/dist/wu-preflight-validators.js +56 -1
  183. package/dist/wu-recovery.d.ts +2 -2
  184. package/dist/wu-recovery.js +4 -4
  185. package/dist/wu-repair-core.d.ts +5 -5
  186. package/dist/wu-repair-core.js +6 -6
  187. package/dist/wu-schema-normalization.d.ts +1 -1
  188. package/dist/wu-schema-normalization.js +1 -1
  189. package/dist/wu-schema.d.ts +7 -7
  190. package/dist/wu-schema.js +8 -8
  191. package/dist/wu-spawn-context.d.ts +87 -0
  192. package/dist/wu-spawn-context.js +175 -0
  193. package/dist/wu-spawn-helpers.d.ts +1 -1
  194. package/dist/wu-spawn-helpers.js +1 -1
  195. package/dist/wu-spawn.d.ts +177 -4
  196. package/dist/wu-spawn.js +694 -72
  197. package/dist/wu-state-schema.d.ts +1 -1
  198. package/dist/wu-state-schema.js +1 -1
  199. package/dist/wu-state-store.d.ts +3 -3
  200. package/dist/wu-state-store.js +3 -3
  201. package/dist/wu-status-transition.d.ts +1 -1
  202. package/dist/wu-status-transition.js +1 -1
  203. package/dist/wu-status-updater.d.ts +3 -3
  204. package/dist/wu-status-updater.js +3 -3
  205. package/dist/wu-validation-constants.d.ts +2 -2
  206. package/dist/wu-validation-constants.js +2 -2
  207. package/dist/wu-validation.d.ts +3 -3
  208. package/dist/wu-validation.js +3 -3
  209. package/dist/wu-yaml-fixer.d.ts +2 -2
  210. package/dist/wu-yaml-fixer.js +3 -3
  211. package/dist/wu-yaml.d.ts +23 -0
  212. package/dist/wu-yaml.js +76 -2
  213. package/package.json +5 -2
@@ -3,7 +3,7 @@
3
3
  * Lane Occupancy Checker
4
4
  *
5
5
  * Enforces one-WU-per-lane rule by checking status.md for active WUs in a given lane.
6
- * Used by wu-claim.mjs and wu-unblock.mjs to prevent WIP violations.
6
+ * Used by wu-claim.ts and wu-unblock.ts to prevent WIP violations.
7
7
  */
8
8
  import { existsSync, readFileSync } from 'node:fs';
9
9
  import path from 'node:path';
@@ -114,6 +114,77 @@ const SPACE = ' ';
114
114
  * @property {boolean} [strict=true] - When true, throws error for parent-only lanes with taxonomy.
115
115
  * When false, only warns (for existing WU validation).
116
116
  */
117
+ /**
118
+ * WU-1197: Validate colon format in lane string
119
+ * @throws {LumenflowError} If format is invalid
120
+ */
121
+ function validateColonFormat(lane, trimmed, colonIndex) {
122
+ // Check for space before colon
123
+ if (colonIndex > 0 && trimmed[colonIndex - 1] === SPACE) {
124
+ throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" has space before colon. Expected format: "Parent: Subdomain" (space AFTER colon only)`, { lane });
125
+ }
126
+ // Check for space after colon
127
+ if (colonIndex + 1 >= trimmed.length || trimmed[colonIndex + 1] !== SPACE) {
128
+ throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" is missing space after colon. Expected format: "Parent: Subdomain"`, { lane });
129
+ }
130
+ }
131
+ /**
132
+ * WU-1197: Validate sub-lane format (Parent: Subdomain)
133
+ * @throws {LumenflowError} If validation fails
134
+ */
135
+ function validateSubLaneFormat(lane, trimmed, colonIndex, configPath) {
136
+ validateColonFormat(lane, trimmed, colonIndex);
137
+ // Extract parent and subdomain (colonIndex + 2 = skip colon and space)
138
+ const parent = trimmed.substring(0, colonIndex).trim();
139
+ const subdomain = trimmed.substring(colonIndex + LANE_SEPARATOR.length + SPACE.length).trim();
140
+ // Validate parent exists in config
141
+ if (!isValidParentLane(parent, configPath)) {
142
+ throw createError(ErrorCodes.INVALID_LANE, `Unknown parent lane: "${parent}". Check ${CONFIG_FILES.LUMENFLOW_CONFIG} for valid lanes.`, { parent, lane });
143
+ }
144
+ // Validate sub-lane exists in taxonomy
145
+ if (hasSubLaneTaxonomy(parent)) {
146
+ validateSubLaneInTaxonomy(parent, subdomain);
147
+ }
148
+ else {
149
+ // Parent has no taxonomy - reject sub-lane format
150
+ throw createError(ErrorCodes.INVALID_LANE, `Parent lane "${parent}" does not support sub-lanes. Use parent-only format or extend ${CONFIG_FILES.LANE_INFERENCE}.`, { parent, lane });
151
+ }
152
+ return { valid: true, parent, error: null };
153
+ }
154
+ /**
155
+ * WU-1197: Validate that sub-lane exists in taxonomy
156
+ * @throws {LumenflowError} If sub-lane is not valid
157
+ */
158
+ function validateSubLaneInTaxonomy(parent, subdomain) {
159
+ if (!isValidSubLane(parent, subdomain)) {
160
+ const validSubLanes = getSubLanesForParent(parent);
161
+ throw createError(ErrorCodes.INVALID_LANE, `Unknown sub-lane: "${subdomain}" for parent lane "${parent}".\n\n` +
162
+ `Valid sub-lanes: ${validSubLanes.join(', ')}`, { parent, subdomain, validSubLanes });
163
+ }
164
+ }
165
+ /**
166
+ * WU-1197: Validate parent-only lane format
167
+ * @throws {LumenflowError} If validation fails (in strict mode)
168
+ */
169
+ function validateParentOnlyFormat(trimmed, configPath, strict) {
170
+ if (!isValidParentLane(trimmed, configPath)) {
171
+ throw createError(ErrorCodes.INVALID_LANE, `Unknown parent lane: "${trimmed}". Check ${CONFIG_FILES.LUMENFLOW_CONFIG} for valid lanes.`, { lane: trimmed });
172
+ }
173
+ // Block if parent has sub-lane taxonomy (sub-lane required)
174
+ if (hasSubLaneTaxonomy(trimmed)) {
175
+ const validSubLanes = getSubLanesForParent(trimmed);
176
+ const message = `Parent-only lane "${trimmed}" blocked. Sub-lane required. ` +
177
+ `Valid: ${validSubLanes.join(', ')}. ` +
178
+ `Format: "${trimmed}: <sublane>"`;
179
+ if (strict) {
180
+ throw createError(ErrorCodes.INVALID_LANE, message, { lane: trimmed, validSubLanes });
181
+ }
182
+ // Non-strict mode: warn only for existing WU validation
183
+ // eslint-disable-next-line no-console -- Intentional operational logging
184
+ console.warn(`${PREFIX} ⚠️ ${message}`);
185
+ }
186
+ return { valid: true, parent: trimmed, error: null };
187
+ }
117
188
  /**
118
189
  * Validate lane format and parent existence
119
190
  * @param {string} lane - Lane name (e.g., "Operations: Tooling" or "Operations")
@@ -129,62 +200,55 @@ export function validateLaneFormat(lane, configPath = null, options = {}) {
129
200
  if (colonCount > 1) {
130
201
  throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" contains multiple colons. Expected format: "Parent: Subdomain" or "Parent"`, { lane });
131
202
  }
132
- // Check for colon
133
203
  const colonIndex = trimmed.indexOf(LANE_SEPARATOR);
134
- if (colonIndex !== -1) {
135
- // Sub-lane format validation
136
- // Check for space before colon
137
- if (colonIndex > 0 && trimmed[colonIndex - 1] === SPACE) {
138
- throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" has space before colon. Expected format: "Parent: Subdomain" (space AFTER colon only)`, { lane });
139
- }
140
- // Check for space after colon
141
- if (colonIndex + 1 >= trimmed.length || trimmed[colonIndex + 1] !== SPACE) {
142
- throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" is missing space after colon. Expected format: "Parent: Subdomain"`, { lane });
143
- }
144
- // Extract parent and subdomain (colonIndex + 2 = skip colon and space)
145
- const parent = trimmed.substring(0, colonIndex).trim();
146
- const subdomain = trimmed.substring(colonIndex + LANE_SEPARATOR.length + SPACE.length).trim();
147
- // Validate parent exists in config
148
- if (!isValidParentLane(parent, configPath)) {
149
- throw createError(ErrorCodes.INVALID_LANE, `Unknown parent lane: "${parent}". Check ${CONFIG_FILES.LUMENFLOW_CONFIG} for valid lanes.`, { parent, lane });
150
- }
151
- // Validate sub-lane exists in taxonomy
152
- if (hasSubLaneTaxonomy(parent)) {
153
- // Parent has taxonomy - validate sub-lane
154
- if (!isValidSubLane(parent, subdomain)) {
155
- const validSubLanes = getSubLanesForParent(parent);
156
- throw createError(ErrorCodes.INVALID_LANE, `Unknown sub-lane: "${subdomain}" for parent lane "${parent}".\n\n` +
157
- `Valid sub-lanes: ${validSubLanes.join(', ')}`, { parent, subdomain, validSubLanes });
158
- }
159
- }
160
- else {
161
- // Parent has no taxonomy - reject sub-lane format
162
- throw createError(ErrorCodes.INVALID_LANE, `Parent lane "${parent}" does not support sub-lanes. Use parent-only format or extend ${CONFIG_FILES.LANE_INFERENCE}.`, { parent, lane });
163
- }
164
- return { valid: true, parent, error: null };
204
+ const isSubLaneFormat = colonIndex !== -1;
205
+ if (isSubLaneFormat) {
206
+ return validateSubLaneFormat(lane, trimmed, colonIndex, configPath);
165
207
  }
166
- else {
167
- // Parent-only format
168
- if (!isValidParentLane(trimmed, configPath)) {
169
- throw createError(ErrorCodes.INVALID_LANE, `Unknown parent lane: "${trimmed}". Check ${CONFIG_FILES.LUMENFLOW_CONFIG} for valid lanes.`, { lane: trimmed });
170
- }
171
- // Block if parent has sub-lane taxonomy (sub-lane required)
172
- if (hasSubLaneTaxonomy(trimmed)) {
173
- const validSubLanes = getSubLanesForParent(trimmed);
174
- const message = `Parent-only lane "${trimmed}" blocked. Sub-lane required. ` +
175
- `Valid: ${validSubLanes.join(', ')}. ` +
176
- `Format: "${trimmed}: <sublane>"`;
177
- if (strict) {
178
- // Strict mode (default): throw error for new WUs
179
- throw createError(ErrorCodes.INVALID_LANE, message, { lane: trimmed, validSubLanes });
180
- }
181
- else {
182
- // Non-strict mode: warn only for existing WU validation
183
- console.warn(`${PREFIX} ⚠️ ${message}`);
184
- }
208
+ return validateParentOnlyFormat(trimmed, configPath, strict);
209
+ }
210
+ /**
211
+ * WU-1197: Extract lane names and parent lanes from config
212
+ * Handles flat array, definitions, and legacy nested formats
213
+ */
214
+ function extractLanesForParentCheck(config) {
215
+ const allLanes = [];
216
+ const parentLanes = new Set();
217
+ if (!config.lanes) {
218
+ return { allLanes, parentLanes };
219
+ }
220
+ if (Array.isArray(config.lanes)) {
221
+ // Flat array format: lanes: [{name: "Core"}, {name: "CLI"}, ...]
222
+ allLanes.push(...config.lanes.map((l) => l.name));
223
+ return { allLanes, parentLanes };
224
+ }
225
+ // WU-1022: New format with lanes.definitions containing full "Parent: Sublane" names
226
+ if (config.lanes.definitions) {
227
+ for (const lane of config.lanes.definitions) {
228
+ allLanes.push(lane.name);
229
+ // Extract parent from full lane name for parent validation
230
+ const extracted = extractParent(lane.name);
231
+ parentLanes.add(extracted.toLowerCase().trim());
185
232
  }
186
- return { valid: true, parent: trimmed, error: null };
187
233
  }
234
+ // Legacy nested format: lanes: {engineering: [...], business: [...]}
235
+ if (config.lanes.engineering) {
236
+ allLanes.push(...config.lanes.engineering.map((l) => l.name));
237
+ }
238
+ if (config.lanes.business) {
239
+ allLanes.push(...config.lanes.business.map((l) => l.name));
240
+ }
241
+ return { allLanes, parentLanes };
242
+ }
243
+ /**
244
+ * WU-1197: Resolve config path, defaulting to project root if not provided
245
+ */
246
+ function resolveConfigPath(configPath) {
247
+ if (configPath) {
248
+ return configPath;
249
+ }
250
+ const projectRoot = findProjectRoot();
251
+ return path.join(projectRoot, CONFIG_FILES.LUMENFLOW_CONFIG);
188
252
  }
189
253
  /**
190
254
  * Check if a parent lane exists in LumenFlow config
@@ -197,12 +261,7 @@ export function validateLaneFormat(lane, configPath = null, options = {}) {
197
261
  * @returns {boolean} True if parent lane exists
198
262
  */
199
263
  function isValidParentLane(parent, configPath = null) {
200
- // Determine config path
201
- let resolvedConfigPath = configPath;
202
- if (!resolvedConfigPath) {
203
- const projectRoot = findProjectRoot();
204
- resolvedConfigPath = path.join(projectRoot, CONFIG_FILES.LUMENFLOW_CONFIG);
205
- }
264
+ const resolvedConfigPath = resolveConfigPath(configPath);
206
265
  // Read and parse config
207
266
  if (!existsSync(resolvedConfigPath)) {
208
267
  throw createError(ErrorCodes.FILE_NOT_FOUND, `Config file not found: ${resolvedConfigPath}`, {
@@ -211,34 +270,7 @@ function isValidParentLane(parent, configPath = null) {
211
270
  }
212
271
  const configContent = readFileSync(resolvedConfigPath, { encoding: 'utf-8' });
213
272
  const config = parseYAML(configContent);
214
- // Extract all lane names - handle multiple config formats
215
- const allLanes = [];
216
- const parentLanes = new Set();
217
- if (config.lanes) {
218
- if (Array.isArray(config.lanes)) {
219
- // Flat array format: lanes: [{name: "Core"}, {name: "CLI"}, ...]
220
- allLanes.push(...config.lanes.map((l) => l.name));
221
- }
222
- else {
223
- // WU-1022: New format with lanes.definitions containing full "Parent: Sublane" names
224
- if (config.lanes.definitions) {
225
- for (const lane of config.lanes.definitions) {
226
- allLanes.push(lane.name);
227
- // Extract parent from full lane name for parent validation
228
- const extractedParent = extractParent(lane.name);
229
- parentLanes.add(extractedParent.toLowerCase().trim());
230
- }
231
- }
232
- // Legacy nested format: lanes: {engineering: [...], business: [...]}
233
- if (config.lanes.engineering) {
234
- allLanes.push(...config.lanes.engineering.map((l) => l.name));
235
- }
236
- if (config.lanes.business) {
237
- allLanes.push(...config.lanes.business.map((l) => l.name));
238
- }
239
- }
240
- }
241
- // Case-insensitive comparison
273
+ const { allLanes, parentLanes } = extractLanesForParentCheck(config);
242
274
  const normalizedParent = parent.toLowerCase().trim();
243
275
  // WU-1022: If we have extracted parent lanes (from full lane names), check against those
244
276
  if (parentLanes.size > 0) {
@@ -285,7 +317,11 @@ export function getWipLimitForLane(lane, options = {}) {
285
317
  allLanes = config.lanes;
286
318
  }
287
319
  else {
288
- // Nested format: lanes: {engineering: [...], business: [...]}
320
+ // New format with definitions
321
+ if (config.lanes.definitions) {
322
+ allLanes.push(...config.lanes.definitions);
323
+ }
324
+ // Legacy nested format: lanes: {engineering: [...], business: [...]}
289
325
  if (config.lanes.engineering) {
290
326
  allLanes.push(...config.lanes.engineering);
291
327
  }
@@ -306,6 +342,93 @@ export function getWipLimitForLane(lane, options = {}) {
306
342
  return DEFAULT_WIP_LIMIT;
307
343
  }
308
344
  }
345
+ /** WU-1197: Section heading marker for H2 headings */
346
+ const SECTION_HEADING_PREFIX = '## ';
347
+ /**
348
+ * WU-1197: Create an empty lane result (no WUs in progress)
349
+ */
350
+ function createEmptyLaneResult(wipLimit) {
351
+ return {
352
+ free: true,
353
+ occupiedBy: null,
354
+ error: null,
355
+ inProgressWUs: [],
356
+ wipLimit,
357
+ currentCount: 0,
358
+ };
359
+ }
360
+ /**
361
+ * WU-1197: Extract In Progress section from status.md lines
362
+ * @returns Section content or null if not found
363
+ */
364
+ function extractInProgressSection(lines) {
365
+ const inProgressIdx = lines.findIndex((l) => isInProgressHeader(l));
366
+ if (inProgressIdx === -1) {
367
+ return { section: '', error: 'Could not find "## In Progress" section in status.md' };
368
+ }
369
+ // Find end of In Progress section (next ## heading or end of file)
370
+ let endIdx = lines
371
+ .slice(inProgressIdx + 1)
372
+ .findIndex((l) => l.startsWith(SECTION_HEADING_PREFIX));
373
+ if (endIdx === -1) {
374
+ endIdx = lines.length - inProgressIdx - 1;
375
+ }
376
+ else {
377
+ endIdx = inProgressIdx + 1 + endIdx;
378
+ }
379
+ const section = lines.slice(inProgressIdx + 1, endIdx).join(STRING_LITERALS.NEWLINE);
380
+ return { section, error: null };
381
+ }
382
+ /**
383
+ * WU-1197: Check if a WU belongs to the target lane
384
+ * @returns The WU ID if it matches the target lane, null otherwise
385
+ */
386
+ function checkWuLaneMatch(activeWuid, wuid, projectRoot, targetLane) {
387
+ // Skip if it's the same WU we're trying to claim
388
+ if (activeWuid === wuid) {
389
+ return null;
390
+ }
391
+ const wuPath = path.join(projectRoot, WU_PATHS.WU(activeWuid));
392
+ if (!existsSync(wuPath)) {
393
+ // eslint-disable-next-line no-console -- Intentional operational logging
394
+ console.warn(`${PREFIX} Warning: ${activeWuid} referenced in status.md but ${wuPath} not found`);
395
+ return null;
396
+ }
397
+ try {
398
+ const wuContent = readFileSync(wuPath, { encoding: 'utf-8' });
399
+ const wuDoc = parseYAML(wuContent);
400
+ if (!wuDoc || !wuDoc.lane) {
401
+ // eslint-disable-next-line no-console -- Intentional operational logging
402
+ console.warn(`${PREFIX} Warning: ${activeWuid} has no lane field`);
403
+ return null;
404
+ }
405
+ // Normalize lane names for comparison (case-insensitive, trim whitespace)
406
+ const activeLane = wuDoc.lane.toString().trim().toLowerCase();
407
+ if (activeLane === targetLane) {
408
+ return activeWuid;
409
+ }
410
+ }
411
+ catch (e) {
412
+ const errMessage = e instanceof Error ? e.message : String(e);
413
+ // eslint-disable-next-line no-console -- Intentional operational logging
414
+ console.warn(`${PREFIX} Warning: Failed to parse ${activeWuid} YAML: ${errMessage}`);
415
+ }
416
+ return null;
417
+ }
418
+ /**
419
+ * WU-1197: Collect WUs in the target lane from matched WU links
420
+ */
421
+ function collectInProgressWUsForLane(matches, wuid, projectRoot, targetLane) {
422
+ const inProgressWUs = [];
423
+ for (const match of matches) {
424
+ const activeWuid = match[1]; // e.g., "WU-334"
425
+ const matchedWu = checkWuLaneMatch(activeWuid, wuid, projectRoot, targetLane);
426
+ if (matchedWu) {
427
+ inProgressWUs.push(matchedWu);
428
+ }
429
+ }
430
+ return inProgressWUs;
431
+ }
309
432
  /**
310
433
  * Check if a lane is free (in_progress WU count is below wip_limit)
311
434
  *
@@ -320,8 +443,6 @@ export function getWipLimitForLane(lane, options = {}) {
320
443
  * @returns {{ free: boolean, occupiedBy: string | null, error: string | null, inProgressWUs?: string[], wipLimit?: number, currentCount?: number }}
321
444
  */
322
445
  export function checkLaneFree(statusPath, lane, wuid, options = {}) {
323
- /** Section heading marker for H2 headings */
324
- const SECTION_HEADING_PREFIX = '## ';
325
446
  try {
326
447
  // Read status.md
327
448
  if (!existsSync(statusPath)) {
@@ -329,88 +450,26 @@ export function checkLaneFree(statusPath, lane, wuid, options = {}) {
329
450
  }
330
451
  const content = readFileSync(statusPath, { encoding: 'utf-8' });
331
452
  const lines = content.split(/\r?\n/);
332
- // Find "## In Progress" section
333
- const inProgressIdx = lines.findIndex((l) => isInProgressHeader(l));
334
- if (inProgressIdx === -1) {
335
- return {
336
- free: false,
337
- occupiedBy: null,
338
- error: 'Could not find "## In Progress" section in status.md',
339
- };
453
+ const { section, error } = extractInProgressSection(lines);
454
+ if (error) {
455
+ return { free: false, occupiedBy: null, error };
340
456
  }
341
- // Find end of In Progress section (next ## heading or end of file)
342
- let endIdx = lines
343
- .slice(inProgressIdx + 1)
344
- .findIndex((l) => l.startsWith(SECTION_HEADING_PREFIX));
345
- if (endIdx === -1)
346
- endIdx = lines.length - inProgressIdx - 1;
347
- else
348
- endIdx = inProgressIdx + 1 + endIdx;
349
- // Extract WU links from In Progress section
350
- const section = lines.slice(inProgressIdx + 1, endIdx).join(STRING_LITERALS.NEWLINE);
351
457
  // WU-1016: Get WIP limit for this lane from config
352
458
  const wipLimit = getWipLimitForLane(lane, { configPath: options.configPath });
353
- // Check for "No items" marker
459
+ // Check for "No items" marker or no WU links
354
460
  if (section.includes(NO_ITEMS_MARKER)) {
355
- return {
356
- free: true,
357
- occupiedBy: null,
358
- error: null,
359
- inProgressWUs: [],
360
- wipLimit,
361
- currentCount: 0,
362
- };
461
+ return createEmptyLaneResult(wipLimit);
363
462
  }
364
463
  // Extract WU IDs from links like [WU-334 — Title](wu/WU-334.yaml)
365
464
  WU_LINK_PATTERN.lastIndex = 0; // Reset global regex state
366
465
  const matches = [...section.matchAll(WU_LINK_PATTERN)];
367
466
  if (matches.length === 0) {
368
- return {
369
- free: true,
370
- occupiedBy: null,
371
- error: null,
372
- inProgressWUs: [],
373
- wipLimit,
374
- currentCount: 0,
375
- };
467
+ return createEmptyLaneResult(wipLimit);
376
468
  }
377
469
  // Get project root from statusPath (docs/04-operations/tasks/status.md)
378
- // Use path.dirname 4 times: status.md -> tasks -> 04-operations -> docs -> root
379
470
  const projectRoot = path.dirname(path.dirname(path.dirname(path.dirname(statusPath))));
380
- // WU-1016: Collect all WUs in the target lane
381
- const inProgressWUs = [];
382
471
  const targetLane = lane.toString().trim().toLowerCase();
383
- for (const match of matches) {
384
- const activeWuid = match[1]; // e.g., "WU-334"
385
- // Skip if it's the same WU we're trying to claim (shouldn't happen, but be safe)
386
- if (activeWuid === wuid)
387
- continue;
388
- // Use WU_PATHS to build the path consistently
389
- const wuPath = path.join(projectRoot, WU_PATHS.WU(activeWuid));
390
- if (!existsSync(wuPath)) {
391
- console.warn(`${PREFIX} Warning: ${activeWuid} referenced in status.md but ${wuPath} not found`);
392
- continue;
393
- }
394
- try {
395
- const wuContent = readFileSync(wuPath, { encoding: 'utf-8' });
396
- const wuDoc = parseYAML(wuContent);
397
- if (!wuDoc || !wuDoc.lane) {
398
- console.warn(`${PREFIX} Warning: ${activeWuid} has no lane field`);
399
- continue;
400
- }
401
- // Normalize lane names for comparison (case-insensitive, trim whitespace)
402
- const activeLane = wuDoc.lane.toString().trim().toLowerCase();
403
- if (activeLane === targetLane) {
404
- // WU-1016: Add to list of in-progress WUs in this lane
405
- inProgressWUs.push(activeWuid);
406
- }
407
- }
408
- catch (e) {
409
- const errMessage = e instanceof Error ? e.message : String(e);
410
- console.warn(`${PREFIX} Warning: Failed to parse ${activeWuid} YAML: ${errMessage}`);
411
- continue;
412
- }
413
- }
472
+ const inProgressWUs = collectInProgressWUsForLane(matches, wuid, projectRoot, targetLane);
414
473
  // WU-1016: Check if lane is free based on WIP limit
415
474
  const currentCount = inProgressWUs.length;
416
475
  const isFree = currentCount < wipLimit;
@@ -428,3 +487,101 @@ export function checkLaneFree(statusPath, lane, wuid, options = {}) {
428
487
  return { free: false, occupiedBy: null, error: `Unexpected error: ${errMessage}` };
429
488
  }
430
489
  }
490
+ /** WU-1187: Default result when no justification is required */
491
+ const NO_JUSTIFICATION_REQUIRED = {
492
+ valid: true,
493
+ warning: null,
494
+ requiresJustification: false,
495
+ };
496
+ /**
497
+ * WU-1187: Extract all lanes from config (handles all config formats)
498
+ * @param config - Parsed LumenFlow config
499
+ * @returns Array of lane configs with wip settings
500
+ */
501
+ function extractAllLanesFromConfig(config) {
502
+ if (!config.lanes) {
503
+ return [];
504
+ }
505
+ if (Array.isArray(config.lanes)) {
506
+ // Flat array format: lanes: [{name: "Core", wip_limit: 2}, ...]
507
+ return config.lanes;
508
+ }
509
+ // Nested formats with definitions, engineering, business
510
+ const allLanes = [];
511
+ if (config.lanes.definitions) {
512
+ allLanes.push(...config.lanes.definitions);
513
+ }
514
+ if (config.lanes.engineering) {
515
+ allLanes.push(...config.lanes.engineering);
516
+ }
517
+ if (config.lanes.business) {
518
+ allLanes.push(...config.lanes.business);
519
+ }
520
+ return allLanes;
521
+ }
522
+ /**
523
+ * WU-1187: Check if a lane has WIP justification when required
524
+ *
525
+ * Philosophy: If you need WIP > 1, you need better lanes, not higher limits.
526
+ * This is soft enforcement: logs a warning at claim time, but doesn't block.
527
+ *
528
+ * @param {string} lane - Lane name to check
529
+ * @param {CheckWipJustificationOptions} options - Options including configPath for testing
530
+ * @returns {CheckWipJustificationResult} Result with valid=true (always) and optional warning
531
+ */
532
+ export function checkWipJustification(lane, options = {}) {
533
+ // Determine config path
534
+ let resolvedConfigPath = options.configPath;
535
+ if (!resolvedConfigPath) {
536
+ const projectRoot = findProjectRoot();
537
+ resolvedConfigPath = path.join(projectRoot, CONFIG_FILES.LUMENFLOW_CONFIG);
538
+ }
539
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Config path is validated
540
+ if (!existsSync(resolvedConfigPath)) {
541
+ return NO_JUSTIFICATION_REQUIRED;
542
+ }
543
+ try {
544
+ // eslint-disable-next-line security/detect-non-literal-fs-filename -- Config path is validated
545
+ const configContent = readFileSync(resolvedConfigPath, { encoding: 'utf-8' });
546
+ const config = parseYAML(configContent);
547
+ const allLanes = extractAllLanesFromConfig(config);
548
+ if (allLanes.length === 0) {
549
+ return NO_JUSTIFICATION_REQUIRED;
550
+ }
551
+ // Find matching lane (case-insensitive)
552
+ const normalizedLane = lane.toLowerCase().trim();
553
+ const matchingLane = allLanes.find((l) => l.name.toLowerCase().trim() === normalizedLane);
554
+ if (!matchingLane) {
555
+ return NO_JUSTIFICATION_REQUIRED;
556
+ }
557
+ const wipLimit = matchingLane.wip_limit ?? DEFAULT_WIP_LIMIT;
558
+ // WIP <= 1 doesn't need justification
559
+ if (wipLimit <= 1) {
560
+ return NO_JUSTIFICATION_REQUIRED;
561
+ }
562
+ // WIP > 1 - check for justification
563
+ const justification = matchingLane.wip_justification;
564
+ if (justification && justification.trim().length > 0) {
565
+ // Has justification - all good
566
+ return {
567
+ valid: true,
568
+ warning: null,
569
+ requiresJustification: false,
570
+ justification: justification.trim(),
571
+ };
572
+ }
573
+ // WIP > 1 without justification - emit warning
574
+ const warning = `Lane "${lane}" has WIP limit of ${wipLimit} but no wip_justification. ` +
575
+ `Philosophy: If you need WIP > 1, you need better lanes, not higher limits. ` +
576
+ `Add wip_justification to .lumenflow.config.yaml to suppress this warning.`;
577
+ return {
578
+ valid: true, // Soft enforcement - warning only, doesn't block
579
+ warning,
580
+ requiresJustification: true,
581
+ };
582
+ }
583
+ catch {
584
+ // If config parsing fails, don't block
585
+ return NO_JUSTIFICATION_REQUIRED;
586
+ }
587
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Lane Suggestion Prompt Module (WU-1189)
3
+ *
4
+ * Generates prompts for LLM-driven lane suggestions based on codebase context.
5
+ * This module gathers project context and generates structured prompts for
6
+ * the LLM to analyze and suggest appropriate lane definitions.
7
+ *
8
+ * Design decisions:
9
+ * - Uses LLM for semantic understanding (not hardcoded heuristics)
10
+ * - Gathers context from docs, structure, and existing config
11
+ * - Returns structured JSON for easy parsing
12
+ */
13
+ import { z } from 'zod';
14
+ /**
15
+ * Schema for a single lane suggestion from the LLM
16
+ */
17
+ export declare const LaneSuggestionSchema: z.ZodObject<{
18
+ lane: z.ZodString;
19
+ description: z.ZodString;
20
+ rationale: z.ZodString;
21
+ code_paths: z.ZodArray<z.ZodString>;
22
+ keywords: z.ZodArray<z.ZodString>;
23
+ }, z.core.$strip>;
24
+ export type LaneSuggestion = z.infer<typeof LaneSuggestionSchema>;
25
+ /**
26
+ * Schema for the complete LLM response
27
+ */
28
+ export declare const LaneSuggestResponseSchema: z.ZodObject<{
29
+ suggestions: z.ZodArray<z.ZodObject<{
30
+ lane: z.ZodString;
31
+ description: z.ZodString;
32
+ rationale: z.ZodString;
33
+ code_paths: z.ZodArray<z.ZodString>;
34
+ keywords: z.ZodArray<z.ZodString>;
35
+ }, z.core.$strip>>;
36
+ }, z.core.$strip>;
37
+ export type LaneSuggestResponse = z.infer<typeof LaneSuggestResponseSchema>;
38
+ /**
39
+ * Project context gathered for LLM analysis
40
+ */
41
+ export interface ProjectContext {
42
+ /** Names of packages found (for monorepos) */
43
+ packageNames: string[];
44
+ /** Top-level directory structure */
45
+ directoryStructure: string[];
46
+ /** Content of README.md if present */
47
+ readme: string | null;
48
+ /** Content of package.json if present */
49
+ packageJson: {
50
+ name?: string;
51
+ description?: string;
52
+ workspaces?: string[];
53
+ } | null;
54
+ /** Existing lane definitions if any */
55
+ existingLanes: string[];
56
+ /** Whether docs directory exists */
57
+ hasDocsDir: boolean;
58
+ /** Whether apps directory exists (common in monorepos) */
59
+ hasAppsDir: boolean;
60
+ /** Whether packages directory exists (monorepo indicator) */
61
+ hasPackagesDir: boolean;
62
+ /** Whether it's a monorepo */
63
+ isMonorepo: boolean;
64
+ }
65
+ /**
66
+ * Result of lane suggestion including suggestions and metadata
67
+ */
68
+ export interface LaneSuggestResult {
69
+ suggestions: LaneSuggestion[];
70
+ context: {
71
+ packageCount: number;
72
+ docsFound: boolean;
73
+ existingConfig: boolean;
74
+ };
75
+ }
76
+ /**
77
+ * Gather project context from the filesystem
78
+ */
79
+ export declare function gatherProjectContext(projectRoot: string): ProjectContext;
80
+ /**
81
+ * Generate the system prompt for lane suggestion
82
+ */
83
+ export declare function generateSystemPrompt(): string;
84
+ /**
85
+ * Generate the user prompt with project context
86
+ */
87
+ export declare function generateUserPrompt(context: ProjectContext): string;
88
+ /**
89
+ * Parse and validate LLM response
90
+ */
91
+ export declare function parseLLMResponse(response: string): LaneSuggestResponse;
92
+ /**
93
+ * Validate that a lane name follows the "Parent: Sublane" format
94
+ */
95
+ export declare function isValidLaneFormat(lane: string): boolean;
96
+ /**
97
+ * Generate a dry-run preview of what would be sent to the LLM
98
+ */
99
+ export declare function generateDryRunPreview(projectRoot: string): {
100
+ context: ProjectContext;
101
+ systemPrompt: string;
102
+ userPrompt: string;
103
+ };
104
+ /**
105
+ * Get default lane suggestions for a minimal project
106
+ * Used when LLM is not available or for dry-run
107
+ */
108
+ export declare function getDefaultSuggestions(context: ProjectContext): LaneSuggestion[];