@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.
- package/dist/wu-claim.js +2 -1
- package/dist/wu-claim.js.map +1 -1
- package/dist/wu-done-policies.js +9 -9
- package/dist/wu-done-policies.js.map +1 -1
- package/dist/wu-spawn-strategy-resolver.js +14 -6
- package/dist/wu-spawn-strategy-resolver.js.map +1 -1
- package/package.json +8 -8
- package/packs/sidekick/.turbo/turbo-build.log +1 -1
- package/packs/sidekick/package.json +1 -1
- package/packs/software-delivery/.turbo/turbo-build.log +1 -1
- package/packs/software-delivery/package.json +1 -1
- package/dist/chunk-2D2VOCA4.js +0 -37
- package/dist/chunk-2D5KFYGX.js +0 -284
- package/dist/chunk-2GXVIN57.js +0 -14072
- package/dist/chunk-2MQ7HZWZ.js +0 -26
- package/dist/chunk-2UFQ3A3C.js +0 -643
- package/dist/chunk-3RG5ZIWI.js +0 -10
- package/dist/chunk-4N74J3UT.js +0 -15
- package/dist/chunk-5GTOXFYR.js +0 -392
- package/dist/chunk-5VY6MQMC.js +0 -240
- package/dist/chunk-67XVPMRY.js +0 -1297
- package/dist/chunk-6HO4GWJE.js +0 -164
- package/dist/chunk-6W5XHWYV.js +0 -1890
- package/dist/chunk-6X4EMYJQ.js +0 -64
- package/dist/chunk-6XYXI2NQ.js +0 -772
- package/dist/chunk-7ANSOV6Q.js +0 -285
- package/dist/chunk-A624LFLB.js +0 -1380
- package/dist/chunk-ADN5NHG4.js +0 -126
- package/dist/chunk-B7YJYJKG.js +0 -33
- package/dist/chunk-CCLHCPKG.js +0 -210
- package/dist/chunk-CK36VROC.js +0 -1584
- package/dist/chunk-D3UOFRSB.js +0 -81
- package/dist/chunk-DFR4DJBM.js +0 -230
- package/dist/chunk-DSYBDHYH.js +0 -79
- package/dist/chunk-DWMLTXKQ.js +0 -1176
- package/dist/chunk-E3REJTAJ.js +0 -28
- package/dist/chunk-EA3IVO64.js +0 -633
- package/dist/chunk-EK2AKZKD.js +0 -55
- package/dist/chunk-ELD7JTTT.js +0 -343
- package/dist/chunk-EX6TT2XI.js +0 -195
- package/dist/chunk-EXINSFZE.js +0 -82
- package/dist/chunk-EZ6ZBYBM.js +0 -510
- package/dist/chunk-FBKAPTJ2.js +0 -16
- package/dist/chunk-FVLV5RYH.js +0 -1118
- package/dist/chunk-GDNSBQVK.js +0 -2485
- package/dist/chunk-GPQHMBNN.js +0 -278
- package/dist/chunk-GTFJB67L.js +0 -68
- package/dist/chunk-HANJXVKW.js +0 -1127
- package/dist/chunk-HEVS5YLD.js +0 -269
- package/dist/chunk-HMEVZKPQ.js +0 -9
- package/dist/chunk-HRGSYNLM.js +0 -3511
- package/dist/chunk-ISZR5N4K.js +0 -60
- package/dist/chunk-J6SUPR2C.js +0 -226
- package/dist/chunk-JERYVEIZ.js +0 -244
- package/dist/chunk-JHHWGL2N.js +0 -87
- package/dist/chunk-JONWQUB5.js +0 -775
- package/dist/chunk-K2DIWWDM.js +0 -1766
- package/dist/chunk-KY4PGL5V.js +0 -969
- package/dist/chunk-L737LQ4C.js +0 -1285
- package/dist/chunk-LFTWYIB2.js +0 -497
- package/dist/chunk-LV47RFNJ.js +0 -41
- package/dist/chunk-MKSAITI7.js +0 -15
- package/dist/chunk-MZ7RKIX4.js +0 -212
- package/dist/chunk-NAP6CFSO.js +0 -84
- package/dist/chunk-ND6MY37M.js +0 -16
- package/dist/chunk-NMG736UR.js +0 -683
- package/dist/chunk-NRAXROED.js +0 -32
- package/dist/chunk-NRIZR3A7.js +0 -690
- package/dist/chunk-NX43BG3M.js +0 -233
- package/dist/chunk-O645XLSI.js +0 -297
- package/dist/chunk-OMJD6A3S.js +0 -235
- package/dist/chunk-QB6SJD4T.js +0 -430
- package/dist/chunk-QFSTL4J3.js +0 -276
- package/dist/chunk-QLGDFMFX.js +0 -212
- package/dist/chunk-RIAAGL2E.js +0 -13
- package/dist/chunk-RWO5XMZ6.js +0 -86
- package/dist/chunk-RXRKBBSM.js +0 -149
- package/dist/chunk-RZOZMML6.js +0 -363
- package/dist/chunk-U7I7FS7T.js +0 -113
- package/dist/chunk-UI42RODY.js +0 -717
- package/dist/chunk-UTVMVSCO.js +0 -519
- package/dist/chunk-V6OJGLBA.js +0 -1746
- package/dist/chunk-W2JHVH7D.js +0 -152
- package/dist/chunk-WD3Y7VQN.js +0 -280
- package/dist/chunk-WOCTQ5MS.js +0 -303
- package/dist/chunk-WZR3ZUNN.js +0 -696
- package/dist/chunk-XGI665H7.js +0 -150
- package/dist/chunk-XKY65P2T.js +0 -304
- package/dist/chunk-Y4CQZY65.js +0 -57
- package/dist/chunk-YFEXKLVE.js +0 -194
- package/dist/chunk-YHO3HS5X.js +0 -287
- package/dist/chunk-YLS7AZSX.js +0 -738
- package/dist/chunk-ZE473AO6.js +0 -49
- package/dist/chunk-ZF747T3O.js +0 -644
- package/dist/chunk-ZHCZHZH3.js +0 -43
- package/dist/chunk-ZZNZX2XY.js +0 -87
- package/dist/constants-7QAP3VQ4.js +0 -23
- package/dist/dist-IY3UUMWK.js +0 -33
- package/dist/invariants-runner-W5RGHCSU.js +0 -27
- package/dist/lane-lock-6J36HD5O.js +0 -35
- package/dist/mem-checkpoint-core-EANG2GVN.js +0 -14
- package/dist/mem-signal-core-2LZ2WYHW.js +0 -19
- package/dist/memory-store-OLB5FO7K.js +0 -18
- package/dist/service-6BYCOCO5.js +0 -13
- package/dist/spawn-policy-resolver-NTSZYQ6R.js +0 -17
- package/dist/spawn-task-builder-R4E2BHSW.js +0 -22
- package/dist/wu-done-pr-WLFFFEPJ.js +0 -25
- package/dist/wu-done-validation-3J5E36FE.js +0 -30
- package/dist/wu-duplicate-id-detector-5S7JHELK.js +0 -232
package/dist/chunk-JONWQUB5.js
DELETED
|
@@ -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
|
-
};
|