@lumenflow/cli 3.12.4 → 3.12.6
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/chunk-2D2VOCA4.js +37 -0
- package/dist/chunk-2D5KFYGX.js +284 -0
- package/dist/chunk-2GXVIN57.js +14072 -0
- package/dist/chunk-2MQ7HZWZ.js +26 -0
- package/dist/chunk-2UFQ3A3C.js +643 -0
- package/dist/chunk-3RG5ZIWI.js +10 -0
- package/dist/chunk-4N74J3UT.js +15 -0
- package/dist/chunk-5GTOXFYR.js +392 -0
- package/dist/chunk-5VY6MQMC.js +240 -0
- package/dist/chunk-67XVPMRY.js +1297 -0
- package/dist/chunk-6HO4GWJE.js +164 -0
- package/dist/chunk-6W5XHWYV.js +1890 -0
- package/dist/chunk-6X4EMYJQ.js +64 -0
- package/dist/chunk-6XYXI2NQ.js +772 -0
- package/dist/chunk-7ANSOV6Q.js +285 -0
- package/dist/chunk-A624LFLB.js +1380 -0
- package/dist/chunk-ADN5NHG4.js +126 -0
- package/dist/chunk-B7YJYJKG.js +33 -0
- package/dist/chunk-CCLHCPKG.js +210 -0
- package/dist/chunk-CK36VROC.js +1584 -0
- package/dist/chunk-D3UOFRSB.js +81 -0
- package/dist/chunk-DFR4DJBM.js +230 -0
- package/dist/chunk-DSYBDHYH.js +79 -0
- package/dist/chunk-DWMLTXKQ.js +1176 -0
- package/dist/chunk-E3REJTAJ.js +28 -0
- package/dist/chunk-EA3IVO64.js +633 -0
- package/dist/chunk-EK2AKZKD.js +55 -0
- package/dist/chunk-ELD7JTTT.js +343 -0
- package/dist/chunk-EX6TT2XI.js +195 -0
- package/dist/chunk-EXINSFZE.js +82 -0
- package/dist/chunk-EZ6ZBYBM.js +510 -0
- package/dist/chunk-FBKAPTJ2.js +16 -0
- package/dist/chunk-FVLV5RYH.js +1118 -0
- package/dist/chunk-GDNSBQVK.js +2485 -0
- package/dist/chunk-GPQHMBNN.js +278 -0
- package/dist/chunk-GTFJB67L.js +68 -0
- package/dist/chunk-HANJXVKW.js +1127 -0
- package/dist/chunk-HEVS5YLD.js +269 -0
- package/dist/chunk-HMEVZKPQ.js +9 -0
- package/dist/chunk-HRGSYNLM.js +3511 -0
- package/dist/chunk-ISZR5N4K.js +60 -0
- package/dist/chunk-J6SUPR2C.js +226 -0
- package/dist/chunk-JERYVEIZ.js +244 -0
- package/dist/chunk-JHHWGL2N.js +87 -0
- package/dist/chunk-JONWQUB5.js +775 -0
- package/dist/chunk-K2DIWWDM.js +1766 -0
- package/dist/chunk-KY4PGL5V.js +969 -0
- package/dist/chunk-L737LQ4C.js +1285 -0
- package/dist/chunk-LFTWYIB2.js +497 -0
- package/dist/chunk-LV47RFNJ.js +41 -0
- package/dist/chunk-MKSAITI7.js +15 -0
- package/dist/chunk-MZ7RKIX4.js +212 -0
- package/dist/chunk-NAP6CFSO.js +84 -0
- package/dist/chunk-ND6MY37M.js +16 -0
- package/dist/chunk-NMG736UR.js +683 -0
- package/dist/chunk-NRAXROED.js +32 -0
- package/dist/chunk-NRIZR3A7.js +690 -0
- package/dist/chunk-NX43BG3M.js +233 -0
- package/dist/chunk-O645XLSI.js +297 -0
- package/dist/chunk-OMJD6A3S.js +235 -0
- package/dist/chunk-QB6SJD4T.js +430 -0
- package/dist/chunk-QFSTL4J3.js +276 -0
- package/dist/chunk-QLGDFMFX.js +212 -0
- package/dist/chunk-RIAAGL2E.js +13 -0
- package/dist/chunk-RWO5XMZ6.js +86 -0
- package/dist/chunk-RXRKBBSM.js +149 -0
- package/dist/chunk-RZOZMML6.js +363 -0
- package/dist/chunk-U7I7FS7T.js +113 -0
- package/dist/chunk-UI42RODY.js +717 -0
- package/dist/chunk-UTVMVSCO.js +519 -0
- package/dist/chunk-V6OJGLBA.js +1746 -0
- package/dist/chunk-W2JHVH7D.js +152 -0
- package/dist/chunk-WD3Y7VQN.js +280 -0
- package/dist/chunk-WOCTQ5MS.js +303 -0
- package/dist/chunk-WZR3ZUNN.js +696 -0
- package/dist/chunk-XGI665H7.js +150 -0
- package/dist/chunk-XKY65P2T.js +304 -0
- package/dist/chunk-Y4CQZY65.js +57 -0
- package/dist/chunk-YFEXKLVE.js +194 -0
- package/dist/chunk-YHO3HS5X.js +287 -0
- package/dist/chunk-YLS7AZSX.js +738 -0
- package/dist/chunk-ZE473AO6.js +49 -0
- package/dist/chunk-ZF747T3O.js +644 -0
- package/dist/chunk-ZHCZHZH3.js +43 -0
- package/dist/chunk-ZZNZX2XY.js +87 -0
- package/dist/constants-7QAP3VQ4.js +23 -0
- package/dist/dist-IY3UUMWK.js +33 -0
- package/dist/init-templates.js +9 -9
- package/dist/invariants-runner-W5RGHCSU.js +27 -0
- package/dist/lane-lock-6J36HD5O.js +35 -0
- package/dist/mem-checkpoint-core-EANG2GVN.js +14 -0
- package/dist/mem-signal-core-2LZ2WYHW.js +19 -0
- package/dist/memory-store-OLB5FO7K.js +18 -0
- package/dist/service-6BYCOCO5.js +13 -0
- package/dist/spawn-policy-resolver-NTSZYQ6R.js +17 -0
- package/dist/spawn-task-builder-R4E2BHSW.js +22 -0
- package/dist/wu-claim.js +2 -2
- package/dist/wu-claim.js.map +1 -1
- package/dist/wu-done-already-merged.js +12 -5
- package/dist/wu-done-already-merged.js.map +1 -1
- package/dist/wu-done-gates.js +25 -4
- package/dist/wu-done-gates.js.map +1 -1
- package/dist/wu-done-ownership.js +6 -1
- package/dist/wu-done-ownership.js.map +1 -1
- package/dist/wu-done-pr-WLFFFEPJ.js +25 -0
- package/dist/wu-done-validation-3J5E36FE.js +30 -0
- package/dist/wu-done.js +6 -6
- package/dist/wu-done.js.map +1 -1
- package/dist/wu-duplicate-id-detector-5S7JHELK.js +232 -0
- package/dist/wu-edit-operations.js +58 -17
- package/dist/wu-edit-operations.js.map +1 -1
- package/dist/wu-edit-validators.js +104 -28
- package/dist/wu-edit-validators.js.map +1 -1
- package/dist/wu-edit.js +1 -1
- package/dist/wu-edit.js.map +1 -1
- package/dist/wu-spawn-prompt-builders.js +8 -7
- package/dist/wu-spawn-prompt-builders.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/templates/core/LUMENFLOW.md.template +1 -1
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +3 -3
- package/templates/core/ai/onboarding/starting-prompt.md.template +11 -11
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LUMENFLOW_MEMORY_PATHS
|
|
3
|
+
} from "./chunk-4N74J3UT.js";
|
|
4
|
+
import {
|
|
5
|
+
ErrorCodes,
|
|
6
|
+
createError
|
|
7
|
+
} from "./chunk-RXRKBBSM.js";
|
|
8
|
+
|
|
9
|
+
// ../memory/dist/mem-signal-core.js
|
|
10
|
+
import { randomBytes } from "crypto";
|
|
11
|
+
import fs from "fs/promises";
|
|
12
|
+
import path from "path";
|
|
13
|
+
var SIGNAL_FILE_NAME = "signals.jsonl";
|
|
14
|
+
var SIGNAL_RECEIPTS_FILE_NAME = "signal-receipts.jsonl";
|
|
15
|
+
var WU_ID_PATTERN = /^WU-\d+$/;
|
|
16
|
+
var SIGNAL_ID_PREFIX = "sig-";
|
|
17
|
+
var SIGNAL_ID_LENGTH = 8;
|
|
18
|
+
var ERROR_MESSAGES = {
|
|
19
|
+
MESSAGE_REQUIRED: "message is required and cannot be empty",
|
|
20
|
+
INVALID_WU_ID: "Invalid WU ID format. Expected WU-XXX (e.g., WU-1473)",
|
|
21
|
+
INVALID_SIGNAL_METADATA: "Signal metadata fields must be non-empty strings when provided",
|
|
22
|
+
LEGACY_SIGNAL_RECORD: "Legacy signal record is not supported by the strict signal contract. Migrate records before loading signals.",
|
|
23
|
+
INVALID_SIGNAL_RECORD: "Invalid signal record in signals.jsonl"
|
|
24
|
+
};
|
|
25
|
+
var DEFAULT_SIGNAL_TYPE = "coordination";
|
|
26
|
+
var DEFAULT_SIGNAL_SENDER = "system";
|
|
27
|
+
var DEFAULT_SIGNAL_ORIGIN = "local";
|
|
28
|
+
function normalizeOptionalString(value, fieldName) {
|
|
29
|
+
if (value === void 0) {
|
|
30
|
+
return void 0;
|
|
31
|
+
}
|
|
32
|
+
const normalized = value.trim();
|
|
33
|
+
if (normalized.length === 0) {
|
|
34
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, `${ERROR_MESSAGES.INVALID_SIGNAL_METADATA}: ${fieldName}`);
|
|
35
|
+
}
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
38
|
+
function isNonEmptyString(value) {
|
|
39
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
40
|
+
}
|
|
41
|
+
function parseStrictSignalRecord(line, lineNumber) {
|
|
42
|
+
let parsed;
|
|
43
|
+
try {
|
|
44
|
+
parsed = JSON.parse(line);
|
|
45
|
+
} catch {
|
|
46
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, `${ERROR_MESSAGES.INVALID_SIGNAL_RECORD} at line ${lineNumber}`);
|
|
47
|
+
}
|
|
48
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
49
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, `${ERROR_MESSAGES.INVALID_SIGNAL_RECORD} at line ${lineNumber}`);
|
|
50
|
+
}
|
|
51
|
+
const candidate = parsed;
|
|
52
|
+
if (!isNonEmptyString(candidate.id) || !isNonEmptyString(candidate.message) || !isNonEmptyString(candidate.created_at) || typeof candidate.read !== "boolean") {
|
|
53
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, `${ERROR_MESSAGES.INVALID_SIGNAL_RECORD} at line ${lineNumber}`);
|
|
54
|
+
}
|
|
55
|
+
if (!isNonEmptyString(candidate.type) || !isNonEmptyString(candidate.sender) || !isNonEmptyString(candidate.origin) || !isNonEmptyString(candidate.remote_id)) {
|
|
56
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, `${ERROR_MESSAGES.LEGACY_SIGNAL_RECORD} (line ${lineNumber})`);
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
...candidate,
|
|
60
|
+
id: candidate.id,
|
|
61
|
+
message: candidate.message,
|
|
62
|
+
created_at: candidate.created_at,
|
|
63
|
+
read: candidate.read,
|
|
64
|
+
type: candidate.type,
|
|
65
|
+
sender: candidate.sender,
|
|
66
|
+
origin: candidate.origin,
|
|
67
|
+
remote_id: candidate.remote_id
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function generateSignalId() {
|
|
71
|
+
const bytes = randomBytes(4);
|
|
72
|
+
const hex = bytes.toString("hex").slice(0, SIGNAL_ID_LENGTH);
|
|
73
|
+
return `${SIGNAL_ID_PREFIX}${hex}`;
|
|
74
|
+
}
|
|
75
|
+
function getMemoryDir(baseDir) {
|
|
76
|
+
return path.join(baseDir, LUMENFLOW_MEMORY_PATHS.MEMORY_DIR);
|
|
77
|
+
}
|
|
78
|
+
function getSignalsPath(baseDir) {
|
|
79
|
+
return path.join(getMemoryDir(baseDir), SIGNAL_FILE_NAME);
|
|
80
|
+
}
|
|
81
|
+
function getReceiptsPath(baseDir) {
|
|
82
|
+
return path.join(getMemoryDir(baseDir), SIGNAL_RECEIPTS_FILE_NAME);
|
|
83
|
+
}
|
|
84
|
+
async function loadReceiptIds(baseDir) {
|
|
85
|
+
const receiptsPath = getReceiptsPath(baseDir);
|
|
86
|
+
let content;
|
|
87
|
+
try {
|
|
88
|
+
content = await fs.readFile(receiptsPath, { encoding: "utf-8" });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
const error = err;
|
|
91
|
+
if (error.code === "ENOENT") {
|
|
92
|
+
return /* @__PURE__ */ new Set();
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
97
|
+
const ids = /* @__PURE__ */ new Set();
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
const receipt = JSON.parse(line);
|
|
100
|
+
ids.add(receipt.signal_id);
|
|
101
|
+
}
|
|
102
|
+
return ids;
|
|
103
|
+
}
|
|
104
|
+
function isValidWuId(wuId) {
|
|
105
|
+
return WU_ID_PATTERN.test(wuId);
|
|
106
|
+
}
|
|
107
|
+
async function createSignal(baseDir, options) {
|
|
108
|
+
const { message, wuId, lane, type, sender, target_agent, origin, remote_id } = options;
|
|
109
|
+
if (!message || typeof message !== "string" || message.trim().length === 0) {
|
|
110
|
+
throw createError(ErrorCodes.VALIDATION_ERROR, ERROR_MESSAGES.MESSAGE_REQUIRED);
|
|
111
|
+
}
|
|
112
|
+
if (wuId !== void 0 && !isValidWuId(wuId)) {
|
|
113
|
+
throw createError(ErrorCodes.INVALID_WU_ID, ERROR_MESSAGES.INVALID_WU_ID);
|
|
114
|
+
}
|
|
115
|
+
const normalizedType = normalizeOptionalString(type, "type") ?? DEFAULT_SIGNAL_TYPE;
|
|
116
|
+
const normalizedSender = normalizeOptionalString(sender, "sender") ?? DEFAULT_SIGNAL_SENDER;
|
|
117
|
+
const normalizedTargetAgent = normalizeOptionalString(target_agent, "target_agent");
|
|
118
|
+
const normalizedOrigin = normalizeOptionalString(origin, "origin") ?? DEFAULT_SIGNAL_ORIGIN;
|
|
119
|
+
const normalizedRemoteId = normalizeOptionalString(remote_id, "remote_id");
|
|
120
|
+
const signal = {
|
|
121
|
+
id: generateSignalId(),
|
|
122
|
+
message: message.trim(),
|
|
123
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
124
|
+
read: false,
|
|
125
|
+
type: normalizedType,
|
|
126
|
+
sender: normalizedSender,
|
|
127
|
+
origin: normalizedOrigin,
|
|
128
|
+
remote_id: ""
|
|
129
|
+
};
|
|
130
|
+
signal.remote_id = normalizedRemoteId ?? signal.id;
|
|
131
|
+
if (wuId) {
|
|
132
|
+
signal.wu_id = wuId;
|
|
133
|
+
}
|
|
134
|
+
if (lane) {
|
|
135
|
+
signal.lane = lane;
|
|
136
|
+
}
|
|
137
|
+
if (normalizedTargetAgent) {
|
|
138
|
+
signal.target_agent = normalizedTargetAgent;
|
|
139
|
+
}
|
|
140
|
+
const memoryDir = getMemoryDir(baseDir);
|
|
141
|
+
await fs.mkdir(memoryDir, { recursive: true });
|
|
142
|
+
const signalsPath = getSignalsPath(baseDir);
|
|
143
|
+
const line = `${JSON.stringify(signal)}
|
|
144
|
+
`;
|
|
145
|
+
await fs.appendFile(signalsPath, line, "utf-8");
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
signal
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
async function loadSignals(baseDir, options = {}) {
|
|
152
|
+
const { wuId, lane, unreadOnly, since } = options;
|
|
153
|
+
const signalsPath = getSignalsPath(baseDir);
|
|
154
|
+
let content;
|
|
155
|
+
try {
|
|
156
|
+
content = await fs.readFile(signalsPath, { encoding: "utf-8" });
|
|
157
|
+
} catch (err) {
|
|
158
|
+
const error = err;
|
|
159
|
+
if (error.code === "ENOENT") {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
const lines = content.split("\n").filter((line) => line.trim());
|
|
165
|
+
const signals = lines.map((line, index) => parseStrictSignalRecord(line, index + 1));
|
|
166
|
+
const receiptIds = await loadReceiptIds(baseDir);
|
|
167
|
+
for (const signal of signals) {
|
|
168
|
+
if (!signal.read && receiptIds.has(signal.id)) {
|
|
169
|
+
signal.read = true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
let filtered = signals;
|
|
173
|
+
if (wuId) {
|
|
174
|
+
filtered = filtered.filter((sig) => sig.wu_id === wuId);
|
|
175
|
+
}
|
|
176
|
+
if (lane) {
|
|
177
|
+
filtered = filtered.filter((sig) => sig.lane === lane);
|
|
178
|
+
}
|
|
179
|
+
if (unreadOnly) {
|
|
180
|
+
filtered = filtered.filter((sig) => sig.read === false);
|
|
181
|
+
}
|
|
182
|
+
if (since) {
|
|
183
|
+
const sinceTime = since instanceof Date ? since : new Date(since);
|
|
184
|
+
filtered = filtered.filter((sig) => new Date(sig.created_at) > sinceTime);
|
|
185
|
+
}
|
|
186
|
+
return filtered;
|
|
187
|
+
}
|
|
188
|
+
async function markSignalsAsRead(baseDir, signalIds) {
|
|
189
|
+
const signalsPath = getSignalsPath(baseDir);
|
|
190
|
+
const idSet = new Set(signalIds);
|
|
191
|
+
let signalContent;
|
|
192
|
+
try {
|
|
193
|
+
signalContent = await fs.readFile(signalsPath, { encoding: "utf-8" });
|
|
194
|
+
} catch (err) {
|
|
195
|
+
const error = err;
|
|
196
|
+
if (error.code === "ENOENT") {
|
|
197
|
+
return { markedCount: 0 };
|
|
198
|
+
}
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
const lines = signalContent.split("\n").filter((line) => line.trim());
|
|
202
|
+
const inlineReadIds = /* @__PURE__ */ new Set();
|
|
203
|
+
const existingIds = /* @__PURE__ */ new Set();
|
|
204
|
+
for (const line of lines) {
|
|
205
|
+
const signal = JSON.parse(line);
|
|
206
|
+
existingIds.add(signal.id);
|
|
207
|
+
if (signal.read) {
|
|
208
|
+
inlineReadIds.add(signal.id);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const existingReceiptIds = await loadReceiptIds(baseDir);
|
|
212
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
213
|
+
const newReceipts = [];
|
|
214
|
+
for (const id of idSet) {
|
|
215
|
+
if (existingIds.has(id) && !inlineReadIds.has(id) && !existingReceiptIds.has(id)) {
|
|
216
|
+
newReceipts.push({ signal_id: id, read_at: now });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (newReceipts.length > 0) {
|
|
220
|
+
const memoryDir = getMemoryDir(baseDir);
|
|
221
|
+
await fs.mkdir(memoryDir, { recursive: true });
|
|
222
|
+
const receiptsPath = getReceiptsPath(baseDir);
|
|
223
|
+
const receiptLines = newReceipts.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
224
|
+
await fs.appendFile(receiptsPath, receiptLines, "utf-8");
|
|
225
|
+
}
|
|
226
|
+
return { markedCount: newReceipts.length };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export {
|
|
230
|
+
SIGNAL_FILE_NAME,
|
|
231
|
+
SIGNAL_RECEIPTS_FILE_NAME,
|
|
232
|
+
createSignal,
|
|
233
|
+
loadSignals,
|
|
234
|
+
markSignalsAsRead
|
|
235
|
+
};
|
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INLINE_KEYWORD_MAX_OFFSET
|
|
3
|
+
} from "./chunk-2GXVIN57.js";
|
|
4
|
+
import {
|
|
5
|
+
extractParent
|
|
6
|
+
} from "./chunk-JONWQUB5.js";
|
|
7
|
+
import {
|
|
8
|
+
getGitForCwd
|
|
9
|
+
} from "./chunk-2UFQ3A3C.js";
|
|
10
|
+
import {
|
|
11
|
+
BRANCHES,
|
|
12
|
+
EMOJI,
|
|
13
|
+
GIT_COMMANDS,
|
|
14
|
+
LANE_PATH_PATTERNS,
|
|
15
|
+
LOG_PREFIX,
|
|
16
|
+
STRING_LITERALS
|
|
17
|
+
} from "./chunk-DWMLTXKQ.js";
|
|
18
|
+
import {
|
|
19
|
+
ErrorCodes,
|
|
20
|
+
createError
|
|
21
|
+
} from "./chunk-RXRKBBSM.js";
|
|
22
|
+
|
|
23
|
+
// ../core/dist/code-path-validator.js
|
|
24
|
+
import path from "path";
|
|
25
|
+
import { existsSync, readFileSync } from "fs";
|
|
26
|
+
import { execSync } from "child_process";
|
|
27
|
+
import micromatch from "micromatch";
|
|
28
|
+
var VALIDATION_MODES = Object.freeze({
|
|
29
|
+
/** Check file existence - used by wu:done */
|
|
30
|
+
EXIST: "exist",
|
|
31
|
+
/** Check lane pattern matching - used by wu:claim */
|
|
32
|
+
LANE: "lane",
|
|
33
|
+
/** Check code quality (TODOs, mocks) - used by wu:done */
|
|
34
|
+
QUALITY: "quality"
|
|
35
|
+
});
|
|
36
|
+
function isTestFile(filePath) {
|
|
37
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
38
|
+
const testPatterns = [
|
|
39
|
+
/\.test\.(ts|tsx|js|jsx|mjs)$/,
|
|
40
|
+
/\.spec\.(ts|tsx|js|jsx|mjs)$/,
|
|
41
|
+
/__tests__\//,
|
|
42
|
+
/\.test-utils\./,
|
|
43
|
+
/\.mock\./
|
|
44
|
+
];
|
|
45
|
+
return testPatterns.some((pattern) => pattern.test(normalized));
|
|
46
|
+
}
|
|
47
|
+
function isMarkdownFile(filePath) {
|
|
48
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
49
|
+
return /\.md$/i.test(normalized);
|
|
50
|
+
}
|
|
51
|
+
function getRepoRoot() {
|
|
52
|
+
try {
|
|
53
|
+
return execSync("git rev-parse --show-toplevel", {
|
|
54
|
+
encoding: "utf-8",
|
|
55
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
56
|
+
}).trim();
|
|
57
|
+
} catch {
|
|
58
|
+
return process.cwd();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function validateExistenceInWorktree(codePaths, worktreePath) {
|
|
62
|
+
const missing = [];
|
|
63
|
+
const errors = [];
|
|
64
|
+
for (const filePath of codePaths) {
|
|
65
|
+
const fullPath = path.join(worktreePath, filePath);
|
|
66
|
+
if (!existsSync(fullPath)) {
|
|
67
|
+
missing.push(filePath);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (missing.length > 0) {
|
|
71
|
+
errors.push(`code_paths validation failed - ${missing.length} file(s) not found in worktree:
|
|
72
|
+
${missing.map((p) => ` - ${p}`).join(STRING_LITERALS.NEWLINE)}
|
|
73
|
+
|
|
74
|
+
Ensure all files listed in code_paths exist before running wu:done.`);
|
|
75
|
+
}
|
|
76
|
+
return { valid: errors.length === 0, errors, missing };
|
|
77
|
+
}
|
|
78
|
+
async function validateExistenceOnBranch(codePaths, targetBranch) {
|
|
79
|
+
const missing = [];
|
|
80
|
+
const errors = [];
|
|
81
|
+
try {
|
|
82
|
+
const gitAdapter = getGitForCwd();
|
|
83
|
+
for (const filePath of codePaths) {
|
|
84
|
+
try {
|
|
85
|
+
const result = await gitAdapter.raw([GIT_COMMANDS.LS_TREE, targetBranch, "--", filePath]);
|
|
86
|
+
if (!result || result.trim() === "") {
|
|
87
|
+
missing.push(filePath);
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
missing.push(filePath);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (missing.length > 0) {
|
|
94
|
+
errors.push(`code_paths validation failed - ${missing.length} file(s) not found on ${targetBranch}:
|
|
95
|
+
${missing.map((p) => ` - ${p}`).join(STRING_LITERALS.NEWLINE)}
|
|
96
|
+
|
|
97
|
+
\u274C POTENTIAL FALSE COMPLETION DETECTED
|
|
98
|
+
|
|
99
|
+
These files are listed in code_paths but do not exist on ${targetBranch}.
|
|
100
|
+
This prevents creating a stamp for incomplete work.
|
|
101
|
+
|
|
102
|
+
Fix options:
|
|
103
|
+
1. Ensure all code is committed and merged to ${targetBranch}
|
|
104
|
+
2. Update code_paths in WU YAML to match actual files
|
|
105
|
+
3. Remove files that were intentionally not created
|
|
106
|
+
|
|
107
|
+
Context: WU-1351 prevents false completions from INIT-WORKFLOW-INTEGRITY`);
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
111
|
+
console.warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not validate code_paths: ${errMessage}`);
|
|
112
|
+
return { valid: true, errors: [], missing: [] };
|
|
113
|
+
}
|
|
114
|
+
return { valid: errors.length === 0, errors, missing };
|
|
115
|
+
}
|
|
116
|
+
function validateLanePatterns(codePaths, lane) {
|
|
117
|
+
if (!codePaths || codePaths.length === 0) {
|
|
118
|
+
return {
|
|
119
|
+
hasWarnings: false,
|
|
120
|
+
warnings: [],
|
|
121
|
+
violations: [],
|
|
122
|
+
skipped: true
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const parentLane = extractParent(lane);
|
|
126
|
+
const patterns = LANE_PATH_PATTERNS[parentLane];
|
|
127
|
+
if (!patterns) {
|
|
128
|
+
return {
|
|
129
|
+
hasWarnings: false,
|
|
130
|
+
warnings: [],
|
|
131
|
+
violations: [],
|
|
132
|
+
skipped: true
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
const { exclude = [], allowExceptions = [] } = patterns;
|
|
136
|
+
const violations = codePaths.filter((codePath) => {
|
|
137
|
+
const matchesExclude = micromatch.isMatch(codePath, exclude, { nocase: true });
|
|
138
|
+
if (!matchesExclude)
|
|
139
|
+
return false;
|
|
140
|
+
if (allowExceptions.length > 0) {
|
|
141
|
+
const matchesException = micromatch.isMatch(codePath, allowExceptions, { nocase: true });
|
|
142
|
+
if (matchesException)
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
});
|
|
147
|
+
if (violations.length === 0) {
|
|
148
|
+
return {
|
|
149
|
+
hasWarnings: false,
|
|
150
|
+
warnings: [],
|
|
151
|
+
violations: [],
|
|
152
|
+
skipped: false
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const warnings = violations.map((violatingPath) => {
|
|
156
|
+
return `Lane "${lane}" typically doesn't include "${violatingPath}" (expected for different lane)`;
|
|
157
|
+
});
|
|
158
|
+
return {
|
|
159
|
+
hasWarnings: true,
|
|
160
|
+
warnings,
|
|
161
|
+
violations,
|
|
162
|
+
skipped: false
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
function scanFileForTODOs(filePath) {
|
|
166
|
+
if (!existsSync(filePath)) {
|
|
167
|
+
return { found: false, matches: [] };
|
|
168
|
+
}
|
|
169
|
+
if (isTestFile(filePath)) {
|
|
170
|
+
return { found: false, matches: [] };
|
|
171
|
+
}
|
|
172
|
+
if (isMarkdownFile(filePath)) {
|
|
173
|
+
return { found: false, matches: [] };
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const content = readFileSync(filePath, { encoding: "utf-8" });
|
|
177
|
+
const lines = content.split(/\r?\n/);
|
|
178
|
+
const matches = [];
|
|
179
|
+
const checkForActionableMarker = (line) => {
|
|
180
|
+
const trimmed = line.trim();
|
|
181
|
+
if (trimmed.includes("// TODO:,") || trimmed.includes("/* TODO */")) {
|
|
182
|
+
return { found: false, pattern: null };
|
|
183
|
+
}
|
|
184
|
+
if (trimmed.includes("@todo,") || trimmed.includes("@-prefixed:")) {
|
|
185
|
+
return { found: false, pattern: null };
|
|
186
|
+
}
|
|
187
|
+
const atTagMatch = trimmed.match(/^\*\s+@(todo|fixme|hack|xxx)\b/i);
|
|
188
|
+
if (atTagMatch) {
|
|
189
|
+
const atTag = atTagMatch[1];
|
|
190
|
+
return atTag ? { found: true, pattern: atTag.toUpperCase() } : { found: false, pattern: null };
|
|
191
|
+
}
|
|
192
|
+
const commentStartMatch = trimmed.match(/^(?:\/\/|\/\*+|\*|<!--|#)\s*(TODO|FIXME|HACK|XXX)(?::|[\s]|$)/i);
|
|
193
|
+
if (commentStartMatch) {
|
|
194
|
+
const commentKeyword = commentStartMatch[1];
|
|
195
|
+
if (!commentKeyword) {
|
|
196
|
+
return { found: false, pattern: null };
|
|
197
|
+
}
|
|
198
|
+
const afterKeyword = trimmed.slice(trimmed.indexOf(commentKeyword) + commentKeyword.length);
|
|
199
|
+
if (!afterKeyword.startsWith("/")) {
|
|
200
|
+
return { found: true, pattern: commentKeyword.toUpperCase() };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const inlineCommentMatch = line.match(/\/\/\s*(TODO|FIXME|HACK|XXX)(?::|[\s]|$)/i);
|
|
204
|
+
if (inlineCommentMatch && !line.match(/\/\/\s*(TODO|FIXME|HACK|XXX)\//i)) {
|
|
205
|
+
const inlineKeyword = inlineCommentMatch[1];
|
|
206
|
+
if (!inlineKeyword) {
|
|
207
|
+
return { found: false, pattern: null };
|
|
208
|
+
}
|
|
209
|
+
const doubleSlashIndex = line.indexOf("//");
|
|
210
|
+
const beforeSlash = line.slice(0, doubleSlashIndex);
|
|
211
|
+
const singleQuotes = (beforeSlash.match(/(?<!\\)'/g) || []).length;
|
|
212
|
+
const doubleQuotes = (beforeSlash.match(/(?<!\\)"/g) || []).length;
|
|
213
|
+
const backticks = (beforeSlash.match(/(?<!\\)`/g) || []).length;
|
|
214
|
+
if (singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0 || backticks % 2 !== 0) {
|
|
215
|
+
return { found: false, pattern: null };
|
|
216
|
+
}
|
|
217
|
+
const commentPart = line.slice(doubleSlashIndex);
|
|
218
|
+
const keywordIndex = commentPart.search(/\b(TODO|FIXME|HACK|XXX)\b/i);
|
|
219
|
+
if (keywordIndex >= 0 && keywordIndex <= INLINE_KEYWORD_MAX_OFFSET) {
|
|
220
|
+
return { found: true, pattern: inlineKeyword.toUpperCase() };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (trimmed.match(/\bWU-XXX\b/i)) {
|
|
224
|
+
return { found: false, pattern: null };
|
|
225
|
+
}
|
|
226
|
+
return { found: false, pattern: null };
|
|
227
|
+
};
|
|
228
|
+
lines.forEach((line, index) => {
|
|
229
|
+
const lineNumber = index + 1;
|
|
230
|
+
const trimmed = line.trim();
|
|
231
|
+
const isComment = /^(\/\/|\/\*|\*|<!--|#)/.test(trimmed) || line.includes("//") || line.includes("/*");
|
|
232
|
+
if (isComment) {
|
|
233
|
+
const result = checkForActionableMarker(line);
|
|
234
|
+
if (result.found) {
|
|
235
|
+
matches.push({
|
|
236
|
+
line: lineNumber,
|
|
237
|
+
text: trimmed,
|
|
238
|
+
pattern: result.pattern
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
return { found: matches.length > 0, matches };
|
|
244
|
+
} catch {
|
|
245
|
+
return { found: false, matches: [] };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function scanFileForMocks(filePath) {
|
|
249
|
+
if (!existsSync(filePath)) {
|
|
250
|
+
return { found: false, matches: [] };
|
|
251
|
+
}
|
|
252
|
+
if (isTestFile(filePath)) {
|
|
253
|
+
return { found: false, matches: [] };
|
|
254
|
+
}
|
|
255
|
+
try {
|
|
256
|
+
const content = readFileSync(filePath, { encoding: "utf-8" });
|
|
257
|
+
const lines = content.split(/\r?\n/);
|
|
258
|
+
const matches = [];
|
|
259
|
+
const mockPatterns = [
|
|
260
|
+
{ name: "Mock", regex: /\b(class|export\s+class)\s+(\w*Mock\w*)/i },
|
|
261
|
+
{ name: "Stub", regex: /\b(class|export\s+class)\s+(\w*Stub\w*)/i },
|
|
262
|
+
{ name: "Fake", regex: /\b(class|export\s+class)\s+(\w*Fake\w*)/i },
|
|
263
|
+
{ name: "Placeholder", regex: /\b(class|export\s+class)\s+(\w*Placeholder\w*)/i },
|
|
264
|
+
{ name: "Mock", regex: /\b(function|const|let|var)\s+(\w*mock\w*)/i },
|
|
265
|
+
{ name: "Stub", regex: /\b(function|const|let|var)\s+(\w*stub\w*)/i },
|
|
266
|
+
{ name: "Fake", regex: /\b(function|const|let|var)\s+(\w*fake\w*)/i },
|
|
267
|
+
{ name: "Placeholder", regex: /\b(function|const|let|var)\s+(\w*placeholder\w*)/i }
|
|
268
|
+
];
|
|
269
|
+
lines.forEach((line, index) => {
|
|
270
|
+
const lineNumber = index + 1;
|
|
271
|
+
mockPatterns.forEach(({ name, regex }) => {
|
|
272
|
+
const match = regex.exec(line);
|
|
273
|
+
if (match) {
|
|
274
|
+
matches.push({
|
|
275
|
+
line: lineNumber,
|
|
276
|
+
text: line.trim(),
|
|
277
|
+
type: name
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
return { found: matches.length > 0, matches };
|
|
283
|
+
} catch {
|
|
284
|
+
return { found: false, matches: [] };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
function formatTODOFindings(findings) {
|
|
288
|
+
let msg = "\n\u274C TODO/FIXME/HACK/XXX comments found in production code:\n";
|
|
289
|
+
findings.forEach(({ path: filePath, matches }) => {
|
|
290
|
+
msg += `
|
|
291
|
+
${filePath}:
|
|
292
|
+
`;
|
|
293
|
+
matches.forEach(({ line, text }) => {
|
|
294
|
+
msg += ` Line ${line}: ${text}
|
|
295
|
+
`;
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
msg += "\nThese indicate incomplete work and must be resolved before WU completion.";
|
|
299
|
+
msg += "\nEither complete the work or use --allow-todo with justification in WU notes.";
|
|
300
|
+
return msg;
|
|
301
|
+
}
|
|
302
|
+
function formatMockFindings(findings) {
|
|
303
|
+
let msg = "\n\u26A0\uFE0F Mock/Stub/Fake/Placeholder classes found in production code:\n";
|
|
304
|
+
findings.forEach(({ path: filePath, matches }) => {
|
|
305
|
+
msg += `
|
|
306
|
+
${filePath}:
|
|
307
|
+
`;
|
|
308
|
+
matches.forEach(({ line, text }) => {
|
|
309
|
+
msg += ` Line ${line}: ${text}
|
|
310
|
+
`;
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
msg += "\nThese suggest incomplete implementation (interface \u2260 implementation).";
|
|
314
|
+
msg += "\nVerify these are actual implementations, not placeholder code.";
|
|
315
|
+
return msg;
|
|
316
|
+
}
|
|
317
|
+
function validateCodeQuality(codePaths, options = {}) {
|
|
318
|
+
const { allowTodos = false, worktreePath = null } = options;
|
|
319
|
+
const errors = [];
|
|
320
|
+
const warnings = [];
|
|
321
|
+
const repoRoot = worktreePath || getRepoRoot();
|
|
322
|
+
if (!codePaths || codePaths.length === 0) {
|
|
323
|
+
return { valid: true, errors, warnings };
|
|
324
|
+
}
|
|
325
|
+
const todoFindings = [];
|
|
326
|
+
const mockFindings = [];
|
|
327
|
+
for (const codePath of codePaths) {
|
|
328
|
+
const absolutePath = path.join(repoRoot, codePath);
|
|
329
|
+
if (!existsSync(absolutePath)) {
|
|
330
|
+
errors.push(`
|
|
331
|
+
\u274C Code path validation failed: File does not exist: ${codePath}
|
|
332
|
+
|
|
333
|
+
This indicates the WU claims to have created/modified a file that doesn't exist.
|
|
334
|
+
Either create the file, or remove it from code_paths in the WU YAML.
|
|
335
|
+
`);
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
const todoResult = scanFileForTODOs(absolutePath);
|
|
339
|
+
if (todoResult.found) {
|
|
340
|
+
todoFindings.push({ path: codePath, ...todoResult });
|
|
341
|
+
}
|
|
342
|
+
const mockResult = scanFileForMocks(absolutePath);
|
|
343
|
+
if (mockResult.found) {
|
|
344
|
+
mockFindings.push({ path: codePath, ...mockResult });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (todoFindings.length > 0) {
|
|
348
|
+
const message = formatTODOFindings(todoFindings);
|
|
349
|
+
if (allowTodos) {
|
|
350
|
+
warnings.push(message);
|
|
351
|
+
} else {
|
|
352
|
+
errors.push(message);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (mockFindings.length > 0) {
|
|
356
|
+
warnings.push(formatMockFindings(mockFindings));
|
|
357
|
+
}
|
|
358
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
359
|
+
}
|
|
360
|
+
async function validate(codePaths, options = {}) {
|
|
361
|
+
const { mode = VALIDATION_MODES.EXIST } = options;
|
|
362
|
+
switch (mode) {
|
|
363
|
+
case VALIDATION_MODES.EXIST: {
|
|
364
|
+
const { worktreePath, targetBranch = BRANCHES.MAIN } = options;
|
|
365
|
+
if (!codePaths || codePaths.length === 0) {
|
|
366
|
+
return { valid: true, errors: [], missing: [] };
|
|
367
|
+
}
|
|
368
|
+
if (worktreePath && existsSync(worktreePath)) {
|
|
369
|
+
return validateExistenceInWorktree(codePaths, worktreePath);
|
|
370
|
+
}
|
|
371
|
+
return validateExistenceOnBranch(codePaths, targetBranch);
|
|
372
|
+
}
|
|
373
|
+
case VALIDATION_MODES.LANE: {
|
|
374
|
+
const { lane } = options;
|
|
375
|
+
if (!lane) {
|
|
376
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, "Lane name is required for lane validation mode");
|
|
377
|
+
}
|
|
378
|
+
return validateLanePatterns(codePaths, lane);
|
|
379
|
+
}
|
|
380
|
+
case VALIDATION_MODES.QUALITY: {
|
|
381
|
+
const { worktreePath, allowTodos } = options;
|
|
382
|
+
return validateCodeQuality(codePaths, { worktreePath, allowTodos });
|
|
383
|
+
}
|
|
384
|
+
default:
|
|
385
|
+
throw createError(ErrorCodes.INVALID_ARGUMENT, `Unknown validation mode: ${mode}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
async function validateCodePathsExist(doc, _id, options = {}) {
|
|
389
|
+
const codePaths = doc.code_paths || [];
|
|
390
|
+
const { targetBranch = BRANCHES.MAIN, worktreePath = null } = options;
|
|
391
|
+
if (codePaths.length === 0) {
|
|
392
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.INFO} No code_paths to validate for ${_id}`);
|
|
393
|
+
return { valid: true, errors: [], missing: [] };
|
|
394
|
+
}
|
|
395
|
+
console.log(`${LOG_PREFIX.DONE} Validating ${codePaths.length} code_paths exist...`);
|
|
396
|
+
const result = await validate(codePaths, {
|
|
397
|
+
mode: VALIDATION_MODES.EXIST,
|
|
398
|
+
worktreePath: worktreePath ?? void 0,
|
|
399
|
+
targetBranch
|
|
400
|
+
});
|
|
401
|
+
if (result.valid) {
|
|
402
|
+
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} All ${codePaths.length} code_paths verified`);
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
function validateLaneCodePaths(doc, lane) {
|
|
407
|
+
const codePaths = doc.code_paths || [];
|
|
408
|
+
return validateLanePatterns(codePaths, lane);
|
|
409
|
+
}
|
|
410
|
+
function validateWUCodePaths(codePaths, options = {}) {
|
|
411
|
+
const { allowTodos = false, worktreePath = null } = options;
|
|
412
|
+
return validateCodeQuality(codePaths, { worktreePath, allowTodos });
|
|
413
|
+
}
|
|
414
|
+
function logLaneValidationWarnings(result, logPrefix = "[wu-claim]") {
|
|
415
|
+
if (!result.hasWarnings) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
console.warn(`${logPrefix} Lane/code_paths mismatch detected (advisory only):`);
|
|
419
|
+
for (const warning of result.warnings) {
|
|
420
|
+
console.warn(`${logPrefix} ${warning}`);
|
|
421
|
+
}
|
|
422
|
+
console.warn(`${logPrefix} This is a warning only - proceeding with claim.`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
export {
|
|
426
|
+
validateCodePathsExist,
|
|
427
|
+
validateLaneCodePaths,
|
|
428
|
+
validateWUCodePaths,
|
|
429
|
+
logLaneValidationWarnings
|
|
430
|
+
};
|