@lumenflow/cli 3.12.6 → 3.12.7

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 (109) hide show
  1. package/dist/wu-claim.js +2 -1
  2. package/dist/wu-claim.js.map +1 -1
  3. package/dist/wu-done-policies.js +9 -9
  4. package/dist/wu-done-policies.js.map +1 -1
  5. package/dist/wu-spawn-strategy-resolver.js +14 -6
  6. package/dist/wu-spawn-strategy-resolver.js.map +1 -1
  7. package/package.json +8 -8
  8. package/packs/sidekick/.turbo/turbo-build.log +1 -1
  9. package/packs/sidekick/package.json +1 -1
  10. package/packs/software-delivery/.turbo/turbo-build.log +1 -1
  11. package/packs/software-delivery/package.json +1 -1
  12. package/dist/chunk-2D2VOCA4.js +0 -37
  13. package/dist/chunk-2D5KFYGX.js +0 -284
  14. package/dist/chunk-2GXVIN57.js +0 -14072
  15. package/dist/chunk-2MQ7HZWZ.js +0 -26
  16. package/dist/chunk-2UFQ3A3C.js +0 -643
  17. package/dist/chunk-3RG5ZIWI.js +0 -10
  18. package/dist/chunk-4N74J3UT.js +0 -15
  19. package/dist/chunk-5GTOXFYR.js +0 -392
  20. package/dist/chunk-5VY6MQMC.js +0 -240
  21. package/dist/chunk-67XVPMRY.js +0 -1297
  22. package/dist/chunk-6HO4GWJE.js +0 -164
  23. package/dist/chunk-6W5XHWYV.js +0 -1890
  24. package/dist/chunk-6X4EMYJQ.js +0 -64
  25. package/dist/chunk-6XYXI2NQ.js +0 -772
  26. package/dist/chunk-7ANSOV6Q.js +0 -285
  27. package/dist/chunk-A624LFLB.js +0 -1380
  28. package/dist/chunk-ADN5NHG4.js +0 -126
  29. package/dist/chunk-B7YJYJKG.js +0 -33
  30. package/dist/chunk-CCLHCPKG.js +0 -210
  31. package/dist/chunk-CK36VROC.js +0 -1584
  32. package/dist/chunk-D3UOFRSB.js +0 -81
  33. package/dist/chunk-DFR4DJBM.js +0 -230
  34. package/dist/chunk-DSYBDHYH.js +0 -79
  35. package/dist/chunk-DWMLTXKQ.js +0 -1176
  36. package/dist/chunk-E3REJTAJ.js +0 -28
  37. package/dist/chunk-EA3IVO64.js +0 -633
  38. package/dist/chunk-EK2AKZKD.js +0 -55
  39. package/dist/chunk-ELD7JTTT.js +0 -343
  40. package/dist/chunk-EX6TT2XI.js +0 -195
  41. package/dist/chunk-EXINSFZE.js +0 -82
  42. package/dist/chunk-EZ6ZBYBM.js +0 -510
  43. package/dist/chunk-FBKAPTJ2.js +0 -16
  44. package/dist/chunk-FVLV5RYH.js +0 -1118
  45. package/dist/chunk-GDNSBQVK.js +0 -2485
  46. package/dist/chunk-GPQHMBNN.js +0 -278
  47. package/dist/chunk-GTFJB67L.js +0 -68
  48. package/dist/chunk-HANJXVKW.js +0 -1127
  49. package/dist/chunk-HEVS5YLD.js +0 -269
  50. package/dist/chunk-HMEVZKPQ.js +0 -9
  51. package/dist/chunk-HRGSYNLM.js +0 -3511
  52. package/dist/chunk-ISZR5N4K.js +0 -60
  53. package/dist/chunk-J6SUPR2C.js +0 -226
  54. package/dist/chunk-JERYVEIZ.js +0 -244
  55. package/dist/chunk-JHHWGL2N.js +0 -87
  56. package/dist/chunk-JONWQUB5.js +0 -775
  57. package/dist/chunk-K2DIWWDM.js +0 -1766
  58. package/dist/chunk-KY4PGL5V.js +0 -969
  59. package/dist/chunk-L737LQ4C.js +0 -1285
  60. package/dist/chunk-LFTWYIB2.js +0 -497
  61. package/dist/chunk-LV47RFNJ.js +0 -41
  62. package/dist/chunk-MKSAITI7.js +0 -15
  63. package/dist/chunk-MZ7RKIX4.js +0 -212
  64. package/dist/chunk-NAP6CFSO.js +0 -84
  65. package/dist/chunk-ND6MY37M.js +0 -16
  66. package/dist/chunk-NMG736UR.js +0 -683
  67. package/dist/chunk-NRAXROED.js +0 -32
  68. package/dist/chunk-NRIZR3A7.js +0 -690
  69. package/dist/chunk-NX43BG3M.js +0 -233
  70. package/dist/chunk-O645XLSI.js +0 -297
  71. package/dist/chunk-OMJD6A3S.js +0 -235
  72. package/dist/chunk-QB6SJD4T.js +0 -430
  73. package/dist/chunk-QFSTL4J3.js +0 -276
  74. package/dist/chunk-QLGDFMFX.js +0 -212
  75. package/dist/chunk-RIAAGL2E.js +0 -13
  76. package/dist/chunk-RWO5XMZ6.js +0 -86
  77. package/dist/chunk-RXRKBBSM.js +0 -149
  78. package/dist/chunk-RZOZMML6.js +0 -363
  79. package/dist/chunk-U7I7FS7T.js +0 -113
  80. package/dist/chunk-UI42RODY.js +0 -717
  81. package/dist/chunk-UTVMVSCO.js +0 -519
  82. package/dist/chunk-V6OJGLBA.js +0 -1746
  83. package/dist/chunk-W2JHVH7D.js +0 -152
  84. package/dist/chunk-WD3Y7VQN.js +0 -280
  85. package/dist/chunk-WOCTQ5MS.js +0 -303
  86. package/dist/chunk-WZR3ZUNN.js +0 -696
  87. package/dist/chunk-XGI665H7.js +0 -150
  88. package/dist/chunk-XKY65P2T.js +0 -304
  89. package/dist/chunk-Y4CQZY65.js +0 -57
  90. package/dist/chunk-YFEXKLVE.js +0 -194
  91. package/dist/chunk-YHO3HS5X.js +0 -287
  92. package/dist/chunk-YLS7AZSX.js +0 -738
  93. package/dist/chunk-ZE473AO6.js +0 -49
  94. package/dist/chunk-ZF747T3O.js +0 -644
  95. package/dist/chunk-ZHCZHZH3.js +0 -43
  96. package/dist/chunk-ZZNZX2XY.js +0 -87
  97. package/dist/constants-7QAP3VQ4.js +0 -23
  98. package/dist/dist-IY3UUMWK.js +0 -33
  99. package/dist/invariants-runner-W5RGHCSU.js +0 -27
  100. package/dist/lane-lock-6J36HD5O.js +0 -35
  101. package/dist/mem-checkpoint-core-EANG2GVN.js +0 -14
  102. package/dist/mem-signal-core-2LZ2WYHW.js +0 -19
  103. package/dist/memory-store-OLB5FO7K.js +0 -18
  104. package/dist/service-6BYCOCO5.js +0 -13
  105. package/dist/spawn-policy-resolver-NTSZYQ6R.js +0 -17
  106. package/dist/spawn-task-builder-R4E2BHSW.js +0 -22
  107. package/dist/wu-done-pr-WLFFFEPJ.js +0 -25
  108. package/dist/wu-done-validation-3J5E36FE.js +0 -30
  109. package/dist/wu-duplicate-id-detector-5S7JHELK.js +0 -232
