@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-CK36VROC.js
DELETED
|
@@ -1,1584 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
isDocumentationType
|
|
3
|
-
} from "./chunk-2D2VOCA4.js";
|
|
4
|
-
import {
|
|
5
|
-
resolveStateDir
|
|
6
|
-
} from "./chunk-NRAXROED.js";
|
|
7
|
-
import {
|
|
8
|
-
runGates
|
|
9
|
-
} from "./chunk-GDNSBQVK.js";
|
|
10
|
-
import {
|
|
11
|
-
WUStateStore,
|
|
12
|
-
WU_OPTIONS,
|
|
13
|
-
createWUParser,
|
|
14
|
-
defaultBranchFrom,
|
|
15
|
-
formatPreflightResult,
|
|
16
|
-
formatPreflightWarnings,
|
|
17
|
-
getCurrentBranch,
|
|
18
|
-
getLatestWuBriefEvidence,
|
|
19
|
-
getWuBriefEvidenceAgeMinutes,
|
|
20
|
-
hasMatchingWuBriefEvidenceHash,
|
|
21
|
-
isWuBriefEvidenceStale,
|
|
22
|
-
resolveLocation,
|
|
23
|
-
runCLI,
|
|
24
|
-
validatePreflight
|
|
25
|
-
} from "./chunk-2GXVIN57.js";
|
|
26
|
-
import {
|
|
27
|
-
DelegationRegistryStore
|
|
28
|
-
} from "./chunk-6W5XHWYV.js";
|
|
29
|
-
import {
|
|
30
|
-
DOCS_ONLY_ROOT_FILES,
|
|
31
|
-
TEST_FILE_PATTERNS,
|
|
32
|
-
findMissingCodePathCoverage,
|
|
33
|
-
getDocsOnlyPrefixes,
|
|
34
|
-
isCodePathCoveredByChangedFiles,
|
|
35
|
-
resolveChangedFiles,
|
|
36
|
-
validateExposure,
|
|
37
|
-
validateFeatureAccessibility
|
|
38
|
-
} from "./chunk-L737LQ4C.js";
|
|
39
|
-
import {
|
|
40
|
-
createGitForPath
|
|
41
|
-
} from "./chunk-2UFQ3A3C.js";
|
|
42
|
-
import {
|
|
43
|
-
createSignal
|
|
44
|
-
} from "./chunk-OMJD6A3S.js";
|
|
45
|
-
import {
|
|
46
|
-
readWU
|
|
47
|
-
} from "./chunk-NRIZR3A7.js";
|
|
48
|
-
import {
|
|
49
|
-
WU_PATHS,
|
|
50
|
-
createWuPaths
|
|
51
|
-
} from "./chunk-6HO4GWJE.js";
|
|
52
|
-
import {
|
|
53
|
-
CLI_FLAGS,
|
|
54
|
-
CONTEXT_VALIDATION,
|
|
55
|
-
DIRECTORIES,
|
|
56
|
-
EMOJI,
|
|
57
|
-
EXIT_CODES,
|
|
58
|
-
LOG_PREFIX,
|
|
59
|
-
LUMENFLOW_PATHS,
|
|
60
|
-
PATTERNS,
|
|
61
|
-
PKG_MANAGER,
|
|
62
|
-
SCRIPTS
|
|
63
|
-
} from "./chunk-DWMLTXKQ.js";
|
|
64
|
-
import {
|
|
65
|
-
CLAIMED_MODES,
|
|
66
|
-
GIT_DIRECTORY_NAME,
|
|
67
|
-
MS_PER_DAY,
|
|
68
|
-
WU_EXPOSURE,
|
|
69
|
-
WU_STATUS,
|
|
70
|
-
WU_TYPES,
|
|
71
|
-
getConfig
|
|
72
|
-
} from "./chunk-V6OJGLBA.js";
|
|
73
|
-
import {
|
|
74
|
-
die,
|
|
75
|
-
getErrorMessage
|
|
76
|
-
} from "./chunk-RXRKBBSM.js";
|
|
77
|
-
|
|
78
|
-
// src/wu-prep.ts
|
|
79
|
-
import { spawnSync as spawnSync2 } from "child_process";
|
|
80
|
-
import { existsSync as existsSync2 } from "fs";
|
|
81
|
-
import path3 from "path";
|
|
82
|
-
import { fileURLToPath } from "url";
|
|
83
|
-
|
|
84
|
-
// ../core/dist/wu-checkpoint.js
|
|
85
|
-
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync } from "fs";
|
|
86
|
-
import path from "path";
|
|
87
|
-
import crypto from "crypto";
|
|
88
|
-
var CHECKPOINT_SCHEMA_VERSION = 1;
|
|
89
|
-
var CHECKPOINT_DIR = "checkpoints";
|
|
90
|
-
var CHECKPOINT_MAX_AGE_MS = MS_PER_DAY;
|
|
91
|
-
function getCheckpointPath(wuId, options = {}) {
|
|
92
|
-
const baseDir = options.baseDir || process.cwd();
|
|
93
|
-
return path.join(baseDir, LUMENFLOW_PATHS.BASE, CHECKPOINT_DIR, `${wuId}.checkpoint.json`);
|
|
94
|
-
}
|
|
95
|
-
function ensureCheckpointDir(options = {}) {
|
|
96
|
-
const baseDir = options.baseDir || process.cwd();
|
|
97
|
-
const checkpointDir = path.join(baseDir, LUMENFLOW_PATHS.BASE, CHECKPOINT_DIR);
|
|
98
|
-
if (!existsSync(checkpointDir)) {
|
|
99
|
-
mkdirSync(checkpointDir, { recursive: true });
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
function generateCheckpointId() {
|
|
103
|
-
return `ckpt-${crypto.randomUUID().slice(0, 8)}`;
|
|
104
|
-
}
|
|
105
|
-
function getHeadSha(dir) {
|
|
106
|
-
try {
|
|
107
|
-
const gitDir = path.join(dir, GIT_DIRECTORY_NAME);
|
|
108
|
-
if (existsSync(gitDir)) {
|
|
109
|
-
const headPath = path.join(gitDir, "HEAD");
|
|
110
|
-
if (existsSync(headPath)) {
|
|
111
|
-
const headContent = readFileSync(headPath, "utf8").trim();
|
|
112
|
-
if (headContent.startsWith("ref: ")) {
|
|
113
|
-
const refPath = path.join(gitDir, headContent.slice(5));
|
|
114
|
-
if (existsSync(refPath)) {
|
|
115
|
-
return readFileSync(refPath, "utf8").trim();
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return headContent;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
const gitFile = path.join(dir, GIT_DIRECTORY_NAME);
|
|
122
|
-
if (existsSync(gitFile)) {
|
|
123
|
-
const gitFileContent = readFileSync(gitFile, "utf8").trim();
|
|
124
|
-
if (gitFileContent.startsWith("gitdir: ")) {
|
|
125
|
-
const worktreeGitDir = gitFileContent.slice(8);
|
|
126
|
-
const headPath = path.join(worktreeGitDir, "HEAD");
|
|
127
|
-
if (existsSync(headPath)) {
|
|
128
|
-
const headContent = readFileSync(headPath, "utf8").trim();
|
|
129
|
-
if (headContent.startsWith("ref: ")) {
|
|
130
|
-
const mainGitDir = path.resolve(worktreeGitDir, "..", "..");
|
|
131
|
-
const refPath = path.join(mainGitDir, headContent.slice(5));
|
|
132
|
-
if (existsSync(refPath)) {
|
|
133
|
-
return readFileSync(refPath, "utf8").trim();
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
return headContent;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
return "unknown-sha";
|
|
141
|
-
} catch {
|
|
142
|
-
return "unknown-sha";
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
async function createPreGatesCheckpoint(params, options = {}) {
|
|
146
|
-
const { wuId, worktreePath, branchName, gatesPassed = false } = params;
|
|
147
|
-
const { baseDir } = options;
|
|
148
|
-
ensureCheckpointDir({ baseDir });
|
|
149
|
-
const checkpoint = {
|
|
150
|
-
schemaVersion: CHECKPOINT_SCHEMA_VERSION,
|
|
151
|
-
checkpointId: generateCheckpointId(),
|
|
152
|
-
wuId,
|
|
153
|
-
worktreePath,
|
|
154
|
-
branchName,
|
|
155
|
-
worktreeHeadSha: getHeadSha(worktreePath),
|
|
156
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
157
|
-
gatesPassed,
|
|
158
|
-
gatesPassedAt: gatesPassed ? (/* @__PURE__ */ new Date()).toISOString() : null
|
|
159
|
-
};
|
|
160
|
-
const checkpointPath = getCheckpointPath(wuId, { baseDir });
|
|
161
|
-
writeFileSync(checkpointPath, JSON.stringify(checkpoint, null, 2));
|
|
162
|
-
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Created pre-gates checkpoint for ${wuId}`);
|
|
163
|
-
return checkpoint;
|
|
164
|
-
}
|
|
165
|
-
function markGatesPassed(wuId, options = {}) {
|
|
166
|
-
const checkpoint = getCheckpoint(wuId, options);
|
|
167
|
-
if (!checkpoint) {
|
|
168
|
-
return false;
|
|
169
|
-
}
|
|
170
|
-
checkpoint.gatesPassed = true;
|
|
171
|
-
checkpoint.gatesPassedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
172
|
-
const checkpointPath = getCheckpointPath(wuId, options);
|
|
173
|
-
writeFileSync(checkpointPath, JSON.stringify(checkpoint, null, 2));
|
|
174
|
-
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Marked gates passed for ${wuId}`);
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
function getCheckpoint(wuId, options = {}) {
|
|
178
|
-
const checkpointPath = getCheckpointPath(wuId, options);
|
|
179
|
-
if (!existsSync(checkpointPath)) {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
try {
|
|
183
|
-
const content = readFileSync(checkpointPath, "utf8");
|
|
184
|
-
return JSON.parse(content);
|
|
185
|
-
} catch {
|
|
186
|
-
return null;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
function clearCheckpoint(wuId, options = {}) {
|
|
190
|
-
const checkpointPath = getCheckpointPath(wuId, options);
|
|
191
|
-
if (existsSync(checkpointPath)) {
|
|
192
|
-
unlinkSync(checkpointPath);
|
|
193
|
-
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Cleared checkpoint for ${wuId}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
function canSkipGates(wuId, options = {}) {
|
|
197
|
-
const { baseDir, currentHeadSha } = options;
|
|
198
|
-
const checkpoint = getCheckpoint(wuId, { baseDir });
|
|
199
|
-
if (!checkpoint) {
|
|
200
|
-
return { canSkip: false, reason: "No checkpoint exists" };
|
|
201
|
-
}
|
|
202
|
-
if (checkpoint.schemaVersion !== CHECKPOINT_SCHEMA_VERSION) {
|
|
203
|
-
return {
|
|
204
|
-
canSkip: false,
|
|
205
|
-
reason: `Checkpoint schema version mismatch (got ${checkpoint.schemaVersion}, expected ${CHECKPOINT_SCHEMA_VERSION})`
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
if (!checkpoint.gatesPassed) {
|
|
209
|
-
return { canSkip: false, reason: "Gates did not pass at checkpoint" };
|
|
210
|
-
}
|
|
211
|
-
const checkpointAge = Date.now() - new Date(checkpoint.createdAt).getTime();
|
|
212
|
-
if (checkpointAge > CHECKPOINT_MAX_AGE_MS) {
|
|
213
|
-
return { canSkip: false, reason: "Checkpoint is stale (older than 24 hours)" };
|
|
214
|
-
}
|
|
215
|
-
if (currentHeadSha && currentHeadSha !== checkpoint.worktreeHeadSha) {
|
|
216
|
-
return {
|
|
217
|
-
canSkip: false,
|
|
218
|
-
reason: "Worktree has changed since checkpoint (SHA mismatch)"
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
console.log(`${LOG_PREFIX.DONE} ${EMOJI.SUCCESS} Gates can be skipped - checkpoint valid (${checkpoint.checkpointId})`);
|
|
222
|
-
return { canSkip: true, checkpoint };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// src/hooks/path-utils.ts
|
|
226
|
-
import * as path2 from "path";
|
|
227
|
-
import * as os from "os";
|
|
228
|
-
var GIT_STATUS_QUOTE = '"';
|
|
229
|
-
var PATH_PREFIX_CURRENT_DIR = "./";
|
|
230
|
-
var PATH_SEPARATOR_WINDOWS = "\\";
|
|
231
|
-
var PATH_SEPARATOR_POSIX = "/";
|
|
232
|
-
function ensureRepoRelativePrefix(value) {
|
|
233
|
-
const normalized = value.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
|
234
|
-
return normalized.length > 0 ? `${normalized}/` : "";
|
|
235
|
-
}
|
|
236
|
-
function stripWrappingQuotes(value) {
|
|
237
|
-
if (value.startsWith(GIT_STATUS_QUOTE) && value.endsWith(GIT_STATUS_QUOTE) && value.length >= 2) {
|
|
238
|
-
return value.slice(1, -1);
|
|
239
|
-
}
|
|
240
|
-
return value;
|
|
241
|
-
}
|
|
242
|
-
function normalizeRepoRelativePath(value) {
|
|
243
|
-
const withoutQuotes = stripWrappingQuotes(value.trim());
|
|
244
|
-
const normalizedSeparators = withoutQuotes.split(PATH_SEPARATOR_WINDOWS).join(PATH_SEPARATOR_POSIX);
|
|
245
|
-
if (normalizedSeparators.startsWith(PATH_PREFIX_CURRENT_DIR)) {
|
|
246
|
-
return normalizedSeparators.slice(PATH_PREFIX_CURRENT_DIR.length);
|
|
247
|
-
}
|
|
248
|
-
return normalizedSeparators;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// src/hooks/config-resolver.ts
|
|
252
|
-
var MAIN_WRITE_STATIC_ALLOWLIST_PREFIXES = [".lumenflow/", ".claude/", "plan/"];
|
|
253
|
-
var DEFAULT_WORKTREES_DIR_SEGMENT = DIRECTORIES.WORKTREES.replace(/\/+$/g, "");
|
|
254
|
-
var DEFAULT_WU_ALLOWLIST_PREFIX = `${DIRECTORIES.WU_DIR.replace(/\/+$/g, "")}/`;
|
|
255
|
-
function resolveWuAllowlistPrefix(mainRepoPath) {
|
|
256
|
-
try {
|
|
257
|
-
const configuredPath = createWuPaths({ projectRoot: mainRepoPath }).WU_DIR();
|
|
258
|
-
return ensureRepoRelativePrefix(configuredPath);
|
|
259
|
-
} catch {
|
|
260
|
-
return DEFAULT_WU_ALLOWLIST_PREFIX;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
function resolveMainWriteAllowlistPrefixes(mainRepoPath) {
|
|
264
|
-
return [resolveWuAllowlistPrefix(mainRepoPath), ...MAIN_WRITE_STATIC_ALLOWLIST_PREFIXES];
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// src/hooks/git-status-parser.ts
|
|
268
|
-
var GIT_STATUS_PREFIX_LENGTH = 3;
|
|
269
|
-
var GIT_STATUS_RENAME_SEPARATOR = " -> ";
|
|
270
|
-
var MAX_BLOCKED_PATHS_IN_MESSAGE = 10;
|
|
271
|
-
function parsePathFromStatusLine(line) {
|
|
272
|
-
if (line.length < GIT_STATUS_PREFIX_LENGTH) {
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
const pathField = line.slice(GIT_STATUS_PREFIX_LENGTH).trim();
|
|
276
|
-
if (pathField.length === 0) {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
const renameSegments = pathField.split(GIT_STATUS_RENAME_SEPARATOR);
|
|
280
|
-
const destinationPath = renameSegments[renameSegments.length - 1];
|
|
281
|
-
const normalizedPath = normalizeRepoRelativePath(destinationPath);
|
|
282
|
-
return normalizedPath.length > 0 ? normalizedPath : null;
|
|
283
|
-
}
|
|
284
|
-
function parseDirtyPathsFromStatus(mainStatus) {
|
|
285
|
-
const uniquePaths = /* @__PURE__ */ new Set();
|
|
286
|
-
for (const line of mainStatus.split("\n")) {
|
|
287
|
-
const trimmed = line.trimEnd();
|
|
288
|
-
if (trimmed.length === 0) {
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
291
|
-
const parsed = parsePathFromStatusLine(trimmed);
|
|
292
|
-
if (parsed) {
|
|
293
|
-
uniquePaths.add(parsed);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return Array.from(uniquePaths);
|
|
297
|
-
}
|
|
298
|
-
function getNonAllowlistedDirtyPaths(mainStatus, allowlistPrefixes) {
|
|
299
|
-
return parseDirtyPathsFromStatus(mainStatus).filter(
|
|
300
|
-
(relativePath) => !allowlistPrefixes.some((prefix) => relativePath.startsWith(prefix))
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
function formatBlockedPaths(paths) {
|
|
304
|
-
const displayed = paths.slice(0, MAX_BLOCKED_PATHS_IN_MESSAGE);
|
|
305
|
-
const lines = displayed.map((dirtyPath) => ` - ${dirtyPath}`);
|
|
306
|
-
const remainder = paths.length - displayed.length;
|
|
307
|
-
if (remainder > 0) {
|
|
308
|
-
lines.push(` - ... and ${remainder} more`);
|
|
309
|
-
}
|
|
310
|
-
return lines.join("\n");
|
|
311
|
-
}
|
|
312
|
-
function formatMainDirtyMutationGuardMessage(options) {
|
|
313
|
-
const { commandName, mainCheckout, blockedPaths, allowlistPrefixes } = options;
|
|
314
|
-
const allowlistLines = allowlistPrefixes.map((prefix) => ` - ${prefix}`).join("\n");
|
|
315
|
-
return `${commandName} blocked: main checkout has non-allowlisted dirty files while a worktree WU is active.
|
|
316
|
-
|
|
317
|
-
Dirty paths:
|
|
318
|
-
${formatBlockedPaths(blockedPaths)}
|
|
319
|
-
|
|
320
|
-
Allowed dirty prefixes on main:
|
|
321
|
-
${allowlistLines}
|
|
322
|
-
|
|
323
|
-
How to resolve:
|
|
324
|
-
1. Move edits into the active worktree (recommended)
|
|
325
|
-
2. Revert or commit unintended main edits
|
|
326
|
-
3. If writes came from MCP/tools, rerun them in the worktree path
|
|
327
|
-
4. Retry ${commandName}
|
|
328
|
-
|
|
329
|
-
Main checkout: ${mainCheckout}`;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// src/hooks/dirty-guard.ts
|
|
333
|
-
var DIRTY_MAIN_GUARD_REASONS = {
|
|
334
|
-
BRANCH_PR_MODE: "branch-pr-mode",
|
|
335
|
-
NO_WORKTREE_CONTEXT: "no-worktree-context",
|
|
336
|
-
CLEAN_OR_ALLOWLISTED: "clean-or-allowlisted",
|
|
337
|
-
BLOCKED_NON_ALLOWLISTED_DIRTY_MAIN: "blocked-non-allowlisted-dirty-main"
|
|
338
|
-
};
|
|
339
|
-
function evaluateMainDirtyMutationGuard(options) {
|
|
340
|
-
const { commandName, mainCheckout, mainStatus, hasActiveWorktreeContext, isBranchPrMode: isBranchPrMode2 } = options;
|
|
341
|
-
const allowlistPrefixes = resolveMainWriteAllowlistPrefixes(mainCheckout);
|
|
342
|
-
if (isBranchPrMode2) {
|
|
343
|
-
return {
|
|
344
|
-
blocked: false,
|
|
345
|
-
blockedPaths: [],
|
|
346
|
-
reason: DIRTY_MAIN_GUARD_REASONS.BRANCH_PR_MODE
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
if (!hasActiveWorktreeContext) {
|
|
350
|
-
return {
|
|
351
|
-
blocked: false,
|
|
352
|
-
blockedPaths: [],
|
|
353
|
-
reason: DIRTY_MAIN_GUARD_REASONS.NO_WORKTREE_CONTEXT
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
const blockedPaths = getNonAllowlistedDirtyPaths(mainStatus, allowlistPrefixes);
|
|
357
|
-
if (blockedPaths.length === 0) {
|
|
358
|
-
return {
|
|
359
|
-
blocked: false,
|
|
360
|
-
blockedPaths: [],
|
|
361
|
-
reason: DIRTY_MAIN_GUARD_REASONS.CLEAN_OR_ALLOWLISTED
|
|
362
|
-
};
|
|
363
|
-
}
|
|
364
|
-
return {
|
|
365
|
-
blocked: true,
|
|
366
|
-
blockedPaths,
|
|
367
|
-
reason: DIRTY_MAIN_GUARD_REASONS.BLOCKED_NON_ALLOWLISTED_DIRTY_MAIN,
|
|
368
|
-
message: formatMainDirtyMutationGuardMessage({
|
|
369
|
-
commandName,
|
|
370
|
-
mainCheckout,
|
|
371
|
-
blockedPaths,
|
|
372
|
-
allowlistPrefixes
|
|
373
|
-
})
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
// src/wu-done-policies.ts
|
|
378
|
-
var WU_BRIEF_POLICY_MODES = ["off", "manual", "auto", "required"];
|
|
379
|
-
var DEFAULT_WU_BRIEF_POLICY_MODE = "auto";
|
|
380
|
-
var DEFAULT_WU_BRIEF_FRESHNESS_MINUTES = 1440;
|
|
381
|
-
var PREP_LOG_PREFIX = "[wu-prep]";
|
|
382
|
-
var PREP_FORCE_REASON_REQUIRED_MESSAGE = "Missing required --reason for wu:brief policy bypass in wu:prep.";
|
|
383
|
-
function printExposureWarnings(wu, options = {}) {
|
|
384
|
-
const result = validateExposure(wu, { skipExposureCheck: options.skipExposureCheck });
|
|
385
|
-
if (result.warnings.length > 0) {
|
|
386
|
-
console.log(`
|
|
387
|
-
${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1999: Exposure validation warnings:`);
|
|
388
|
-
for (const warning of result.warnings) {
|
|
389
|
-
console.log(`${LOG_PREFIX.DONE} ${warning}`);
|
|
390
|
-
}
|
|
391
|
-
console.log(
|
|
392
|
-
`${LOG_PREFIX.DONE} These are non-blocking warnings. To skip, use --skip-exposure-check flag.
|
|
393
|
-
`
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
function validateAccessibilityOrDie(wu, options = {}) {
|
|
398
|
-
const result = validateFeatureAccessibility(wu, {
|
|
399
|
-
skipAccessibilityCheck: options.skipAccessibilityCheck
|
|
400
|
-
});
|
|
401
|
-
if (!result.valid) {
|
|
402
|
-
console.log(
|
|
403
|
-
`
|
|
404
|
-
${LOG_PREFIX.DONE} ${EMOJI.FAILURE} WU-2022: Feature accessibility validation failed`
|
|
405
|
-
);
|
|
406
|
-
die(
|
|
407
|
-
`\u274C FEATURE ACCESSIBILITY VALIDATION FAILED (WU-2022)
|
|
408
|
-
|
|
409
|
-
Cannot complete wu:done - UI feature accessibility not verified.
|
|
410
|
-
|
|
411
|
-
${result.errors.join("\n\n")}
|
|
412
|
-
|
|
413
|
-
This gate prevents "orphaned code" - features that exist but users cannot access.`
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
function validateDocsOnlyFlag(wu, args) {
|
|
418
|
-
if (!args.docsOnly) {
|
|
419
|
-
return { valid: true, errors: [] };
|
|
420
|
-
}
|
|
421
|
-
const wuId = wu.id || "unknown";
|
|
422
|
-
const exposure = wu.exposure;
|
|
423
|
-
const type = wu.type;
|
|
424
|
-
const codePaths = wu.code_paths;
|
|
425
|
-
if (exposure === WU_EXPOSURE.DOCUMENTATION) {
|
|
426
|
-
return { valid: true, errors: [] };
|
|
427
|
-
}
|
|
428
|
-
if (isDocumentationType(type)) {
|
|
429
|
-
return { valid: true, errors: [] };
|
|
430
|
-
}
|
|
431
|
-
const docsOnlyPrefixes = getDocsOnlyPrefixes().map((prefix) => prefix.toLowerCase());
|
|
432
|
-
const isDocsPath = (p) => {
|
|
433
|
-
const normalizedPath = p.trim().toLowerCase();
|
|
434
|
-
for (const prefix of docsOnlyPrefixes) {
|
|
435
|
-
if (normalizedPath.startsWith(prefix)) {
|
|
436
|
-
return true;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
if (normalizedPath.endsWith(".md")) {
|
|
440
|
-
return true;
|
|
441
|
-
}
|
|
442
|
-
for (const pattern of DOCS_ONLY_ROOT_FILES) {
|
|
443
|
-
if (normalizedPath.startsWith(pattern)) {
|
|
444
|
-
return true;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return false;
|
|
448
|
-
};
|
|
449
|
-
if (codePaths && Array.isArray(codePaths) && codePaths.length > 0) {
|
|
450
|
-
const allDocsOnly = codePaths.every((p) => typeof p === "string" && isDocsPath(p));
|
|
451
|
-
if (allDocsOnly) {
|
|
452
|
-
return { valid: true, errors: [] };
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
const currentExposure = exposure || "not set";
|
|
456
|
-
const currentType = type || "not set";
|
|
457
|
-
return {
|
|
458
|
-
valid: false,
|
|
459
|
-
errors: [
|
|
460
|
-
`--docs-only flag used on ${wuId} but WU is not documentation-focused.
|
|
461
|
-
|
|
462
|
-
Current exposure: ${currentExposure}
|
|
463
|
-
Current type: ${currentType}
|
|
464
|
-
|
|
465
|
-
--docs-only requires one of:
|
|
466
|
-
1. exposure: documentation
|
|
467
|
-
2. type: documentation
|
|
468
|
-
3. All code_paths under configured docs prefixes (${docsOnlyPrefixes.join(", ")}), or *.md files
|
|
469
|
-
|
|
470
|
-
To fix, either:
|
|
471
|
-
- Remove --docs-only flag and run full gates
|
|
472
|
-
- Change WU exposure to 'documentation' if this is truly a docs-only change`
|
|
473
|
-
]
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
function buildGatesCommand(options) {
|
|
477
|
-
const { docsOnly = false, isDocsOnly = false } = options;
|
|
478
|
-
if (docsOnly || isDocsOnly) {
|
|
479
|
-
return `${PKG_MANAGER} ${SCRIPTS.GATES} -- ${CLI_FLAGS.DOCS_ONLY}`;
|
|
480
|
-
}
|
|
481
|
-
return `${PKG_MANAGER} ${SCRIPTS.GATES}`;
|
|
482
|
-
}
|
|
483
|
-
function shouldEnforceWuBriefEvidence(doc) {
|
|
484
|
-
return doc.type === WU_TYPES.FEATURE || doc.type === WU_TYPES.BUG;
|
|
485
|
-
}
|
|
486
|
-
function isWuBriefPolicyMode(value) {
|
|
487
|
-
return typeof value === "string" && WU_BRIEF_POLICY_MODES.includes(value);
|
|
488
|
-
}
|
|
489
|
-
function resolveWuBriefPolicyMode(config = getConfig()) {
|
|
490
|
-
const configured = config.wu?.brief?.policyMode;
|
|
491
|
-
if (isWuBriefPolicyMode(configured)) {
|
|
492
|
-
return configured;
|
|
493
|
-
}
|
|
494
|
-
return DEFAULT_WU_BRIEF_POLICY_MODE;
|
|
495
|
-
}
|
|
496
|
-
function resolveWuBriefFreshnessMinutes(config = getConfig()) {
|
|
497
|
-
const configured = config.wu?.brief?.freshnessMinutes;
|
|
498
|
-
if (typeof configured === "number" && Number.isFinite(configured)) {
|
|
499
|
-
if (configured <= 0) {
|
|
500
|
-
return null;
|
|
501
|
-
}
|
|
502
|
-
return Math.floor(configured);
|
|
503
|
-
}
|
|
504
|
-
return DEFAULT_WU_BRIEF_FRESHNESS_MINUTES;
|
|
505
|
-
}
|
|
506
|
-
function buildMissingWuBriefEvidenceMessage(id) {
|
|
507
|
-
return `Missing wu:brief evidence for ${id}.
|
|
508
|
-
|
|
509
|
-
Completion policy requires an auditable wu:brief execution record for feature/bug WUs.
|
|
510
|
-
|
|
511
|
-
Fix options:
|
|
512
|
-
1. If you are delegating this WU, generate handoff prompt + evidence:
|
|
513
|
-
pnpm wu:brief --id ${id}
|
|
514
|
-
2. If you are implementing this WU yourself, record evidence only:
|
|
515
|
-
pnpm wu:brief --id ${id} --evidence-only
|
|
516
|
-
3. Retry completion:
|
|
517
|
-
pnpm wu:done --id ${id}
|
|
518
|
-
4. Legacy/manual override (audited):
|
|
519
|
-
pnpm wu:done --id ${id} --force`;
|
|
520
|
-
}
|
|
521
|
-
function buildMissingWuBriefEvidenceMessageForPrep(id, mode) {
|
|
522
|
-
return `Missing wu:brief evidence for ${id} (policy=${mode}).
|
|
523
|
-
|
|
524
|
-
wu:prep enforces wu:brief evidence when policy mode is required.
|
|
525
|
-
|
|
526
|
-
Fix options:
|
|
527
|
-
1. Record evidence by generating a brief prompt:
|
|
528
|
-
pnpm wu:brief --id ${id}
|
|
529
|
-
2. If self-implementing, record evidence only:
|
|
530
|
-
pnpm wu:brief --id ${id} --evidence-only
|
|
531
|
-
3. Retry prep:
|
|
532
|
-
pnpm wu:prep --id ${id}
|
|
533
|
-
4. Emergency audited bypass (requires explicit reason):
|
|
534
|
-
pnpm wu:prep --id ${id} --force --reason "<why bypass is required>"`;
|
|
535
|
-
}
|
|
536
|
-
function describeWuBriefFreshness(evidenceTimestamp, freshnessMinutes, now) {
|
|
537
|
-
const ageMinutes = getWuBriefEvidenceAgeMinutes(evidenceTimestamp, now);
|
|
538
|
-
if (ageMinutes === null) {
|
|
539
|
-
return `Evidence timestamp is invalid: ${evidenceTimestamp}`;
|
|
540
|
-
}
|
|
541
|
-
return `Evidence age is ${ageMinutes} minute(s); threshold is ${freshnessMinutes} minute(s).`;
|
|
542
|
-
}
|
|
543
|
-
function buildStaleWuBriefEvidenceMessageForPrep(options) {
|
|
544
|
-
return `Stale wu:brief evidence for ${options.id} (policy=${options.mode}).
|
|
545
|
-
|
|
546
|
-
${describeWuBriefFreshness(options.evidenceTimestamp, options.freshnessMinutes, options.now)}
|
|
547
|
-
|
|
548
|
-
Fix options:
|
|
549
|
-
1. Refresh evidence:
|
|
550
|
-
pnpm wu:brief --id ${options.id}
|
|
551
|
-
2. If self-implementing, refresh evidence only:
|
|
552
|
-
pnpm wu:brief --id ${options.id} --evidence-only
|
|
553
|
-
3. Retry prep:
|
|
554
|
-
pnpm wu:prep --id ${options.id}
|
|
555
|
-
4. Emergency audited bypass (requires explicit reason):
|
|
556
|
-
pnpm wu:prep --id ${options.id} --force --reason "<why stale evidence bypass is required>"`;
|
|
557
|
-
}
|
|
558
|
-
function buildStaleWuBriefEvidenceMessageForDone(options) {
|
|
559
|
-
return `Stale wu:brief evidence for ${options.id} (policy=${options.mode}).
|
|
560
|
-
|
|
561
|
-
${describeWuBriefFreshness(options.evidenceTimestamp, options.freshnessMinutes, options.now)}
|
|
562
|
-
|
|
563
|
-
Fix options:
|
|
564
|
-
1. Refresh evidence:
|
|
565
|
-
pnpm wu:brief --id ${options.id}
|
|
566
|
-
2. If self-implementing, refresh evidence only:
|
|
567
|
-
pnpm wu:brief --id ${options.id} --evidence-only
|
|
568
|
-
3. Retry completion:
|
|
569
|
-
pnpm wu:done --id ${options.id}
|
|
570
|
-
4. Legacy/manual override (audited):
|
|
571
|
-
pnpm wu:done --id ${options.id} --force`;
|
|
572
|
-
}
|
|
573
|
-
async function recordWuBriefPrepBypassAudit(options) {
|
|
574
|
-
const baseDir = options.baseDir ?? process.cwd();
|
|
575
|
-
const stateDir = resolveStateDir(baseDir);
|
|
576
|
-
const store = new WUStateStore(stateDir);
|
|
577
|
-
await store.checkpoint(
|
|
578
|
-
options.wuId,
|
|
579
|
-
`[wu:brief] prep force bypass accepted (${options.policyMode}): ${options.reason}`,
|
|
580
|
-
{
|
|
581
|
-
progress: "wu:brief policy bypass",
|
|
582
|
-
nextSteps: `policy=${options.policyMode}`
|
|
583
|
-
}
|
|
584
|
-
);
|
|
585
|
-
}
|
|
586
|
-
async function enforceWuBriefEvidenceForPrep(id, doc, options = {}) {
|
|
587
|
-
if (!shouldEnforceWuBriefEvidence(doc)) {
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
const mode = options.mode ?? resolveWuBriefPolicyMode();
|
|
591
|
-
if (mode === "off" || mode === "manual") {
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
const baseDir = options.baseDir ?? process.cwd();
|
|
595
|
-
const stateDir = resolveStateDir(baseDir);
|
|
596
|
-
const force = options.force === true;
|
|
597
|
-
const getBriefEvidenceFn = options.getBriefEvidenceFn ?? getLatestWuBriefEvidence;
|
|
598
|
-
const blocker = options.blocker ?? ((message) => die(message));
|
|
599
|
-
const warn = options.warn ?? console.warn;
|
|
600
|
-
const recordBypassAudit = options.recordBypassAudit ?? recordWuBriefPrepBypassAudit;
|
|
601
|
-
const freshnessMinutes = options.freshnessMinutes === void 0 ? resolveWuBriefFreshnessMinutes() : options.freshnessMinutes;
|
|
602
|
-
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
603
|
-
let evidence;
|
|
604
|
-
try {
|
|
605
|
-
evidence = await getBriefEvidenceFn(stateDir, id);
|
|
606
|
-
} catch (error) {
|
|
607
|
-
if (mode === "required" && !force) {
|
|
608
|
-
blocker(buildWuBriefEvidenceReadFailureMessage(id, stateDir, error));
|
|
609
|
-
return;
|
|
610
|
-
}
|
|
611
|
-
warn(
|
|
612
|
-
`${PREP_LOG_PREFIX} ${EMOJI.WARNING} Could not verify wu:brief evidence for ${id}: ${getErrorMessage(error)}`
|
|
613
|
-
);
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
if (evidence && freshnessMinutes !== null && isWuBriefEvidenceStale({ timestamp: evidence.timestamp, freshnessMinutes, now })) {
|
|
617
|
-
if (mode === "auto") {
|
|
618
|
-
warn(`${PREP_LOG_PREFIX} ${EMOJI.WARNING} wu:brief evidence stale for ${id} (policy=auto).`);
|
|
619
|
-
warn(
|
|
620
|
-
buildStaleWuBriefEvidenceMessageForPrep({
|
|
621
|
-
id,
|
|
622
|
-
mode,
|
|
623
|
-
evidenceTimestamp: evidence.timestamp,
|
|
624
|
-
freshnessMinutes,
|
|
625
|
-
now
|
|
626
|
-
})
|
|
627
|
-
);
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
if (!force) {
|
|
631
|
-
blocker(
|
|
632
|
-
buildStaleWuBriefEvidenceMessageForPrep({
|
|
633
|
-
id,
|
|
634
|
-
mode,
|
|
635
|
-
evidenceTimestamp: evidence.timestamp,
|
|
636
|
-
freshnessMinutes,
|
|
637
|
-
now
|
|
638
|
-
})
|
|
639
|
-
);
|
|
640
|
-
return;
|
|
641
|
-
}
|
|
642
|
-
const reason2 = typeof options.reason === "string" ? options.reason.trim() : "";
|
|
643
|
-
if (!reason2) {
|
|
644
|
-
blocker(PREP_FORCE_REASON_REQUIRED_MESSAGE);
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
await recordBypassAudit({ wuId: id, baseDir, reason: reason2, policyMode: mode });
|
|
648
|
-
warn(
|
|
649
|
-
`${PREP_LOG_PREFIX} ${EMOJI.WARNING} wu:brief policy override accepted for ${id} (policy=${mode}, reason="${reason2}").`
|
|
650
|
-
);
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
if (evidence) {
|
|
654
|
-
return;
|
|
655
|
-
}
|
|
656
|
-
if (mode === "auto") {
|
|
657
|
-
warn(`${PREP_LOG_PREFIX} ${EMOJI.WARNING} wu:brief evidence missing for ${id} (policy=auto).`);
|
|
658
|
-
warn(buildMissingWuBriefEvidenceMessageForPrep(id, mode));
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
if (!force) {
|
|
662
|
-
blocker(buildMissingWuBriefEvidenceMessageForPrep(id, mode));
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
const reason = typeof options.reason === "string" ? options.reason.trim() : "";
|
|
666
|
-
if (!reason) {
|
|
667
|
-
blocker(PREP_FORCE_REASON_REQUIRED_MESSAGE);
|
|
668
|
-
return;
|
|
669
|
-
}
|
|
670
|
-
await recordBypassAudit({ wuId: id, baseDir, reason, policyMode: mode });
|
|
671
|
-
warn(
|
|
672
|
-
`${PREP_LOG_PREFIX} ${EMOJI.WARNING} wu:brief policy override accepted for ${id} (policy=${mode}, reason="${reason}").`
|
|
673
|
-
);
|
|
674
|
-
}
|
|
675
|
-
function buildWuBriefEvidenceReadFailureMessage(id, stateDir, error) {
|
|
676
|
-
return `Could not verify wu:brief evidence for ${id}.
|
|
677
|
-
|
|
678
|
-
State path: ${stateDir}
|
|
679
|
-
Error: ${getErrorMessage(error)}
|
|
680
|
-
|
|
681
|
-
Fix options:
|
|
682
|
-
1. Repair/restore state store, then rerun wu:done
|
|
683
|
-
2. Use --force for audited override when recovery is not possible`;
|
|
684
|
-
}
|
|
685
|
-
async function enforceWuBriefEvidenceForDone(id, doc, options = {}) {
|
|
686
|
-
if (!shouldEnforceWuBriefEvidence(doc)) {
|
|
687
|
-
return;
|
|
688
|
-
}
|
|
689
|
-
const baseDir = options.baseDir ?? process.cwd();
|
|
690
|
-
const mode = options.mode ?? resolveWuBriefPolicyMode();
|
|
691
|
-
const force = options.force === true;
|
|
692
|
-
const freshnessMinutes = options.freshnessMinutes === void 0 ? resolveWuBriefFreshnessMinutes() : options.freshnessMinutes;
|
|
693
|
-
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
694
|
-
const stateDir = resolveStateDir(baseDir);
|
|
695
|
-
const getBriefEvidenceFn = options.getBriefEvidenceFn ?? getLatestWuBriefEvidence;
|
|
696
|
-
const blocker = options.blocker ?? ((message) => die(message));
|
|
697
|
-
const warn = options.warn ?? console.warn;
|
|
698
|
-
let evidence;
|
|
699
|
-
try {
|
|
700
|
-
evidence = await getBriefEvidenceFn(stateDir, id);
|
|
701
|
-
} catch (error) {
|
|
702
|
-
if (!force) {
|
|
703
|
-
blocker(buildWuBriefEvidenceReadFailureMessage(id, stateDir, error));
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
warn(
|
|
707
|
-
`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2132: brief evidence verification failed for ${id}, override accepted via --force`
|
|
708
|
-
);
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
if (evidence && freshnessMinutes !== null && isWuBriefEvidenceStale({ timestamp: evidence.timestamp, freshnessMinutes, now })) {
|
|
712
|
-
if (mode === "off" || mode === "manual") {
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
if (mode === "auto") {
|
|
716
|
-
warn(`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2289: stale wu:brief evidence for ${id}`);
|
|
717
|
-
warn(
|
|
718
|
-
buildStaleWuBriefEvidenceMessageForDone({
|
|
719
|
-
id,
|
|
720
|
-
mode,
|
|
721
|
-
evidenceTimestamp: evidence.timestamp,
|
|
722
|
-
freshnessMinutes,
|
|
723
|
-
now
|
|
724
|
-
})
|
|
725
|
-
);
|
|
726
|
-
return;
|
|
727
|
-
}
|
|
728
|
-
if (!force) {
|
|
729
|
-
blocker(
|
|
730
|
-
buildStaleWuBriefEvidenceMessageForDone({
|
|
731
|
-
id,
|
|
732
|
-
mode,
|
|
733
|
-
evidenceTimestamp: evidence.timestamp,
|
|
734
|
-
freshnessMinutes,
|
|
735
|
-
now
|
|
736
|
-
})
|
|
737
|
-
);
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
warn(
|
|
741
|
-
`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2289: stale brief evidence override accepted for ${id} via --force`
|
|
742
|
-
);
|
|
743
|
-
return;
|
|
744
|
-
}
|
|
745
|
-
if (evidence) {
|
|
746
|
-
return;
|
|
747
|
-
}
|
|
748
|
-
if (!force) {
|
|
749
|
-
blocker(buildMissingWuBriefEvidenceMessage(id));
|
|
750
|
-
return;
|
|
751
|
-
}
|
|
752
|
-
warn(
|
|
753
|
-
`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2132: brief evidence override accepted for ${id} via --force`
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
function shouldEnforceSpawnProvenance(doc) {
|
|
757
|
-
return typeof doc?.initiative === "string" && doc.initiative.trim().length > 0;
|
|
758
|
-
}
|
|
759
|
-
function buildMissingSpawnProvenanceMessage(id, initiativeId) {
|
|
760
|
-
return `Missing spawn provenance for initiative-governed WU ${id} (${initiativeId}).
|
|
761
|
-
|
|
762
|
-
This completion path enforces auditable delegation lineage for initiative work.
|
|
763
|
-
|
|
764
|
-
Fix options:
|
|
765
|
-
1. Re-run with --force for an audited override (legacy/manual workflow)
|
|
766
|
-
2. Register spawn lineage before completion (preferred):
|
|
767
|
-
pnpm wu:delegate --id ${id} --parent-wu WU-XXXX --client codex-cli
|
|
768
|
-
|
|
769
|
-
Then retry: pnpm wu:done --id ${id}`;
|
|
770
|
-
}
|
|
771
|
-
function buildMissingSpawnPickupEvidenceMessage(id, initiativeId) {
|
|
772
|
-
return `Missing pickup evidence for initiative-governed WU ${id} (${initiativeId}).
|
|
773
|
-
|
|
774
|
-
Delegation intent exists, but this WU has no claim-time pickup handshake.
|
|
775
|
-
Completion policy requires both intent and pickup evidence.
|
|
776
|
-
|
|
777
|
-
Fix options:
|
|
778
|
-
1. Re-run with --force for an audited override (legacy/manual claim)
|
|
779
|
-
2. Ensure future delegated work is picked up via wu:claim (records handshake automatically)
|
|
780
|
-
|
|
781
|
-
Then retry: pnpm wu:done --id ${id}`;
|
|
782
|
-
}
|
|
783
|
-
function buildMissingSpawnBriefAttestationMessage(id, initiativeId) {
|
|
784
|
-
return `Missing brief attestation for delegated WU ${id} (${initiativeId}).
|
|
785
|
-
|
|
786
|
-
Delegation provenance exists, but no attested wu:brief payload hash was stored.
|
|
787
|
-
Completion policy now requires auditable prompt-hash attestation.
|
|
788
|
-
|
|
789
|
-
Fix options:
|
|
790
|
-
1. Re-run delegation from parent WU using full wu:delegate output:
|
|
791
|
-
pnpm wu:delegate --id ${id} --parent-wu WU-XXXX --client codex-cli
|
|
792
|
-
2. Re-run with --force for audited legacy override
|
|
793
|
-
|
|
794
|
-
Then retry: pnpm wu:done --id ${id}`;
|
|
795
|
-
}
|
|
796
|
-
function buildSpawnBriefAttestationMismatchMessage(id, initiativeId) {
|
|
797
|
-
return `Spawn brief attestation mismatch for delegated WU ${id} (${initiativeId}).
|
|
798
|
-
|
|
799
|
-
Stored delegation hash could not be matched to wu:brief evidence for this WU.
|
|
800
|
-
This indicates the delegated prompt was modified, condensed, or not generated through wu:delegate.
|
|
801
|
-
|
|
802
|
-
Fix options:
|
|
803
|
-
1. Re-run delegation using full wu:delegate output for ${id}
|
|
804
|
-
2. Re-run with --force for audited legacy override
|
|
805
|
-
|
|
806
|
-
Then retry: pnpm wu:done --id ${id}`;
|
|
807
|
-
}
|
|
808
|
-
function hasSpawnPickupEvidence(spawnEntry) {
|
|
809
|
-
const pickedUpAt = typeof spawnEntry?.pickedUpAt === "string" && spawnEntry.pickedUpAt.trim().length > 0 ? spawnEntry.pickedUpAt : "";
|
|
810
|
-
const pickedUpBy = typeof spawnEntry?.pickedUpBy === "string" && spawnEntry.pickedUpBy.trim().length > 0 ? spawnEntry.pickedUpBy : "";
|
|
811
|
-
return pickedUpAt.length > 0 && pickedUpBy.length > 0;
|
|
812
|
-
}
|
|
813
|
-
function hasDelegationIntent(spawnEntry) {
|
|
814
|
-
return typeof spawnEntry?.intent === "string" && spawnEntry.intent.trim() === "delegation";
|
|
815
|
-
}
|
|
816
|
-
function hasSpawnBriefAttestation(spawnEntry) {
|
|
817
|
-
const attestation = spawnEntry?.briefAttestation;
|
|
818
|
-
if (!attestation) {
|
|
819
|
-
return false;
|
|
820
|
-
}
|
|
821
|
-
return attestation.algorithm === "sha256" && typeof attestation.promptHash === "string" && /^[a-f0-9]{64}$/.test(attestation.promptHash);
|
|
822
|
-
}
|
|
823
|
-
async function recordSpawnProvenanceOverride(id, doc, baseDir = process.cwd()) {
|
|
824
|
-
try {
|
|
825
|
-
const initiativeId = typeof doc?.initiative === "string" ? doc.initiative.trim() : "unknown";
|
|
826
|
-
const lane = typeof doc?.lane === "string" ? doc.lane : void 0;
|
|
827
|
-
const result = await createSignal(baseDir, {
|
|
828
|
-
message: `spawn-provenance override used for ${id} in ${initiativeId} via --force`,
|
|
829
|
-
wuId: id,
|
|
830
|
-
lane
|
|
831
|
-
});
|
|
832
|
-
if (result.success) {
|
|
833
|
-
console.log(
|
|
834
|
-
`${LOG_PREFIX.DONE} ${EMOJI.INFO} Spawn-provenance override recorded (${result.signal.id})`
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
} catch (err) {
|
|
838
|
-
console.warn(
|
|
839
|
-
`${LOG_PREFIX.DONE} ${EMOJI.WARNING} Could not record spawn-provenance override: ${getErrorMessage(err)}`
|
|
840
|
-
);
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
async function enforceSpawnProvenanceForDone(id, doc, options = {}) {
|
|
844
|
-
const initiativeId = typeof doc.initiative === "string" && doc.initiative.trim() ? doc.initiative.trim() : "unknown";
|
|
845
|
-
const enforceForInitiative = shouldEnforceSpawnProvenance(doc);
|
|
846
|
-
const baseDir = options.baseDir ?? process.cwd();
|
|
847
|
-
const force = options.force === true;
|
|
848
|
-
const stateDir = resolveStateDir(baseDir);
|
|
849
|
-
const store = new DelegationRegistryStore(stateDir);
|
|
850
|
-
await store.load();
|
|
851
|
-
const spawnEntry = store.getByTarget(id);
|
|
852
|
-
const enforceForDelegation = hasDelegationIntent(spawnEntry);
|
|
853
|
-
if (!enforceForInitiative && !enforceForDelegation) {
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
if (!spawnEntry) {
|
|
857
|
-
if (!force) {
|
|
858
|
-
die(buildMissingSpawnProvenanceMessage(id, initiativeId));
|
|
859
|
-
}
|
|
860
|
-
console.warn(
|
|
861
|
-
`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1599: spawn provenance override accepted for ${id} (${initiativeId}) via --force`
|
|
862
|
-
);
|
|
863
|
-
await recordSpawnProvenanceOverride(id, doc, baseDir);
|
|
864
|
-
return;
|
|
865
|
-
}
|
|
866
|
-
if (hasSpawnPickupEvidence(spawnEntry)) {
|
|
867
|
-
if (!hasSpawnBriefAttestation(spawnEntry)) {
|
|
868
|
-
if (!force) {
|
|
869
|
-
die(buildMissingSpawnBriefAttestationMessage(id, initiativeId));
|
|
870
|
-
}
|
|
871
|
-
console.warn(
|
|
872
|
-
`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2301: brief attestation override accepted for ${id} (${initiativeId}) via --force`
|
|
873
|
-
);
|
|
874
|
-
await recordSpawnProvenanceOverride(id, doc, baseDir);
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
const attestedPromptHash = spawnEntry.briefAttestation?.promptHash;
|
|
878
|
-
if (!attestedPromptHash) {
|
|
879
|
-
if (!force) {
|
|
880
|
-
die(buildMissingSpawnBriefAttestationMessage(id, initiativeId));
|
|
881
|
-
}
|
|
882
|
-
console.warn(
|
|
883
|
-
`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2301: missing prompt hash override accepted for ${id} (${initiativeId}) via --force`
|
|
884
|
-
);
|
|
885
|
-
await recordSpawnProvenanceOverride(id, doc, baseDir);
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
let hashMatchesEvidence;
|
|
889
|
-
try {
|
|
890
|
-
hashMatchesEvidence = await hasMatchingWuBriefEvidenceHash(stateDir, id, attestedPromptHash);
|
|
891
|
-
} catch (error) {
|
|
892
|
-
if (!force) {
|
|
893
|
-
die(
|
|
894
|
-
`Could not verify spawn brief attestation for ${id}.
|
|
895
|
-
|
|
896
|
-
State path: ${stateDir}
|
|
897
|
-
Error: ${getErrorMessage(error)}
|
|
898
|
-
|
|
899
|
-
Fix options:
|
|
900
|
-
1. Repair/restore state store, then rerun wu:done
|
|
901
|
-
2. Use --force for audited override when recovery is not possible`
|
|
902
|
-
);
|
|
903
|
-
}
|
|
904
|
-
console.warn(
|
|
905
|
-
`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2301: attestation verification failure override accepted for ${id} (${initiativeId}) via --force`
|
|
906
|
-
);
|
|
907
|
-
await recordSpawnProvenanceOverride(id, doc, baseDir);
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
if (hashMatchesEvidence) {
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
if (!force) {
|
|
914
|
-
die(buildSpawnBriefAttestationMismatchMessage(id, initiativeId));
|
|
915
|
-
}
|
|
916
|
-
console.warn(
|
|
917
|
-
`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-2301: attestation mismatch override accepted for ${id} (${initiativeId}) via --force`
|
|
918
|
-
);
|
|
919
|
-
await recordSpawnProvenanceOverride(id, doc, baseDir);
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
if (!force) {
|
|
923
|
-
die(buildMissingSpawnPickupEvidenceMessage(id, initiativeId));
|
|
924
|
-
}
|
|
925
|
-
console.warn(
|
|
926
|
-
`${LOG_PREFIX.DONE} ${EMOJI.WARNING} WU-1605: pickup evidence override accepted for ${id} (${initiativeId}) via --force`
|
|
927
|
-
);
|
|
928
|
-
await recordSpawnProvenanceOverride(id, doc, baseDir);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
// src/wu-code-path-coverage.ts
|
|
932
|
-
import { spawnSync } from "child_process";
|
|
933
|
-
var DEFAULT_BASE_REF = "main";
|
|
934
|
-
var DEFAULT_HEAD_REF = "HEAD";
|
|
935
|
-
var GIT_DIFF_STDIO = ["pipe", "pipe", "pipe"];
|
|
936
|
-
function isCodePathCoveredByChanges(options) {
|
|
937
|
-
return isCodePathCoveredByChangedFiles(options);
|
|
938
|
-
}
|
|
939
|
-
function findMissingCodePathCoverage2(options) {
|
|
940
|
-
return findMissingCodePathCoverage(options);
|
|
941
|
-
}
|
|
942
|
-
function checkCodePathCoverageBeforeGates(options) {
|
|
943
|
-
const {
|
|
944
|
-
codePaths = [],
|
|
945
|
-
cwd,
|
|
946
|
-
baseRef = DEFAULT_BASE_REF,
|
|
947
|
-
headRef = DEFAULT_HEAD_REF,
|
|
948
|
-
spawnSyncFn = spawnSync
|
|
949
|
-
} = options;
|
|
950
|
-
const scopedCodePaths = codePaths.filter((codePath) => typeof codePath === "string").map((codePath) => codePath.trim()).filter(Boolean);
|
|
951
|
-
if (scopedCodePaths.length === 0) {
|
|
952
|
-
return {
|
|
953
|
-
valid: true,
|
|
954
|
-
missingCodePaths: [],
|
|
955
|
-
changedFiles: []
|
|
956
|
-
};
|
|
957
|
-
}
|
|
958
|
-
const range = `${baseRef}...${headRef}`;
|
|
959
|
-
const diffResult = spawnSyncFn("git", ["diff", "--name-only", range], {
|
|
960
|
-
cwd,
|
|
961
|
-
encoding: "utf-8",
|
|
962
|
-
stdio: GIT_DIFF_STDIO
|
|
963
|
-
});
|
|
964
|
-
if ((diffResult.status ?? EXIT_CODES.ERROR) !== EXIT_CODES.SUCCESS) {
|
|
965
|
-
const stderrText = String(diffResult.stderr ?? "").trim();
|
|
966
|
-
const errorText = stderrText || (diffResult.error instanceof Error ? diffResult.error.message : "");
|
|
967
|
-
return {
|
|
968
|
-
valid: false,
|
|
969
|
-
missingCodePaths: scopedCodePaths,
|
|
970
|
-
changedFiles: [],
|
|
971
|
-
error: errorText || `git diff --name-only ${range} failed`
|
|
972
|
-
};
|
|
973
|
-
}
|
|
974
|
-
const changedFiles = String(diffResult.stdout ?? "").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
975
|
-
const missingCodePaths = findMissingCodePathCoverage2({
|
|
976
|
-
codePaths: scopedCodePaths,
|
|
977
|
-
changedFiles
|
|
978
|
-
});
|
|
979
|
-
return {
|
|
980
|
-
valid: missingCodePaths.length === 0,
|
|
981
|
-
missingCodePaths,
|
|
982
|
-
changedFiles
|
|
983
|
-
};
|
|
984
|
-
}
|
|
985
|
-
function formatCodePathCoverageFailure(options) {
|
|
986
|
-
const { wuId, missingCodePaths, changedFiles, error } = options;
|
|
987
|
-
const missingSection = missingCodePaths.map((filePath) => ` - ${filePath}`).join("\n");
|
|
988
|
-
const changedSection = changedFiles.length > 0 ? changedFiles.map((filePath) => ` - ${filePath}`).join("\n") : " - (none)";
|
|
989
|
-
const diffErrorSection = error ? `
|
|
990
|
-
Unable to evaluate branch diff:
|
|
991
|
-
${error}
|
|
992
|
-
` : "";
|
|
993
|
-
return `${EMOJI.FAILURE} code_paths preflight failed for ${wuId}.
|
|
994
|
-
${diffErrorSection}
|
|
995
|
-
The following code_paths are not modified on this branch (vs main):
|
|
996
|
-
${missingSection}
|
|
997
|
-
|
|
998
|
-
Changed files detected on branch:
|
|
999
|
-
${changedSection}
|
|
1000
|
-
|
|
1001
|
-
Fix options:
|
|
1002
|
-
1. Commit changes that touch each missing code_path
|
|
1003
|
-
2. Update WU scope to match actual branch work:
|
|
1004
|
-
pnpm wu:edit --id ${wuId} --replace-code-paths --code-paths "<path1>" --code-paths "<path2>"
|
|
1005
|
-
3. Re-run: pnpm wu:prep --id ${wuId}`;
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
// src/wu-prep.ts
|
|
1009
|
-
var { LOCATION_TYPES } = CONTEXT_VALIDATION;
|
|
1010
|
-
var TDD_EXCEPTION_MARKER = "tdd-exception:";
|
|
1011
|
-
var PREP_PREFIX = "[wu-prep]";
|
|
1012
|
-
var PREP_OPTIONS = {
|
|
1013
|
-
docsOnly: {
|
|
1014
|
-
name: "docsOnly",
|
|
1015
|
-
flags: "--docs-only",
|
|
1016
|
-
description: "Run docs-only gates (format, spec-linter)"
|
|
1017
|
-
},
|
|
1018
|
-
fullTests: {
|
|
1019
|
-
name: "fullTests",
|
|
1020
|
-
flags: "--full-tests",
|
|
1021
|
-
description: "Run full incremental test suite (disable tests.unit scoped execution)"
|
|
1022
|
-
},
|
|
1023
|
-
force: WU_OPTIONS.force,
|
|
1024
|
-
reason: WU_OPTIONS.reason
|
|
1025
|
-
};
|
|
1026
|
-
function resolveScopedUnitTestsForPrep(options) {
|
|
1027
|
-
if (options.fullTests) {
|
|
1028
|
-
return [];
|
|
1029
|
-
}
|
|
1030
|
-
const unitTestsRaw = options.tests?.unit;
|
|
1031
|
-
if (!Array.isArray(unitTestsRaw)) {
|
|
1032
|
-
return [];
|
|
1033
|
-
}
|
|
1034
|
-
return unitTestsRaw.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean);
|
|
1035
|
-
}
|
|
1036
|
-
function shouldEnforceTddDiffEvidence(doc) {
|
|
1037
|
-
return doc.type === WU_TYPES.FEATURE || doc.type === WU_TYPES.BUG;
|
|
1038
|
-
}
|
|
1039
|
-
function hasDocumentedTddException(notes) {
|
|
1040
|
-
if (typeof notes === "string") {
|
|
1041
|
-
return notes.toLowerCase().includes(TDD_EXCEPTION_MARKER);
|
|
1042
|
-
}
|
|
1043
|
-
if (Array.isArray(notes)) {
|
|
1044
|
-
return notes.some((entry) => typeof entry === "string" && hasDocumentedTddException(entry));
|
|
1045
|
-
}
|
|
1046
|
-
return false;
|
|
1047
|
-
}
|
|
1048
|
-
function normalizeDeclaredAutomatedTestPaths(tests) {
|
|
1049
|
-
const buckets = ["unit", "e2e", "integration"];
|
|
1050
|
-
const paths = [];
|
|
1051
|
-
for (const bucket of buckets) {
|
|
1052
|
-
const value = tests?.[bucket];
|
|
1053
|
-
if (!Array.isArray(value)) {
|
|
1054
|
-
continue;
|
|
1055
|
-
}
|
|
1056
|
-
for (const entry of value) {
|
|
1057
|
-
if (typeof entry !== "string") {
|
|
1058
|
-
continue;
|
|
1059
|
-
}
|
|
1060
|
-
const normalized = entry.trim();
|
|
1061
|
-
if (normalized.length === 0) {
|
|
1062
|
-
continue;
|
|
1063
|
-
}
|
|
1064
|
-
paths.push(normalized);
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
return paths;
|
|
1068
|
-
}
|
|
1069
|
-
function isTestFileFromDiff(filePath) {
|
|
1070
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
1071
|
-
return TEST_FILE_PATTERNS.some((pattern) => pattern.test(normalized));
|
|
1072
|
-
}
|
|
1073
|
-
function hasTddEvidenceInWorkingDiff(options) {
|
|
1074
|
-
const { changedFiles, declaredTestPaths } = options;
|
|
1075
|
-
if (changedFiles.some((filePath) => isTestFileFromDiff(filePath))) {
|
|
1076
|
-
return true;
|
|
1077
|
-
}
|
|
1078
|
-
for (const testPath of declaredTestPaths) {
|
|
1079
|
-
if (isCodePathCoveredByChangedFiles({ codePath: testPath, changedFiles })) {
|
|
1080
|
-
return true;
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
return false;
|
|
1084
|
-
}
|
|
1085
|
-
async function evaluateTddDiffEvidenceForPrep(options) {
|
|
1086
|
-
const { doc } = options;
|
|
1087
|
-
if (!shouldEnforceTddDiffEvidence(doc)) {
|
|
1088
|
-
return { valid: true, reason: "not-applicable", changedFiles: [] };
|
|
1089
|
-
}
|
|
1090
|
-
if (hasDocumentedTddException(doc.notes)) {
|
|
1091
|
-
return { valid: true, reason: "documented-exception", changedFiles: [] };
|
|
1092
|
-
}
|
|
1093
|
-
const resolveChangedFilesFn = options.resolveChangedFilesFn ?? resolveChangedFiles;
|
|
1094
|
-
const changedFilesResult = await resolveChangedFilesFn({ cwd: options.cwd });
|
|
1095
|
-
if (!changedFilesResult.ok) {
|
|
1096
|
-
return {
|
|
1097
|
-
valid: false,
|
|
1098
|
-
reason: "diff-unavailable",
|
|
1099
|
-
changedFiles: [],
|
|
1100
|
-
diffReason: changedFilesResult.reason
|
|
1101
|
-
};
|
|
1102
|
-
}
|
|
1103
|
-
const declaredTestPaths = normalizeDeclaredAutomatedTestPaths(doc.tests ?? {});
|
|
1104
|
-
const hasEvidence = hasTddEvidenceInWorkingDiff({
|
|
1105
|
-
changedFiles: changedFilesResult.files,
|
|
1106
|
-
declaredTestPaths
|
|
1107
|
-
});
|
|
1108
|
-
if (hasEvidence) {
|
|
1109
|
-
return {
|
|
1110
|
-
valid: true,
|
|
1111
|
-
reason: "test-diff-evidence-found",
|
|
1112
|
-
changedFiles: changedFilesResult.files,
|
|
1113
|
-
baseRef: changedFilesResult.baseRef,
|
|
1114
|
-
headRef: changedFilesResult.headRef
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
return {
|
|
1118
|
-
valid: false,
|
|
1119
|
-
reason: "missing-test-diff-evidence",
|
|
1120
|
-
changedFiles: changedFilesResult.files,
|
|
1121
|
-
baseRef: changedFilesResult.baseRef,
|
|
1122
|
-
headRef: changedFilesResult.headRef
|
|
1123
|
-
};
|
|
1124
|
-
}
|
|
1125
|
-
function formatTddDiffEvidenceFailure(options) {
|
|
1126
|
-
const { wuId, result } = options;
|
|
1127
|
-
if (result.reason === "diff-unavailable") {
|
|
1128
|
-
return `${EMOJI.FAILURE} TDD provenance check failed for ${wuId}.
|
|
1129
|
-
|
|
1130
|
-
Unable to evaluate branch diff for test evidence: ${result.diffReason || "unknown diff error"}
|
|
1131
|
-
|
|
1132
|
-
Fix options:
|
|
1133
|
-
1. Ensure base ref is available (origin/main or main)
|
|
1134
|
-
2. Retry: pnpm wu:prep --id ${wuId}`;
|
|
1135
|
-
}
|
|
1136
|
-
const changedSection = result.changedFiles.length > 0 ? result.changedFiles.map((filePath) => ` - ${filePath}`).join("\n") : " - (none)";
|
|
1137
|
-
return `${EMOJI.FAILURE} Missing TDD diff evidence for ${wuId}.
|
|
1138
|
-
|
|
1139
|
-
Feature/bug WUs must touch at least one automated test file before wu:prep.
|
|
1140
|
-
|
|
1141
|
-
Changed files (${result.baseRef || "main"}...${result.headRef || "HEAD"}):
|
|
1142
|
-
${changedSection}
|
|
1143
|
-
|
|
1144
|
-
Fix options:
|
|
1145
|
-
1. Add/update test files in this branch and retry wu:prep
|
|
1146
|
-
2. Add an explicit exception marker in WU notes when test edits are not expected:
|
|
1147
|
-
${TDD_EXCEPTION_MARKER} <reason>`;
|
|
1148
|
-
}
|
|
1149
|
-
function isPreExistingSpecLinterFailure(gateName) {
|
|
1150
|
-
const normalizedName = gateName.toLowerCase().replace(/[:-]/g, "");
|
|
1151
|
-
return normalizedName === "speclinter";
|
|
1152
|
-
}
|
|
1153
|
-
function formatSkipGatesCommand(options) {
|
|
1154
|
-
const { wuId, mainCheckout } = options;
|
|
1155
|
-
return `cd ${mainCheckout} && pnpm wu:done --id ${wuId} --skip-gates --reason "pre-existing on main" --fix-wu WU-XXXX`;
|
|
1156
|
-
}
|
|
1157
|
-
function defaultExecOnMain(mainCheckout) {
|
|
1158
|
-
return async (cmd) => {
|
|
1159
|
-
const distDir = path3.dirname(fileURLToPath(import.meta.url));
|
|
1160
|
-
const wuValidateDist = path3.join(distDir, "wu-validate.js");
|
|
1161
|
-
const shouldUseDistWuValidate = cmd.includes("wu:validate") && cmd.includes("--json") && existsSync2(wuValidateDist);
|
|
1162
|
-
if (shouldUseDistWuValidate) {
|
|
1163
|
-
const result2 = spawnSync2("node", [wuValidateDist, "--all", "--json"], {
|
|
1164
|
-
cwd: mainCheckout,
|
|
1165
|
-
encoding: "utf-8",
|
|
1166
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1167
|
-
});
|
|
1168
|
-
return {
|
|
1169
|
-
exitCode: result2.status ?? 1,
|
|
1170
|
-
stdout: result2.stdout ?? "",
|
|
1171
|
-
stderr: result2.stderr ?? ""
|
|
1172
|
-
};
|
|
1173
|
-
}
|
|
1174
|
-
const parts = cmd.split(/\s+/);
|
|
1175
|
-
const executable = parts[0];
|
|
1176
|
-
const args = parts.slice(1);
|
|
1177
|
-
const result = spawnSync2(executable, args, {
|
|
1178
|
-
cwd: mainCheckout,
|
|
1179
|
-
encoding: "utf-8",
|
|
1180
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
1181
|
-
});
|
|
1182
|
-
return {
|
|
1183
|
-
exitCode: result.status ?? 1,
|
|
1184
|
-
stdout: result.stdout ?? "",
|
|
1185
|
-
stderr: result.stderr ?? ""
|
|
1186
|
-
};
|
|
1187
|
-
};
|
|
1188
|
-
}
|
|
1189
|
-
function runScopedValidation({ wuId, cwd }) {
|
|
1190
|
-
const result = spawnSync2("pnpm", ["wu:validate", "--id", wuId], {
|
|
1191
|
-
cwd,
|
|
1192
|
-
encoding: "utf-8",
|
|
1193
|
-
stdio: "inherit"
|
|
1194
|
-
});
|
|
1195
|
-
return (result.status ?? 1) === EXIT_CODES.SUCCESS;
|
|
1196
|
-
}
|
|
1197
|
-
function parseSpecLinterReport(output) {
|
|
1198
|
-
try {
|
|
1199
|
-
const parsed = JSON.parse(output.trim());
|
|
1200
|
-
return parsed;
|
|
1201
|
-
} catch {
|
|
1202
|
-
return null;
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
function classifySpecLinterFailures(options) {
|
|
1206
|
-
const mainSet = new Set(options.mainInvalid);
|
|
1207
|
-
const worktreeSet = new Set(options.worktreeInvalid);
|
|
1208
|
-
const preExistingFailures = [...worktreeSet].filter((id) => mainSet.has(id));
|
|
1209
|
-
const newFailures = [...worktreeSet].filter((id) => !mainSet.has(id));
|
|
1210
|
-
return {
|
|
1211
|
-
hasPreExisting: preExistingFailures.length > 0,
|
|
1212
|
-
hasNewFailures: newFailures.length > 0,
|
|
1213
|
-
newFailures,
|
|
1214
|
-
preExistingFailures
|
|
1215
|
-
};
|
|
1216
|
-
}
|
|
1217
|
-
async function runSpecLinterJson(execOnCwd) {
|
|
1218
|
-
const result = await execOnCwd("pnpm --silent wu:validate --all --json");
|
|
1219
|
-
const report = parseSpecLinterReport(result.stdout);
|
|
1220
|
-
if (!report || !Array.isArray(report.invalid)) {
|
|
1221
|
-
return {
|
|
1222
|
-
invalidIds: [],
|
|
1223
|
-
error: "Failed to parse wu:validate JSON output."
|
|
1224
|
-
};
|
|
1225
|
-
}
|
|
1226
|
-
const invalidIds = report.invalid.map((item) => item?.wuId).filter((id) => typeof id === "string");
|
|
1227
|
-
return { invalidIds };
|
|
1228
|
-
}
|
|
1229
|
-
async function checkPreExistingFailures(options) {
|
|
1230
|
-
const {
|
|
1231
|
-
mainCheckout,
|
|
1232
|
-
execOnMain = defaultExecOnMain(mainCheckout),
|
|
1233
|
-
execOnWorktree = defaultExecOnMain(process.cwd())
|
|
1234
|
-
} = options;
|
|
1235
|
-
try {
|
|
1236
|
-
const worktreeResult = await runSpecLinterJson(execOnWorktree);
|
|
1237
|
-
const mainResult = await runSpecLinterJson(execOnMain);
|
|
1238
|
-
if (worktreeResult.error || mainResult.error) {
|
|
1239
|
-
return {
|
|
1240
|
-
hasPreExisting: false,
|
|
1241
|
-
hasNewFailures: false,
|
|
1242
|
-
newFailures: [],
|
|
1243
|
-
preExistingFailures: [],
|
|
1244
|
-
error: worktreeResult.error || mainResult.error
|
|
1245
|
-
};
|
|
1246
|
-
}
|
|
1247
|
-
const classification = classifySpecLinterFailures({
|
|
1248
|
-
mainInvalid: mainResult.invalidIds,
|
|
1249
|
-
worktreeInvalid: worktreeResult.invalidIds
|
|
1250
|
-
});
|
|
1251
|
-
return {
|
|
1252
|
-
...classification,
|
|
1253
|
-
error: void 0
|
|
1254
|
-
};
|
|
1255
|
-
} catch (error) {
|
|
1256
|
-
return {
|
|
1257
|
-
hasPreExisting: false,
|
|
1258
|
-
hasNewFailures: false,
|
|
1259
|
-
newFailures: [],
|
|
1260
|
-
preExistingFailures: [],
|
|
1261
|
-
error: error.message
|
|
1262
|
-
};
|
|
1263
|
-
}
|
|
1264
|
-
}
|
|
1265
|
-
function isBranchPrMode(doc) {
|
|
1266
|
-
return doc.claimed_mode === CLAIMED_MODES.BRANCH_PR;
|
|
1267
|
-
}
|
|
1268
|
-
function validateBranchPrBranch(options) {
|
|
1269
|
-
const { currentBranch, expectedBranch } = options;
|
|
1270
|
-
if (currentBranch === expectedBranch) {
|
|
1271
|
-
return { valid: true };
|
|
1272
|
-
}
|
|
1273
|
-
return {
|
|
1274
|
-
valid: false,
|
|
1275
|
-
error: `Current branch '${currentBranch}' does not match expected lane branch '${expectedBranch}'.
|
|
1276
|
-
|
|
1277
|
-
Switch to the lane branch first:
|
|
1278
|
-
git checkout ${expectedBranch}`
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
function formatBranchPrSuccessMessage(options) {
|
|
1282
|
-
const { wuId, laneBranch } = options;
|
|
1283
|
-
return `${PREP_PREFIX} ${EMOJI.SUCCESS} ${wuId}: Prep completed successfully!
|
|
1284
|
-
|
|
1285
|
-
${PREP_PREFIX} Gates passed on branch ${laneBranch}.
|
|
1286
|
-
|
|
1287
|
-
${PREP_PREFIX} Next steps for branch-pr mode:
|
|
1288
|
-
|
|
1289
|
-
1. Push the branch: git push origin ${laneBranch}
|
|
1290
|
-
2. Create a PR: gh pr create --base main --head ${laneBranch}
|
|
1291
|
-
3. After PR merge: pnpm wu:cleanup --id ${wuId}
|
|
1292
|
-
`;
|
|
1293
|
-
}
|
|
1294
|
-
function evaluatePrepMainMutationGuard(options) {
|
|
1295
|
-
return evaluateMainDirtyMutationGuard({
|
|
1296
|
-
commandName: "wu:prep",
|
|
1297
|
-
mainCheckout: options.mainCheckout,
|
|
1298
|
-
mainStatus: options.mainStatus,
|
|
1299
|
-
hasActiveWorktreeContext: !options.isBranchPr,
|
|
1300
|
-
isBranchPrMode: options.isBranchPr
|
|
1301
|
-
});
|
|
1302
|
-
}
|
|
1303
|
-
function printSuccessMessage(wuId, mainCheckout) {
|
|
1304
|
-
console.log("");
|
|
1305
|
-
console.log(`${PREP_PREFIX} ${EMOJI.SUCCESS} ${wuId}: Prep completed successfully!`);
|
|
1306
|
-
console.log("");
|
|
1307
|
-
console.log(`${PREP_PREFIX} Gates passed in worktree.`);
|
|
1308
|
-
console.log("");
|
|
1309
|
-
console.log(`${PREP_PREFIX} Next step - copy and paste this command:`);
|
|
1310
|
-
console.log("");
|
|
1311
|
-
console.log(` cd ${mainCheckout} && pnpm wu:done --id ${wuId}`);
|
|
1312
|
-
console.log("");
|
|
1313
|
-
}
|
|
1314
|
-
async function createPrepCheckpoint(params) {
|
|
1315
|
-
await createPreGatesCheckpoint(
|
|
1316
|
-
{
|
|
1317
|
-
wuId: params.wuId,
|
|
1318
|
-
worktreePath: params.worktreePath,
|
|
1319
|
-
branchName: params.branchName,
|
|
1320
|
-
gatesPassed: true
|
|
1321
|
-
},
|
|
1322
|
-
{}
|
|
1323
|
-
);
|
|
1324
|
-
}
|
|
1325
|
-
async function main() {
|
|
1326
|
-
const args = createWUParser({
|
|
1327
|
-
name: "wu-prep",
|
|
1328
|
-
description: "Prepare WU for completion (run gates in worktree)",
|
|
1329
|
-
options: [
|
|
1330
|
-
WU_OPTIONS.id,
|
|
1331
|
-
PREP_OPTIONS.docsOnly,
|
|
1332
|
-
PREP_OPTIONS.fullTests,
|
|
1333
|
-
PREP_OPTIONS.force,
|
|
1334
|
-
PREP_OPTIONS.reason
|
|
1335
|
-
],
|
|
1336
|
-
required: ["id"],
|
|
1337
|
-
allowPositionalId: true
|
|
1338
|
-
});
|
|
1339
|
-
const id = args.id.toUpperCase();
|
|
1340
|
-
if (!PATTERNS.WU_ID.test(id)) {
|
|
1341
|
-
die(`Invalid WU id '${args.id}'. Expected format WU-123`);
|
|
1342
|
-
}
|
|
1343
|
-
const location = await resolveLocation();
|
|
1344
|
-
const wuPath = WU_PATHS.WU(id);
|
|
1345
|
-
let doc;
|
|
1346
|
-
try {
|
|
1347
|
-
doc = readWU(wuPath, id);
|
|
1348
|
-
} catch (error) {
|
|
1349
|
-
die(
|
|
1350
|
-
`Failed to read WU ${id}: ${error.message}
|
|
1351
|
-
|
|
1352
|
-
Options:
|
|
1353
|
-
1. Check if WU file exists: ls -la ${wuPath}
|
|
1354
|
-
2. Validate YAML syntax: pnpm wu:validate --id ${id}`
|
|
1355
|
-
);
|
|
1356
|
-
}
|
|
1357
|
-
const branchPr = isBranchPrMode(doc);
|
|
1358
|
-
if (branchPr) {
|
|
1359
|
-
const expectedBranch = defaultBranchFrom(doc);
|
|
1360
|
-
const currentBranch = getCurrentBranch();
|
|
1361
|
-
if (!expectedBranch) {
|
|
1362
|
-
die(
|
|
1363
|
-
`${EMOJI.FAILURE} Cannot determine lane branch for ${id}.
|
|
1364
|
-
|
|
1365
|
-
Check that the WU has a valid lane and id in its YAML spec.`
|
|
1366
|
-
);
|
|
1367
|
-
}
|
|
1368
|
-
if (!currentBranch) {
|
|
1369
|
-
die(
|
|
1370
|
-
`${EMOJI.FAILURE} Cannot determine current git branch.
|
|
1371
|
-
|
|
1372
|
-
Ensure you are in a git repository.`
|
|
1373
|
-
);
|
|
1374
|
-
}
|
|
1375
|
-
const branchCheck = validateBranchPrBranch({
|
|
1376
|
-
currentBranch,
|
|
1377
|
-
expectedBranch
|
|
1378
|
-
});
|
|
1379
|
-
if (!branchCheck.valid) {
|
|
1380
|
-
die(`${EMOJI.FAILURE} ${branchCheck.error}`);
|
|
1381
|
-
}
|
|
1382
|
-
console.log(`${PREP_PREFIX} branch-pr mode detected for ${id}`);
|
|
1383
|
-
} else {
|
|
1384
|
-
if (location.type !== LOCATION_TYPES.WORKTREE) {
|
|
1385
|
-
die(
|
|
1386
|
-
`${EMOJI.FAILURE} wu:prep must be run from a worktree, not ${location.type}.
|
|
1387
|
-
|
|
1388
|
-
Current location: ${location.cwd}
|
|
1389
|
-
|
|
1390
|
-
If you have a worktree for ${id}, navigate to it first:
|
|
1391
|
-
cd worktrees/<lane>-${id.toLowerCase()}
|
|
1392
|
-
|
|
1393
|
-
If you don't have a worktree yet, claim the WU first:
|
|
1394
|
-
pnpm wu:claim --id ${id} --lane "<lane>"`
|
|
1395
|
-
);
|
|
1396
|
-
}
|
|
1397
|
-
if (location.worktreeWuId && location.worktreeWuId !== id) {
|
|
1398
|
-
console.warn(
|
|
1399
|
-
`${PREP_PREFIX} ${EMOJI.WARNING} Worktree is for ${location.worktreeWuId}, but you specified ${id}.`
|
|
1400
|
-
);
|
|
1401
|
-
console.warn(`${PREP_PREFIX} Proceeding with ${id} as specified.`);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
if (doc.status !== WU_STATUS.IN_PROGRESS) {
|
|
1405
|
-
die(
|
|
1406
|
-
`${EMOJI.FAILURE} WU ${id} status is '${doc.status}', expected '${WU_STATUS.IN_PROGRESS}'.
|
|
1407
|
-
|
|
1408
|
-
wu:prep can only be run on WUs that are in progress.`
|
|
1409
|
-
);
|
|
1410
|
-
}
|
|
1411
|
-
await enforceWuBriefEvidenceForPrep(id, doc, {
|
|
1412
|
-
baseDir: location.cwd,
|
|
1413
|
-
mode: resolveWuBriefPolicyMode(),
|
|
1414
|
-
freshnessMinutes: resolveWuBriefFreshnessMinutes(),
|
|
1415
|
-
force: Boolean(args.force),
|
|
1416
|
-
reason: typeof args.reason === "string" ? args.reason : void 0
|
|
1417
|
-
});
|
|
1418
|
-
if (!branchPr) {
|
|
1419
|
-
const mainStatus = await createGitForPath(location.mainCheckout).getStatus();
|
|
1420
|
-
const mainMutationGuard = evaluatePrepMainMutationGuard({
|
|
1421
|
-
mainCheckout: location.mainCheckout,
|
|
1422
|
-
isBranchPr: branchPr,
|
|
1423
|
-
mainStatus
|
|
1424
|
-
});
|
|
1425
|
-
if (mainMutationGuard.blocked) {
|
|
1426
|
-
die(mainMutationGuard.message ?? `${EMOJI.FAILURE} wu:prep blocked by dirty-main guard.`);
|
|
1427
|
-
}
|
|
1428
|
-
}
|
|
1429
|
-
console.log(`${PREP_PREFIX} Preparing ${id} for completion...`);
|
|
1430
|
-
console.log(`${PREP_PREFIX} Location: ${location.cwd}`);
|
|
1431
|
-
console.log(`${PREP_PREFIX} Main checkout: ${location.mainCheckout}`);
|
|
1432
|
-
console.log("");
|
|
1433
|
-
console.log(`${PREP_PREFIX} Running scoped validation for ${id}...`);
|
|
1434
|
-
const scopedOk = runScopedValidation({ wuId: id, cwd: location.cwd });
|
|
1435
|
-
if (!scopedOk) {
|
|
1436
|
-
die(
|
|
1437
|
-
`${EMOJI.FAILURE} Scoped validation failed for ${id}.
|
|
1438
|
-
|
|
1439
|
-
Fix the WU spec issues and rerun wu:prep.`
|
|
1440
|
-
);
|
|
1441
|
-
}
|
|
1442
|
-
console.log(`${PREP_PREFIX} Running reality preflight checks...`);
|
|
1443
|
-
const realityPreflight = await validatePreflight(id, {
|
|
1444
|
-
rootDir: location.cwd,
|
|
1445
|
-
worktreePath: location.cwd,
|
|
1446
|
-
phase: "reality"
|
|
1447
|
-
});
|
|
1448
|
-
if (!realityPreflight.valid) {
|
|
1449
|
-
die(formatPreflightResult(id, realityPreflight));
|
|
1450
|
-
}
|
|
1451
|
-
if (Array.isArray(realityPreflight.warnings) && realityPreflight.warnings.length > 0) {
|
|
1452
|
-
const warningLines = formatPreflightWarnings(
|
|
1453
|
-
realityPreflight.warnings,
|
|
1454
|
-
`${PREP_PREFIX} ${EMOJI.WARNING} Reality preflight warnings:`
|
|
1455
|
-
);
|
|
1456
|
-
for (const line of warningLines) {
|
|
1457
|
-
console.log(line.startsWith(" - ") ? `${PREP_PREFIX} ${line}` : line);
|
|
1458
|
-
}
|
|
1459
|
-
}
|
|
1460
|
-
const tddEvidenceResult = await evaluateTddDiffEvidenceForPrep({
|
|
1461
|
-
wuId: id,
|
|
1462
|
-
doc,
|
|
1463
|
-
cwd: location.cwd
|
|
1464
|
-
});
|
|
1465
|
-
if (!tddEvidenceResult.valid) {
|
|
1466
|
-
die(formatTddDiffEvidenceFailure({ wuId: id, result: tddEvidenceResult }));
|
|
1467
|
-
}
|
|
1468
|
-
console.log(`${PREP_PREFIX} Checking for pre-existing spec:linter failures on main...`);
|
|
1469
|
-
const preExistingCheck = await checkPreExistingFailures({
|
|
1470
|
-
mainCheckout: location.mainCheckout
|
|
1471
|
-
});
|
|
1472
|
-
const hasPreExistingOnly = preExistingCheck.hasPreExisting && !preExistingCheck.hasNewFailures;
|
|
1473
|
-
if (preExistingCheck.error) {
|
|
1474
|
-
console.log(
|
|
1475
|
-
`${PREP_PREFIX} ${EMOJI.WARNING} Unable to compare spec:linter results: ${preExistingCheck.error}`
|
|
1476
|
-
);
|
|
1477
|
-
} else if (hasPreExistingOnly) {
|
|
1478
|
-
console.log(`${PREP_PREFIX} ${EMOJI.WARNING} Pre-existing failures detected on main.`);
|
|
1479
|
-
process.on("exit", (code) => {
|
|
1480
|
-
if (code !== EXIT_CODES.SUCCESS) {
|
|
1481
|
-
console.log("");
|
|
1482
|
-
console.log(
|
|
1483
|
-
`${PREP_PREFIX} ${EMOJI.WARNING} Since failures are pre-existing on main, you can skip gates:`
|
|
1484
|
-
);
|
|
1485
|
-
console.log("");
|
|
1486
|
-
console.log(
|
|
1487
|
-
` ${formatSkipGatesCommand({ wuId: id, mainCheckout: location.mainCheckout })}`
|
|
1488
|
-
);
|
|
1489
|
-
console.log("");
|
|
1490
|
-
console.log(`${PREP_PREFIX} Replace WU-XXXX with the WU that will fix these spec issues.`);
|
|
1491
|
-
console.log("");
|
|
1492
|
-
}
|
|
1493
|
-
});
|
|
1494
|
-
} else if (preExistingCheck.hasNewFailures) {
|
|
1495
|
-
console.log(
|
|
1496
|
-
`${PREP_PREFIX} ${EMOJI.WARNING} New spec:linter failures detected in worktree: ${preExistingCheck.newFailures.join(
|
|
1497
|
-
", "
|
|
1498
|
-
)}`
|
|
1499
|
-
);
|
|
1500
|
-
} else {
|
|
1501
|
-
console.log(`${PREP_PREFIX} No pre-existing failures on main.`);
|
|
1502
|
-
}
|
|
1503
|
-
console.log("");
|
|
1504
|
-
const scopedUnitTests = resolveScopedUnitTestsForPrep({
|
|
1505
|
-
fullTests: args.fullTests,
|
|
1506
|
-
tests: doc.tests
|
|
1507
|
-
});
|
|
1508
|
-
if (args.fullTests) {
|
|
1509
|
-
console.log(
|
|
1510
|
-
`${PREP_PREFIX} --full-tests enabled; using default incremental/full test gate flow.`
|
|
1511
|
-
);
|
|
1512
|
-
} else if (scopedUnitTests.length > 0) {
|
|
1513
|
-
console.log(
|
|
1514
|
-
`${PREP_PREFIX} Using scoped tests from tests.unit (${scopedUnitTests.length} path${scopedUnitTests.length === 1 ? "" : "s"}).`
|
|
1515
|
-
);
|
|
1516
|
-
}
|
|
1517
|
-
console.log(`${PREP_PREFIX} Running gates...`);
|
|
1518
|
-
const gatesResult = await runGates({
|
|
1519
|
-
cwd: location.cwd,
|
|
1520
|
-
docsOnly: args.docsOnly,
|
|
1521
|
-
fullTests: args.fullTests,
|
|
1522
|
-
scopedTestPaths: scopedUnitTests
|
|
1523
|
-
});
|
|
1524
|
-
if (!gatesResult) {
|
|
1525
|
-
if (!hasPreExistingOnly) {
|
|
1526
|
-
die(`${EMOJI.FAILURE} Gates failed.
|
|
1527
|
-
|
|
1528
|
-
Fix the gate failures and run wu:prep again.`);
|
|
1529
|
-
}
|
|
1530
|
-
process.exit(EXIT_CODES.ERROR);
|
|
1531
|
-
}
|
|
1532
|
-
const branchName = defaultBranchFrom(doc) ?? getCurrentBranch() ?? "";
|
|
1533
|
-
await createPrepCheckpoint({ wuId: id, worktreePath: location.cwd, branchName });
|
|
1534
|
-
if (branchPr) {
|
|
1535
|
-
const laneBranch = defaultBranchFrom(doc) ?? "";
|
|
1536
|
-
const message = formatBranchPrSuccessMessage({ wuId: id, laneBranch });
|
|
1537
|
-
console.log("");
|
|
1538
|
-
console.log(message);
|
|
1539
|
-
} else {
|
|
1540
|
-
printSuccessMessage(id, location.mainCheckout);
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
if (import.meta.main) {
|
|
1544
|
-
void runCLI(main);
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
export {
|
|
1548
|
-
createPreGatesCheckpoint,
|
|
1549
|
-
markGatesPassed,
|
|
1550
|
-
clearCheckpoint,
|
|
1551
|
-
canSkipGates,
|
|
1552
|
-
evaluateMainDirtyMutationGuard,
|
|
1553
|
-
printExposureWarnings,
|
|
1554
|
-
validateAccessibilityOrDie,
|
|
1555
|
-
validateDocsOnlyFlag,
|
|
1556
|
-
buildGatesCommand,
|
|
1557
|
-
shouldEnforceWuBriefEvidence,
|
|
1558
|
-
buildMissingWuBriefEvidenceMessage,
|
|
1559
|
-
enforceWuBriefEvidenceForDone,
|
|
1560
|
-
shouldEnforceSpawnProvenance,
|
|
1561
|
-
buildMissingSpawnProvenanceMessage,
|
|
1562
|
-
buildMissingSpawnPickupEvidenceMessage,
|
|
1563
|
-
hasSpawnPickupEvidence,
|
|
1564
|
-
enforceSpawnProvenanceForDone,
|
|
1565
|
-
isCodePathCoveredByChanges,
|
|
1566
|
-
findMissingCodePathCoverage2 as findMissingCodePathCoverage,
|
|
1567
|
-
checkCodePathCoverageBeforeGates,
|
|
1568
|
-
formatCodePathCoverageFailure,
|
|
1569
|
-
resolveScopedUnitTestsForPrep,
|
|
1570
|
-
shouldEnforceTddDiffEvidence,
|
|
1571
|
-
hasDocumentedTddException,
|
|
1572
|
-
hasTddEvidenceInWorkingDiff,
|
|
1573
|
-
evaluateTddDiffEvidenceForPrep,
|
|
1574
|
-
isPreExistingSpecLinterFailure,
|
|
1575
|
-
formatSkipGatesCommand,
|
|
1576
|
-
classifySpecLinterFailures,
|
|
1577
|
-
checkPreExistingFailures,
|
|
1578
|
-
isBranchPrMode,
|
|
1579
|
-
validateBranchPrBranch,
|
|
1580
|
-
formatBranchPrSuccessMessage,
|
|
1581
|
-
evaluatePrepMainMutationGuard,
|
|
1582
|
-
createPrepCheckpoint,
|
|
1583
|
-
main
|
|
1584
|
-
};
|