@@ -1,775 +0,0 @@
1
- import {
2
- parseYAML
3
- } from "./chunk-NRIZR3A7.js";
4
- import {
5
- ENV_VARS,
6
- LUMENFLOW_PATHS,
7
- STRING_LITERALS,
8
- getProjectRoot,
9
- toKebab
10
- } from "./chunk-DWMLTXKQ.js";
11
- import {
12
- WORKSPACE_CONFIG_FILE_NAME,
13
- WORKSPACE_V2_KEYS,
14
- WU_STATUS,
15
- asRecord,
16
- findProjectRoot,
17
- getConfig,
18
- resolveWUStatus
19
- } from "./chunk-V6OJGLBA.js";
20
- import {
21
- ErrorCodes,
22
- createError
23
- } from "./chunk-RXRKBBSM.js";
24
- import {
25
- __require
26
- } from "./chunk-3RG5ZIWI.js";
27
-
28
- // ../core/dist/lane-lock.js
29
- import { openSync, closeSync, writeFileSync, readFileSync as readFileSync2, unlinkSync, existsSync as existsSync2, mkdirSync } from "fs";
30
- import path2 from "path";
31
-
32
- // ../core/dist/lane-checker.js
33
- import { existsSync, readFileSync } from "fs";
34
- import path from "path";
35
-
36
- // ../core/dist/constants/backlog-patterns.js
37
- var IN_PROGRESS_HEADERS = ["## in progress", "## \u{1F527} in progress"];
38
- var WU_LINK_PATTERN = /\[([A-Z]+-\d+)\s*—\s*[^\]]+\]\([^)]+\)/gi;
39
- function isInProgressHeader(line) {
40
- const normalized = line.trim().toLowerCase();
41
- return IN_PROGRESS_HEADERS.some((header) => normalized === header || normalized.startsWith(header));
42
- }
43
-
44
- // ../core/dist/lane-checker.js
45
- var ACTIVE_COUNTED_STATUSES_ALL = /* @__PURE__ */ new Set([WU_STATUS.IN_PROGRESS, WU_STATUS.BLOCKED]);
46
- var ACTIVE_COUNTED_STATUSES_PROGRESS_ONLY = /* @__PURE__ */ new Set([WU_STATUS.IN_PROGRESS]);
47
- var PREFIX = "[lane-checker]";
48
- var SOFTWARE_DELIVERY_KEY = WORKSPACE_V2_KEYS.SOFTWARE_DELIVERY;
49
- var LANE_DEFINITIONS_HINT = `${WORKSPACE_CONFIG_FILE_NAME} (${SOFTWARE_DELIVERY_KEY}.lanes.definitions)`;
50
- var NO_ITEMS_MARKER = "No items currently in progress";
51
- function extractParent(lane) {
52
- const trimmed = lane.trim();
53
- const colonIndex = trimmed.indexOf(":");
54
- if (colonIndex === -1) {
55
- return trimmed;
56
- }
57
- return trimmed.substring(0, colonIndex).trim();
58
- }
59
- function countChar(str, char) {
60
- let count = 0;
61
- for (const c of str) {
62
- if (c === char)
63
- count++;
64
- }
65
- return count;
66
- }
67
- var LANE_SEPARATOR = ":";
68
- var SPACE = " ";
69
- function validateColonFormat(lane, trimmed, colonIndex) {
70
- if (colonIndex > 0 && trimmed[colonIndex - 1] === SPACE) {
71
- throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" has space before colon. Expected format: "Parent: Subdomain" (space AFTER colon only)`, { lane });
72
- }
73
- if (colonIndex + 1 >= trimmed.length || trimmed[colonIndex + 1] !== SPACE) {
74
- throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" is missing space after colon. Expected format: "Parent: Subdomain"`, { lane });
75
- }
76
- }
77
- function validateSubLaneFormat(lane, trimmed, colonIndex, configPath) {
78
- validateColonFormat(lane, trimmed, colonIndex);
79
- const parent = trimmed.substring(0, colonIndex).trim();
80
- const subdomain = trimmed.substring(colonIndex + LANE_SEPARATOR.length + SPACE.length).trim();
81
- if (!isValidParentLane(parent, configPath)) {
82
- throw createError(ErrorCodes.INVALID_LANE, `Unknown parent lane: "${parent}". Check ${LANE_DEFINITIONS_HINT} for valid lanes.`, { parent, lane });
83
- }
84
- validateSubLaneInWorkspaceDefinitions(parent, subdomain, lane, configPath);
85
- return { valid: true, parent, error: null };
86
- }
87
- function validateSubLaneInWorkspaceDefinitions(parent, subdomain, lane, configPath) {
88
- const validSubLanes = getConfiguredSubLanesForParent(parent, configPath);
89
- if (validSubLanes.length === 0) {
90
- throw createError(ErrorCodes.INVALID_LANE, `Parent lane "${parent}" does not support sub-lanes in ${LANE_DEFINITIONS_HINT}. Use parent-only format "${parent}" or add "${parent}: <sublane>" to workspace.yaml.`, { parent, lane });
91
- }
92
- if (!validSubLanes.includes(subdomain)) {
93
- throw createError(ErrorCodes.INVALID_LANE, `Unknown sub-lane: "${subdomain}" for parent lane "${parent}".
94
-
95
- Valid sub-lanes in ${LANE_DEFINITIONS_HINT}: ${validSubLanes.join(", ")}`, { parent, subdomain, validSubLanes });
96
- }
97
- }
98
- function validateParentOnlyFormat(trimmed, configPath, strict) {
99
- if (!isValidParentLane(trimmed, configPath)) {
100
- throw createError(ErrorCodes.INVALID_LANE, `Unknown parent lane: "${trimmed}". Check ${LANE_DEFINITIONS_HINT} for valid lanes.`, { lane: trimmed });
101
- }
102
- const validSubLanes = getConfiguredSubLanesForParent(trimmed, configPath);
103
- if (validSubLanes.length > 0) {
104
- const message = `Parent-only lane "${trimmed}" blocked. Sub-lane required. Valid: ${validSubLanes.join(", ")}. Format: "${trimmed}: <sublane>"`;
105
- if (strict) {
106
- throw createError(ErrorCodes.INVALID_LANE, message, { lane: trimmed, validSubLanes });
107
- }
108
- console.warn(`${PREFIX} \u26A0\uFE0F ${message}`);
109
- }
110
- return { valid: true, parent: trimmed, error: null };
111
- }
112
- function validateLaneFormat(lane, configPath = null, options = {}) {
113
- const { strict = true } = options;
114
- const trimmed = lane.trim();
115
- const colonCount = countChar(trimmed, LANE_SEPARATOR);
116
- if (colonCount > 1) {
117
- throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" contains multiple colons. Expected format: "Parent: Subdomain" or "Parent"`, { lane });
118
- }
119
- const colonIndex = trimmed.indexOf(LANE_SEPARATOR);
120
- const isSubLaneFormat = colonIndex !== -1;
121
- if (isSubLaneFormat) {
122
- return validateSubLaneFormat(lane, trimmed, colonIndex, configPath);
123
- }
124
- return validateParentOnlyFormat(trimmed, configPath, strict);
125
- }
126
- function extractLanesForParentCheck(config) {
127
- const allLanes = [];
128
- const parentLanes = /* @__PURE__ */ new Set();
129
- if (!config.lanes) {
130
- return { allLanes, parentLanes };
131
- }
132
- if (Array.isArray(config.lanes)) {
133
- allLanes.push(...config.lanes.map((l) => l.name));
134
- return { allLanes, parentLanes };
135
- }
136
- if (config.lanes.definitions) {
137
- for (const lane of config.lanes.definitions) {
138
- allLanes.push(lane.name);
139
- const extracted = extractParent(lane.name);
140
- parentLanes.add(extracted.toLowerCase().trim());
141
- }
142
- }
143
- if (config.lanes.engineering) {
144
- allLanes.push(...config.lanes.engineering.map((l) => l.name));
145
- }
146
- if (config.lanes.business) {
147
- allLanes.push(...config.lanes.business.map((l) => l.name));
148
- }
149
- return { allLanes, parentLanes };
150
- }
151
- function resolveConfigPath(configPath) {
152
- if (configPath) {
153
- return configPath;
154
- }
155
- const projectRoot = findProjectRoot();
156
- return path.join(projectRoot, WORKSPACE_CONFIG_FILE_NAME);
157
- }
158
- function readConfigFromPath(configPath) {
159
- if (!existsSync(configPath)) {
160
- return null;
161
- }
162
- try {
163
- const configContent = readFileSync(configPath, { encoding: "utf-8" });
164
- const parsed = parseYAML(configContent);
165
- const workspace = asRecord(parsed);
166
- if (!workspace) {
167
- return null;
168
- }
169
- const softwareDelivery = asRecord(workspace[SOFTWARE_DELIVERY_KEY]);
170
- if (!softwareDelivery) {
171
- return null;
172
- }
173
- return softwareDelivery;
174
- } catch {
175
- return null;
176
- }
177
- }
178
- function readRuntimeConfig(projectRoot) {
179
- const workspacePath = path.join(projectRoot, WORKSPACE_CONFIG_FILE_NAME);
180
- if (!existsSync(workspacePath)) {
181
- return { kind: "missing", config: null };
182
- }
183
- try {
184
- const config = getConfig({
185
- projectRoot,
186
- reload: true,
187
- strictWorkspace: true
188
- });
189
- return { kind: "ok", config: { lanes: config.lanes } };
190
- } catch {
191
- return { kind: "invalid", config: null };
192
- }
193
- }
194
- function loadLanesConfig(configPath) {
195
- const resolvedConfigPath = resolveConfigPath(configPath);
196
- let config;
197
- if (configPath !== null) {
198
- config = readConfigFromPath(resolvedConfigPath);
199
- } else {
200
- const result = readRuntimeConfig(findProjectRoot());
201
- if (result.kind === "invalid") {
202
- throw createError(ErrorCodes.CONFIG_ERROR, `workspace.yaml exists but failed validation: ${resolvedConfigPath}
203
-
204
- The control_plane section may have an incompatible schema.
205
- Run: pnpm lumenflow:doctor to diagnose config issues.`, { path: resolvedConfigPath });
206
- }
207
- config = result.config;
208
- }
209
- if (!config) {
210
- throw createError(ErrorCodes.FILE_NOT_FOUND, `Config file not found: ${resolvedConfigPath}`, {
211
- path: resolvedConfigPath
212
- });
213
- }
214
- return { config, resolvedConfigPath };
215
- }
216
- function getConfiguredSubLanesForParent(parent, configPath = null) {
217
- const { config } = loadLanesConfig(configPath);
218
- const { allLanes } = extractLanesForParentCheck(config);
219
- const normalizedParent = parent.toLowerCase().trim();
220
- const subLanes = /* @__PURE__ */ new Set();
221
- for (const laneName of allLanes) {
222
- const trimmedLane = laneName.trim();
223
- const colonIndex = trimmedLane.indexOf(LANE_SEPARATOR);
224
- if (colonIndex === -1) {
225
- continue;
226
- }
227
- const laneParent = trimmedLane.substring(0, colonIndex).trim().toLowerCase();
228
- if (laneParent !== normalizedParent) {
229
- continue;
230
- }
231
- const subLane = trimmedLane.substring(colonIndex + 1).trim();
232
- if (subLane.length > 0) {
233
- subLanes.add(subLane);
234
- }
235
- }
236
- return [...subLanes];
237
- }
238
- function isValidParentLane(parent, configPath = null) {
239
- const { config } = loadLanesConfig(configPath);
240
- const { allLanes, parentLanes } = extractLanesForParentCheck(config);
241
- const normalizedParent = parent.toLowerCase().trim();
242
- if (parentLanes.size > 0) {
243
- return parentLanes.has(normalizedParent);
244
- }
245
- return allLanes.some((lane) => lane.toLowerCase().trim() === normalizedParent);
246
- }
247
- var DEFAULT_WIP_LIMIT = 1;
248
- function getWipLimitForLane(lane, options = {}) {
249
- const config = options.configPath ? readConfigFromPath(options.configPath) : readRuntimeConfig(findProjectRoot()).config;
250
- if (!config?.lanes) {
251
- return DEFAULT_WIP_LIMIT;
252
- }
253
- const normalizedLane = lane.toLowerCase().trim();
254
- const allLanes = extractAllLanesFromConfig(config);
255
- const matchingLane = allLanes.find((l) => l.name.toLowerCase().trim() === normalizedLane);
256
- return matchingLane?.wip_limit ?? DEFAULT_WIP_LIMIT;
257
- }
258
- var DEFAULT_LOCK_POLICY = "all";
259
- function getLockPolicyForLane(lane, options = {}) {
260
- const config = options.configPath ? readConfigFromPath(options.configPath) : readRuntimeConfig(findProjectRoot()).config;
261
- if (!config?.lanes) {
262
- return DEFAULT_LOCK_POLICY;
263
- }
264
- const normalizedLane = lane.toLowerCase().trim();
265
- const allLanes = extractAllLanesFromConfig(config);
266
- const matchingLane = allLanes.find((l) => l.name.toLowerCase().trim() === normalizedLane);
267
- const policy = matchingLane?.lock_policy;
268
- return policy === "all" || policy === "active" || policy === "none" ? policy : DEFAULT_LOCK_POLICY;
269
- }
270
- var SECTION_HEADING_PREFIX = "## ";
271
- function createEmptyLaneResult(wipLimit) {
272
- return {
273
- free: true,
274
- occupiedBy: null,
275
- error: null,
276
- inProgressWUs: [],
277
- wipLimit,
278
- currentCount: 0
279
- };
280
- }
281
- function extractInProgressSection(lines) {
282
- const inProgressIdx = lines.findIndex((l) => isInProgressHeader(l));
283
- if (inProgressIdx === -1) {
284
- return { section: "", error: 'Could not find "## In Progress" section in status.md' };
285
- }
286
- let endIdx = lines.slice(inProgressIdx + 1).findIndex((l) => l.startsWith(SECTION_HEADING_PREFIX));
287
- if (endIdx === -1) {
288
- endIdx = lines.length - inProgressIdx - 1;
289
- } else {
290
- endIdx = inProgressIdx + 1 + endIdx;
291
- }
292
- const section = lines.slice(inProgressIdx + 1, endIdx).join(STRING_LITERALS.NEWLINE);
293
- return { section, error: null };
294
- }
295
- var BLOCKED_HEADERS = ["## blocked", "## \u26D4 blocked"];
296
- function isBlockedHeader(line) {
297
- const normalized = line.trim().toLowerCase();
298
- return BLOCKED_HEADERS.some((header) => normalized === header || normalized.startsWith(header));
299
- }
300
- function extractBlockedSection(lines) {
301
- const blockedIdx = lines.findIndex((l) => isBlockedHeader(l));
302
- if (blockedIdx === -1) {
303
- return { section: "" };
304
- }
305
- let endIdx = lines.slice(blockedIdx + 1).findIndex((l) => l.startsWith(SECTION_HEADING_PREFIX));
306
- if (endIdx === -1) {
307
- endIdx = lines.length - blockedIdx - 1;
308
- } else {
309
- endIdx = blockedIdx + 1 + endIdx;
310
- }
311
- const section = lines.slice(blockedIdx + 1, endIdx).join(STRING_LITERALS.NEWLINE);
312
- return { section };
313
- }
314
- function checkWuLaneMatch(activeWuid, wuid, wuDir, targetLane, allowedStatuses) {
315
- if (activeWuid === wuid) {
316
- return null;
317
- }
318
- const wuPath = path.join(wuDir, `${activeWuid}.yaml`);
319
- if (!existsSync(wuPath)) {
320
- console.warn(`${PREFIX} Warning: ${activeWuid} referenced in status.md but ${wuPath} not found`);
321
- return null;
322
- }
323
- try {
324
- const wuContent = readFileSync(wuPath, { encoding: "utf-8" });
325
- const wuDoc = parseYAML(wuContent);
326
- if (!wuDoc || !wuDoc.lane) {
327
- console.warn(`${PREFIX} Warning: ${activeWuid} has no lane field`);
328
- return null;
329
- }
330
- const activeLane = wuDoc.lane.toString().trim().toLowerCase();
331
- const resolvedStatus = resolveWUStatus(wuDoc.status);
332
- if (activeLane === targetLane && allowedStatuses.has(resolvedStatus)) {
333
- return activeWuid;
334
- }
335
- } catch (e) {
336
- const errMessage = e instanceof Error ? e.message : String(e);
337
- console.warn(`${PREFIX} Warning: Failed to parse ${activeWuid} YAML: ${errMessage}`);
338
- }
339
- return null;
340
- }
341
- function collectInProgressWUsForLane(matches, wuid, wuDir, targetLane, allowedStatuses) {
342
- const inProgressWUs = [];
343
- for (const match of matches) {
344
- const activeWuid = match[1];
345
- if (!activeWuid) {
346
- continue;
347
- }
348
- const matchedWu = checkWuLaneMatch(activeWuid, wuid, wuDir, targetLane, allowedStatuses);
349
- if (matchedWu) {
350
- inProgressWUs.push(matchedWu);
351
- }
352
- }
353
- return inProgressWUs;
354
- }
355
- function extractWUsFromSection(section, wuid, wuDir, targetLane, allowedStatuses) {
356
- if (!section || section.includes(NO_ITEMS_MARKER)) {
357
- return [];
358
- }
359
- WU_LINK_PATTERN.lastIndex = 0;
360
- const matches = [...section.matchAll(WU_LINK_PATTERN)];
361
- if (matches.length === 0) {
362
- return [];
363
- }
364
- return collectInProgressWUsForLane(matches, wuid, wuDir, targetLane, allowedStatuses);
365
- }
366
- function checkLaneFree(statusPath, lane, wuid, options = {}) {
367
- try {
368
- const wipLimit = getWipLimitForLane(lane, { configPath: options.configPath });
369
- const lockPolicy = getLockPolicyForLane(lane, { configPath: options.configPath });
370
- if (lockPolicy === "none") {
371
- return createEmptyLaneResult(wipLimit);
372
- }
373
- if (!existsSync(statusPath)) {
374
- return { free: false, occupiedBy: null, error: `status.md not found: ${statusPath}` };
375
- }
376
- const content = readFileSync(statusPath, { encoding: "utf-8" });
377
- const lines = content.split(/\r?\n/);
378
- const { section: inProgressSection, error } = extractInProgressSection(lines);
379
- if (error) {
380
- return { free: false, occupiedBy: null, error };
381
- }
382
- const resolvedStatusPath = path.resolve(statusPath);
383
- const projectRoot = findProjectRoot(path.dirname(resolvedStatusPath));
384
- const wuDir = path.join(projectRoot, getConfig({ projectRoot }).directories.wuDir);
385
- const targetLane = lane.toString().trim().toLowerCase();
386
- const inProgressAllowedStatuses = lockPolicy === "all" ? ACTIVE_COUNTED_STATUSES_ALL : ACTIVE_COUNTED_STATUSES_PROGRESS_ONLY;
387
- const inProgressWUs = extractWUsFromSection(inProgressSection, wuid, wuDir, targetLane, inProgressAllowedStatuses);
388
- let blockedWUs = [];
389
- if (lockPolicy === "all") {
390
- const { section: blockedSection } = extractBlockedSection(lines);
391
- blockedWUs = extractWUsFromSection(blockedSection, wuid, wuDir, targetLane, ACTIVE_COUNTED_STATUSES_ALL);
392
- }
393
- const allCountedWUs = [...inProgressWUs, ...blockedWUs];
394
- const currentCount = allCountedWUs.length;
395
- const isFree = currentCount < wipLimit;
396
- return {
397
- free: isFree,
398
- occupiedBy: isFree ? null : allCountedWUs[0] || null,
399
- error: null,
400
- inProgressWUs: allCountedWUs,
401
- // Include all counted WUs for visibility
402
- wipLimit,
403
- currentCount
404
- };
405
- } catch (error) {
406
- const errMessage = error instanceof Error ? error.message : String(error);
407
- return { free: false, occupiedBy: null, error: `Unexpected error: ${errMessage}` };
408
- }
409
- }
410
- var NO_JUSTIFICATION_REQUIRED = {
411
- valid: true,
412
- warning: null,
413
- requiresJustification: false
414
- };
415
- function extractAllLanesFromConfig(config) {
416
- if (!config.lanes) {
417
- return [];
418
- }
419
- if (Array.isArray(config.lanes)) {
420
- return config.lanes;
421
- }
422
- const allLanes = [];
423
- if (config.lanes.definitions) {
424
- allLanes.push(...config.lanes.definitions);
425
- }
426
- if (config.lanes.engineering) {
427
- allLanes.push(...config.lanes.engineering);
428
- }
429
- if (config.lanes.business) {
430
- allLanes.push(...config.lanes.business);
431
- }
432
- return allLanes;
433
- }
434
- function checkWipJustification(lane, options = {}) {
435
- const config = options.configPath ? readConfigFromPath(options.configPath) : readRuntimeConfig(findProjectRoot()).config;
436
- if (!config) {
437
- return NO_JUSTIFICATION_REQUIRED;
438
- }
439
- const allLanes = extractAllLanesFromConfig(config);
440
- if (allLanes.length === 0) {
441
- return NO_JUSTIFICATION_REQUIRED;
442
- }
443
- const normalizedLane = lane.toLowerCase().trim();
444
- const matchingLane = allLanes.find((l) => l.name.toLowerCase().trim() === normalizedLane);
445
- if (!matchingLane) {
446
- return NO_JUSTIFICATION_REQUIRED;
447
- }
448
- const wipLimit = matchingLane.wip_limit ?? DEFAULT_WIP_LIMIT;
449
- if (wipLimit <= 1) {
450
- return NO_JUSTIFICATION_REQUIRED;
451
- }
452
- const justification = matchingLane.wip_justification;
453
- if (justification && justification.trim().length > 0) {
454
- return {
455
- valid: true,
456
- warning: null,
457
- requiresJustification: false,
458
- justification: justification.trim()
459
- };
460
- }
461
- const warning = `Lane "${lane}" has WIP limit of ${wipLimit} but no wip_justification. Philosophy: If you need WIP > 1, you need better lanes, not higher limits. Add wip_justification under ${LANE_DEFINITIONS_HINT} to suppress this warning.`;
462
- return {
463
- valid: true,
464
- warning,
465
- requiresJustification: true
466
- };
467
- }
468
-
469
- // ../core/dist/lane-lock.js
470
- var LOG_PREFIX = "[lane-lock]";
471
- var LOCKS_DIR = LUMENFLOW_PATHS.LOCKS_DIR;
472
- var DEFAULT_STALE_LOCK_THRESHOLD_HOURS = 2;
473
- function getStaleThresholdMs() {
474
- const envValue = process.env[ENV_VARS.STALE_LOCK_THRESHOLD_HOURS];
475
- if (envValue) {
476
- const hours = parseFloat(envValue);
477
- if (!Number.isNaN(hours) && hours > 0) {
478
- return hours * 60 * 60 * 1e3;
479
- }
480
- }
481
- return DEFAULT_STALE_LOCK_THRESHOLD_HOURS * 60 * 60 * 1e3;
482
- }
483
- function getLocksDir(baseDir = null) {
484
- const projectRoot = baseDir || getProjectRoot(import.meta.url);
485
- return path2.join(projectRoot, LOCKS_DIR);
486
- }
487
- function getLockFilePath(lane, baseDir = null) {
488
- const laneKebab = toKebab(lane);
489
- const locksDir = getLocksDir(baseDir);
490
- return path2.join(locksDir, `${laneKebab}.lock`);
491
- }
492
- function ensureLocksDir(baseDir = null) {
493
- const locksDir = getLocksDir(baseDir);
494
- if (!existsSync2(locksDir)) {
495
- mkdirSync(locksDir, { recursive: true });
496
- }
497
- }
498
- function isLockStale(metadata) {
499
- if (!metadata || !metadata.timestamp) {
500
- return true;
501
- }
502
- const lockTime = new Date(metadata.timestamp).getTime();
503
- const now = Date.now();
504
- return now - lockTime > getStaleThresholdMs();
505
- }
506
- function isZombieLock(metadata) {
507
- if (!metadata || typeof metadata.pid !== "number") {
508
- return true;
509
- }
510
- try {
511
- process.kill(metadata.pid, 0);
512
- return false;
513
- } catch {
514
- return true;
515
- }
516
- }
517
- function readLockMetadata(lockPath) {
518
- try {
519
- if (!existsSync2(lockPath)) {
520
- return null;
521
- }
522
- const content = readFileSync2(lockPath, { encoding: "utf-8" });
523
- return JSON.parse(content);
524
- } catch {
525
- return null;
526
- }
527
- }
528
- function acquireLaneLock(lane, wuId, options = {}) {
529
- const { agentSession = null, baseDir = null } = options;
530
- const lockPolicy = getLockPolicyForLane(lane);
531
- if (lockPolicy === "none") {
532
- console.log(`${LOG_PREFIX} Skipping lock acquisition for "${lane}" (lock_policy=none)`);
533
- return {
534
- acquired: true,
535
- error: null,
536
- existingLock: null,
537
- isStale: false,
538
- skipped: true
539
- };
540
- }
541
- try {
542
- ensureLocksDir(baseDir);
543
- const lockPath = getLockFilePath(lane, baseDir);
544
- const metadata = {
545
- wuId,
546
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
547
- agentSession,
548
- pid: process.pid,
549
- lane
550
- };
551
- try {
552
- const fd = openSync(lockPath, "wx");
553
- writeFileSync(lockPath, JSON.stringify(metadata, null, 2), { encoding: "utf-8" });
554
- closeSync(fd);
555
- console.log(`${LOG_PREFIX} Acquired lane lock for "${lane}" (${wuId})`);
556
- return {
557
- acquired: true,
558
- error: null,
559
- existingLock: null,
560
- isStale: false
561
- };
562
- } catch (err) {
563
- if (err.code === "EEXIST") {
564
- const existingLock = readLockMetadata(lockPath);
565
- const stale = existingLock ? isLockStale(existingLock) : true;
566
- const zombie = existingLock ? isZombieLock(existingLock) : true;
567
- if (existingLock && existingLock.wuId === wuId) {
568
- console.log(`${LOG_PREFIX} Lock already held by same WU (${wuId})`);
569
- return {
570
- acquired: true,
571
- // Allow re-claim of same WU
572
- error: null,
573
- existingLock,
574
- isStale: stale
575
- };
576
- }
577
- if (zombie && stale) {
578
- console.warn(`${LOG_PREFIX} Detected stale zombie lock for "${lane}" (PID ${existingLock?.pid} not running, lock age > threshold)`);
579
- console.warn(`${LOG_PREFIX} Previous owner: ${existingLock?.wuId}`);
580
- console.warn(`${LOG_PREFIX} Lock timestamp: ${existingLock?.timestamp}`);
581
- console.warn(`${LOG_PREFIX} Auto-clearing stale zombie lock...`);
582
- try {
583
- unlinkSync(lockPath);
584
- } catch {
585
- }
586
- return acquireLaneLock(lane, wuId, options);
587
- }
588
- return {
589
- acquired: false,
590
- error: existingLock ? `Lane "${lane}" is locked by ${existingLock.wuId} (since ${existingLock.timestamp})` : `Lane "${lane}" has an invalid lock file`,
591
- existingLock,
592
- isStale: stale
593
- };
594
- }
595
- throw err;
596
- }
597
- } catch (err) {
598
- const errMessage = err instanceof Error ? err.message : String(err);
599
- return {
600
- acquired: false,
601
- error: `Failed to acquire lane lock: ${errMessage}`,
602
- existingLock: null,
603
- isStale: false
604
- };
605
- }
606
- }
607
- function releaseLaneLock(lane, options = {}) {
608
- const { wuId = null, baseDir = null, force = false } = options;
609
- try {
610
- const lockPath = getLockFilePath(lane, baseDir);
611
- if (!existsSync2(lockPath)) {
612
- return {
613
- released: true,
614
- error: null,
615
- notFound: true
616
- };
617
- }
618
- if (wuId && !force) {
619
- const existingLock = readLockMetadata(lockPath);
620
- if (existingLock && existingLock.wuId !== wuId) {
621
- return {
622
- released: false,
623
- error: `Cannot release lock: owned by ${existingLock.wuId}, not ${wuId}`,
624
- notFound: false
625
- };
626
- }
627
- }
628
- unlinkSync(lockPath);
629
- console.log(`${LOG_PREFIX} Released lane lock for "${lane}"`);
630
- return {
631
- released: true,
632
- error: null,
633
- notFound: false
634
- };
635
- } catch (err) {
636
- const errMessage = err instanceof Error ? err.message : String(err);
637
- return {
638
- released: false,
639
- error: `Failed to release lane lock: ${errMessage}`,
640
- notFound: false
641
- };
642
- }
643
- }
644
- function checkLaneLock(lane, options = {}) {
645
- const { baseDir = null } = options;
646
- const lockPath = getLockFilePath(lane, baseDir);
647
- const metadata = readLockMetadata(lockPath);
648
- if (!metadata) {
649
- return {
650
- locked: false,
651
- metadata: null,
652
- isStale: false
653
- };
654
- }
655
- return {
656
- locked: true,
657
- metadata,
658
- isStale: isLockStale(metadata)
659
- };
660
- }
661
- function forceRemoveStaleLock(lane, options = {}) {
662
- const { baseDir = null } = options;
663
- const lockPath = getLockFilePath(lane, baseDir);
664
- const existingLock = readLockMetadata(lockPath);
665
- if (!existingLock) {
666
- return {
667
- released: true,
668
- error: null,
669
- notFound: true
670
- };
671
- }
672
- if (!isLockStale(existingLock)) {
673
- return {
674
- released: false,
675
- error: `Cannot force-remove: lock is not stale (${existingLock.wuId} since ${existingLock.timestamp})`,
676
- notFound: false
677
- };
678
- }
679
- console.warn(`${LOG_PREFIX} \u26A0\uFE0F Force-removing stale lock for "${lane}"`);
680
- console.warn(`${LOG_PREFIX} Previous owner: ${existingLock.wuId}`);
681
- console.warn(`${LOG_PREFIX} Lock timestamp: ${existingLock.timestamp}`);
682
- return releaseLaneLock(lane, { baseDir, force: true });
683
- }
684
- function getAllLaneLocks(options = {}) {
685
- const { baseDir = null } = options;
686
- const locksDir = getLocksDir(baseDir);
687
- const locks = /* @__PURE__ */ new Map();
688
- if (!existsSync2(locksDir)) {
689
- return locks;
690
- }
691
- try {
692
- const files = __require("fs").readdirSync(locksDir);
693
- for (const file of files) {
694
- if (!file.endsWith(".lock"))
695
- continue;
696
- const lockPath = path2.join(locksDir, file);
697
- const metadata = readLockMetadata(lockPath);
698
- if (metadata && metadata.lane) {
699
- locks.set(metadata.lane, metadata);
700
- }
701
- }
702
- } catch {
703
- }
704
- return locks;
705
- }
706
- function auditedUnlock(lane, options) {
707
- const { reason, baseDir = null, force = false } = options;
708
- if (!reason) {
709
- return {
710
- released: false,
711
- error: 'Reason is required for audited unlock. Use --reason "<text>"',
712
- notFound: false
713
- };
714
- }
715
- const lockPath = getLockFilePath(lane, baseDir);
716
- const existingLock = readLockMetadata(lockPath);
717
- if (!existingLock) {
718
- return {
719
- released: true,
720
- error: null,
721
- notFound: true,
722
- reason
723
- };
724
- }
725
- const stale = isLockStale(existingLock);
726
- const zombie = isZombieLock(existingLock);
727
- const safeToRemove = stale || zombie;
728
- if (!safeToRemove && !force) {
729
- return {
730
- released: false,
731
- error: `Cannot unlock active lock for "${lane}" (${existingLock.wuId}).
732
- Lock is recent (${existingLock.timestamp}) and PID ${existingLock.pid} is running.
733
- Use --force to override (emergency only).`,
734
- notFound: false,
735
- previousLock: existingLock
736
- };
737
- }
738
- const unlockType = force ? "FORCED" : zombie ? "ZOMBIE" : "STALE";
739
- console.log(`${LOG_PREFIX} Audited unlock (${unlockType}) for "${lane}"`);
740
- console.log(`${LOG_PREFIX} Previous owner: ${existingLock.wuId}`);
741
- console.log(`${LOG_PREFIX} Lock timestamp: ${existingLock.timestamp}`);
742
- console.log(`${LOG_PREFIX} Lock PID: ${existingLock.pid}`);
743
- console.log(`${LOG_PREFIX} Reason: ${reason}`);
744
- if (force && !safeToRemove) {
745
- console.warn(`${LOG_PREFIX} \u26A0\uFE0F WARNING: Forced unlock of active lock!`);
746
- }
747
- const releaseResult = releaseLaneLock(lane, { baseDir, force: true });
748
- return {
749
- ...releaseResult,
750
- reason,
751
- forced: force && !safeToRemove,
752
- previousLock: existingLock
753
- };
754
- }
755
-
756
- export {
757
- extractParent,
758
- validateLaneFormat,
759
- getWipLimitForLane,
760
- getLockPolicyForLane,
761
- checkLaneFree,
762
- checkWipJustification,
763
- getStaleThresholdMs,
764
- getLocksDir,
765
- getLockFilePath,
766
- isLockStale,
767
- isZombieLock,
768
- readLockMetadata,
769
- acquireLaneLock,
770
- releaseLaneLock,
771
- checkLaneLock,
772
- forceRemoveStaleLock,
773
- getAllLaneLocks,
774
- auditedUnlock
775
- };