@lumenflow/cli 3.17.7 → 3.18.1
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/README.md +44 -43
- 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/config-set.js +10 -1
- package/dist/config-set.js.map +1 -1
- package/dist/constants-7QAP3VQ4.js +23 -0
- package/dist/dist-IY3UUMWK.js +33 -0
- package/dist/gate-co-change.js +5 -2
- package/dist/gate-co-change.js.map +1 -1
- package/dist/init-detection.js +5 -3
- package/dist/init-detection.js.map +1 -1
- package/dist/init-templates.js +4 -4
- package/dist/init-templates.js.map +1 -1
- package/dist/initiative-edit.js +8 -3
- package/dist/initiative-edit.js.map +1 -1
- package/dist/initiative-plan.js +1 -1
- package/dist/initiative-plan.js.map +1 -1
- package/dist/invariants-runner-W5RGHCSU.js +27 -0
- package/dist/lane-lock-6J36HD5O.js +35 -0
- package/dist/lumenflow-upgrade.js +49 -0
- package/dist/lumenflow-upgrade.js.map +1 -1
- 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/pre-commit-check.js +1 -1
- package/dist/pre-commit-check.js.map +1 -1
- 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-done-pr-WLFFFEPJ.js +25 -0
- package/dist/wu-done-validation-3J5E36FE.js +30 -0
- package/dist/wu-duplicate-id-detector-5S7JHELK.js +232 -0
- package/dist/wu-edit-operations.js +4 -0
- package/dist/wu-edit-operations.js.map +1 -1
- package/dist/wu-edit-validators.js +4 -0
- package/dist/wu-edit-validators.js.map +1 -1
- package/dist/wu-edit.js +11 -0
- package/dist/wu-edit.js.map +1 -1
- package/dist/wu-spawn-strategy-resolver.js +13 -1
- package/dist/wu-spawn-strategy-resolver.js.map +1 -1
- package/package.json +8 -8
- package/packs/agent-runtime/.turbo/turbo-build.log +4 -0
- package/packs/agent-runtime/README.md +147 -0
- package/packs/agent-runtime/capability-factory.ts +104 -0
- package/packs/agent-runtime/config.schema.json +87 -0
- package/packs/agent-runtime/constants.ts +21 -0
- package/packs/agent-runtime/index.ts +11 -0
- package/packs/agent-runtime/manifest.ts +207 -0
- package/packs/agent-runtime/manifest.yaml +193 -0
- package/packs/agent-runtime/orchestration.ts +1787 -0
- package/packs/agent-runtime/pack-registration.ts +110 -0
- package/packs/agent-runtime/package.json +57 -0
- package/packs/agent-runtime/policy-factory.ts +165 -0
- package/packs/agent-runtime/tool-impl/agent-turn-tools.ts +793 -0
- package/packs/agent-runtime/tool-impl/index.ts +5 -0
- package/packs/agent-runtime/tool-impl/provider-adapters.ts +1245 -0
- package/packs/agent-runtime/tools/index.ts +4 -0
- package/packs/agent-runtime/tools/types.ts +47 -0
- package/packs/agent-runtime/tsconfig.json +20 -0
- package/packs/agent-runtime/types.ts +128 -0
- package/packs/agent-runtime/vitest.config.ts +11 -0
- package/packs/sidekick/.turbo/turbo-build.log +1 -1
- package/packs/sidekick/.turbo/turbo-test.log +12 -0
- package/packs/sidekick/.turbo/turbo-typecheck.log +4 -0
- package/packs/sidekick/package.json +1 -1
- package/packs/software-delivery/.turbo/turbo-build.log +1 -1
- package/packs/software-delivery/.turbo/turbo-typecheck.log +4 -0
- package/packs/software-delivery/package.json +1 -1
- package/templates/core/.lumenflow/rules/wu-workflow.md.template +1 -1
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +2 -2
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +1 -1
- package/templates/core/ai/onboarding/starting-prompt.md.template +1 -1
- package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +1 -1
|
@@ -0,0 +1,2485 @@
|
|
|
1
|
+
import {
|
|
2
|
+
scaffoldProject
|
|
3
|
+
} from "./chunk-A624LFLB.js";
|
|
4
|
+
import {
|
|
5
|
+
runLaneHealthCheck
|
|
6
|
+
} from "./chunk-XKY65P2T.js";
|
|
7
|
+
import {
|
|
8
|
+
LOG_TAIL_MAX_BYTES,
|
|
9
|
+
LOG_TAIL_MAX_LINES
|
|
10
|
+
} from "./chunk-Y4CQZY65.js";
|
|
11
|
+
import {
|
|
12
|
+
createWUParser,
|
|
13
|
+
runCLI,
|
|
14
|
+
validateClaimValidation
|
|
15
|
+
} from "./chunk-2GXVIN57.js";
|
|
16
|
+
import {
|
|
17
|
+
createGitForPath,
|
|
18
|
+
getGitForCwd
|
|
19
|
+
} from "./chunk-2UFQ3A3C.js";
|
|
20
|
+
import {
|
|
21
|
+
emitGateEvent,
|
|
22
|
+
getCurrentLane,
|
|
23
|
+
getCurrentWU,
|
|
24
|
+
syncNdjsonTelemetryToCloud
|
|
25
|
+
} from "./chunk-6XYXI2NQ.js";
|
|
26
|
+
import {
|
|
27
|
+
runInvariants
|
|
28
|
+
} from "./chunk-EZ6ZBYBM.js";
|
|
29
|
+
import {
|
|
30
|
+
readWURaw
|
|
31
|
+
} from "./chunk-NRIZR3A7.js";
|
|
32
|
+
import {
|
|
33
|
+
createWuPaths
|
|
34
|
+
} from "./chunk-6HO4GWJE.js";
|
|
35
|
+
import {
|
|
36
|
+
BRANCHES,
|
|
37
|
+
CACHE_STRATEGIES,
|
|
38
|
+
DIRECTORIES,
|
|
39
|
+
EMOJI,
|
|
40
|
+
ESLINT_COMMANDS,
|
|
41
|
+
ESLINT_DEFAULTS,
|
|
42
|
+
ESLINT_FLAGS,
|
|
43
|
+
EXIT_CODES,
|
|
44
|
+
FILE_SYSTEM,
|
|
45
|
+
GATE_COMMANDS,
|
|
46
|
+
GATE_NAMES,
|
|
47
|
+
GIT_REFS,
|
|
48
|
+
PACKAGES,
|
|
49
|
+
PKG_MANAGER,
|
|
50
|
+
PRETTIER_ARGS,
|
|
51
|
+
PRETTIER_FLAGS,
|
|
52
|
+
SCRIPTS,
|
|
53
|
+
STRING_LITERALS
|
|
54
|
+
} from "./chunk-DWMLTXKQ.js";
|
|
55
|
+
import {
|
|
56
|
+
CoChangeRuleConfigSchema,
|
|
57
|
+
DEFAULT_MIN_COVERAGE,
|
|
58
|
+
WORKSPACE_CONFIG_FILE_NAME,
|
|
59
|
+
WORKSPACE_V2_KEYS,
|
|
60
|
+
getGatesSection,
|
|
61
|
+
loadLaneHealthConfig,
|
|
62
|
+
resolveGatesCommands,
|
|
63
|
+
resolveTestPolicy,
|
|
64
|
+
resolveTestRunner
|
|
65
|
+
} from "./chunk-V6OJGLBA.js";
|
|
66
|
+
import {
|
|
67
|
+
ErrorCodes,
|
|
68
|
+
createError,
|
|
69
|
+
die
|
|
70
|
+
} from "./chunk-RXRKBBSM.js";
|
|
71
|
+
|
|
72
|
+
// src/gates.ts
|
|
73
|
+
import { writeSync as writeSync3 } from "fs";
|
|
74
|
+
|
|
75
|
+
// ../core/dist/gates-agent-mode.js
|
|
76
|
+
import path from "path";
|
|
77
|
+
import { existsSync, unlinkSync, symlinkSync } from "fs";
|
|
78
|
+
function shouldUseGatesAgentMode({ argv, env, stdout } = {}) {
|
|
79
|
+
const isVerbose = Array.isArray(argv) && argv.includes("--verbose");
|
|
80
|
+
if (isVerbose) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const hasClaudeProjectDir = Boolean(env?.CLAUDE_PROJECT_DIR);
|
|
84
|
+
if (hasClaudeProjectDir) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
const isCI = Boolean(env?.CI);
|
|
88
|
+
if (isCI) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const stdoutStream = stdout ?? process.stdout;
|
|
92
|
+
const isTTY = stdoutStream?.isTTY ?? false;
|
|
93
|
+
return !isTTY;
|
|
94
|
+
}
|
|
95
|
+
function getGatesLogDir({ cwd, env }) {
|
|
96
|
+
const configured = env?.LUMENFLOW_LOG_DIR;
|
|
97
|
+
return path.resolve(cwd, configured || ".logs");
|
|
98
|
+
}
|
|
99
|
+
function buildGatesLogPath({ cwd, env, wuId, lane, now = /* @__PURE__ */ new Date() }) {
|
|
100
|
+
const logDir = getGatesLogDir({ cwd, env });
|
|
101
|
+
const safeLane = (lane || "unknown").toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
102
|
+
const safeWu = (wuId || "unknown").toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
103
|
+
const stamp = now.toISOString().replace(/[:.]/g, "-");
|
|
104
|
+
return path.join(logDir, `gates-${safeLane}-${safeWu}-${stamp}.log`);
|
|
105
|
+
}
|
|
106
|
+
function getGatesLatestSymlinkPath({ cwd, env }) {
|
|
107
|
+
const logDir = getGatesLogDir({ cwd, env });
|
|
108
|
+
return path.join(logDir, "gates-latest.log");
|
|
109
|
+
}
|
|
110
|
+
function updateGatesLatestSymlink({ logPath, cwd, env }) {
|
|
111
|
+
const symlinkPath = getGatesLatestSymlinkPath({ cwd, env });
|
|
112
|
+
try {
|
|
113
|
+
if (existsSync(symlinkPath)) {
|
|
114
|
+
unlinkSync(symlinkPath);
|
|
115
|
+
}
|
|
116
|
+
const logDir = path.dirname(symlinkPath);
|
|
117
|
+
const relativePath = path.relative(logDir, logPath);
|
|
118
|
+
symlinkSync(relativePath, symlinkPath);
|
|
119
|
+
return true;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
var WU_DONE_PRE_COMMIT_GATE_DECISION_REASONS = {
|
|
125
|
+
SKIP_GATES_FLAG: "skip-gates-flag",
|
|
126
|
+
REUSE_STEP_ZERO: "reuse-step-zero",
|
|
127
|
+
REUSE_CHECKPOINT: "reuse-checkpoint",
|
|
128
|
+
RUN_REQUIRED: "run-required"
|
|
129
|
+
};
|
|
130
|
+
function resolveWuDonePreCommitGateDecision(input) {
|
|
131
|
+
if (input.skipGates) {
|
|
132
|
+
return {
|
|
133
|
+
runPreCommitFullSuite: false,
|
|
134
|
+
reason: WU_DONE_PRE_COMMIT_GATE_DECISION_REASONS.SKIP_GATES_FLAG,
|
|
135
|
+
message: "Pre-flight hook validation skipped because --skip-gates is active."
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
if (input.fullGatesRanInCurrentRun) {
|
|
139
|
+
return {
|
|
140
|
+
runPreCommitFullSuite: false,
|
|
141
|
+
reason: WU_DONE_PRE_COMMIT_GATE_DECISION_REASONS.REUSE_STEP_ZERO,
|
|
142
|
+
message: "Pre-flight hook validation reuses Step 0 gate results; duplicate full-suite run skipped."
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (input.skippedByCheckpoint) {
|
|
146
|
+
const checkpointSuffix = input.checkpointId ? ` (${input.checkpointId})` : "";
|
|
147
|
+
return {
|
|
148
|
+
runPreCommitFullSuite: false,
|
|
149
|
+
reason: WU_DONE_PRE_COMMIT_GATE_DECISION_REASONS.REUSE_CHECKPOINT,
|
|
150
|
+
message: `Pre-flight hook validation reuses checkpoint gate attestation${checkpointSuffix}; duplicate full-suite run skipped.`
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
runPreCommitFullSuite: true,
|
|
155
|
+
reason: WU_DONE_PRE_COMMIT_GATE_DECISION_REASONS.RUN_REQUIRED,
|
|
156
|
+
message: "No gate attestation found for this wu:done run; executing pre-flight hook gate suite."
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ../core/dist/coverage-gate.js
|
|
161
|
+
import { readFileSync, existsSync as existsSync2 } from "fs";
|
|
162
|
+
var COVERAGE_GATE_MODES = Object.freeze({
|
|
163
|
+
/** Log warnings but don't fail the gate */
|
|
164
|
+
WARN: "warn",
|
|
165
|
+
/** Fail the gate if thresholds not met */
|
|
166
|
+
BLOCK: "block"
|
|
167
|
+
});
|
|
168
|
+
var HEX_CORE_PATTERNS = Object.freeze([
|
|
169
|
+
"packages/@lumenflow/core/",
|
|
170
|
+
"packages/@lumenflow/cli/"
|
|
171
|
+
]);
|
|
172
|
+
var COVERAGE_THRESHOLD = DEFAULT_MIN_COVERAGE;
|
|
173
|
+
var DEFAULT_COVERAGE_PATH = "coverage/coverage-summary.json";
|
|
174
|
+
function isHexCoreFile(filePath) {
|
|
175
|
+
if (!filePath || typeof filePath !== "string") {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
179
|
+
return HEX_CORE_PATTERNS.some((pattern) => normalizedPath.includes(pattern));
|
|
180
|
+
}
|
|
181
|
+
function parseCoverageJson(coveragePath) {
|
|
182
|
+
if (!existsSync2(coveragePath)) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const content = readFileSync(coveragePath, { encoding: "utf-8" });
|
|
187
|
+
const data = JSON.parse(content);
|
|
188
|
+
const files = {};
|
|
189
|
+
for (const [key, value] of Object.entries(data)) {
|
|
190
|
+
if (key === "total")
|
|
191
|
+
continue;
|
|
192
|
+
files[key] = value;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
total: data.total,
|
|
196
|
+
files
|
|
197
|
+
};
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
function checkCoverageThresholds(coverageData, threshold) {
|
|
203
|
+
if (!coverageData || !coverageData.files) {
|
|
204
|
+
return { pass: true, failures: [] };
|
|
205
|
+
}
|
|
206
|
+
const effectiveThreshold = threshold ?? COVERAGE_THRESHOLD;
|
|
207
|
+
const failures = [];
|
|
208
|
+
for (const [file, metricsValue] of Object.entries(coverageData.files)) {
|
|
209
|
+
if (!isHexCoreFile(file)) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const metrics = metricsValue;
|
|
213
|
+
const linesCoverage = metrics.lines?.pct ?? 0;
|
|
214
|
+
if (linesCoverage < effectiveThreshold) {
|
|
215
|
+
failures.push({
|
|
216
|
+
file,
|
|
217
|
+
actual: linesCoverage,
|
|
218
|
+
threshold: effectiveThreshold,
|
|
219
|
+
metric: "lines"
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
pass: failures.length === 0,
|
|
225
|
+
failures
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
function formatCoverageDelta(coverageData) {
|
|
229
|
+
if (!coverageData) {
|
|
230
|
+
return "";
|
|
231
|
+
}
|
|
232
|
+
const lines = [];
|
|
233
|
+
const totalPct = coverageData.total?.lines?.pct ?? 0;
|
|
234
|
+
lines.push(`${STRING_LITERALS.NEWLINE}Coverage Summary: ${totalPct.toFixed(1)}% lines${STRING_LITERALS.NEWLINE}`);
|
|
235
|
+
const hexCoreFiles = Object.entries(coverageData.files || {}).filter(([file]) => isHexCoreFile(file));
|
|
236
|
+
if (hexCoreFiles.length > 0) {
|
|
237
|
+
lines.push("Hex Core Files:");
|
|
238
|
+
for (const [file, metricsValue] of hexCoreFiles) {
|
|
239
|
+
const metrics = metricsValue;
|
|
240
|
+
const pct = metrics.lines?.pct ?? 0;
|
|
241
|
+
const status = pct >= COVERAGE_THRESHOLD ? EMOJI.SUCCESS : EMOJI.FAILURE;
|
|
242
|
+
const shortFile = file.replace("packages/@lumenflow/", "");
|
|
243
|
+
lines.push(` ${status} ${shortFile}: ${pct.toFixed(1)}%`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return lines.join(STRING_LITERALS.NEWLINE);
|
|
247
|
+
}
|
|
248
|
+
async function runCoverageGate(options = {}) {
|
|
249
|
+
const start = Date.now();
|
|
250
|
+
const mode = options.mode || COVERAGE_GATE_MODES.WARN;
|
|
251
|
+
const coveragePath = options.coveragePath || DEFAULT_COVERAGE_PATH;
|
|
252
|
+
const logger = options.logger && typeof options.logger.log === "function" ? options.logger : console;
|
|
253
|
+
const threshold = options.threshold ?? COVERAGE_THRESHOLD;
|
|
254
|
+
const coverageData = parseCoverageJson(coveragePath);
|
|
255
|
+
if (!coverageData) {
|
|
256
|
+
const duration2 = Date.now() - start;
|
|
257
|
+
logger.log(`
|
|
258
|
+
${EMOJI.WARNING} Coverage gate: No coverage data found at ${coveragePath}`);
|
|
259
|
+
logger.log(" Run tests with coverage first: pnpm test:coverage\n");
|
|
260
|
+
return { ok: true, mode, duration: duration2, message: "No coverage data" };
|
|
261
|
+
}
|
|
262
|
+
const { pass, failures } = checkCoverageThresholds(coverageData, threshold);
|
|
263
|
+
const output = formatCoverageDelta(coverageData);
|
|
264
|
+
logger.log(output);
|
|
265
|
+
const duration = Date.now() - start;
|
|
266
|
+
if (!pass) {
|
|
267
|
+
logger.log(`
|
|
268
|
+
${EMOJI.FAILURE} Coverage below ${threshold}% for hex core files:`);
|
|
269
|
+
for (const failure of failures) {
|
|
270
|
+
const shortFile = failure.file.replace("packages/@lumenflow/", "");
|
|
271
|
+
logger.log(` - ${shortFile}: ${failure.actual.toFixed(1)}% (requires ${failure.threshold}%)`);
|
|
272
|
+
}
|
|
273
|
+
if (mode === COVERAGE_GATE_MODES.BLOCK) {
|
|
274
|
+
logger.log(`
|
|
275
|
+
${EMOJI.FAILURE} Coverage gate FAILED (mode: block)
|
|
276
|
+
`);
|
|
277
|
+
return { ok: false, mode, duration, message: "Coverage threshold not met" };
|
|
278
|
+
} else {
|
|
279
|
+
logger.log(`
|
|
280
|
+
${EMOJI.WARNING} Coverage gate WARNING (mode: warn)
|
|
281
|
+
`);
|
|
282
|
+
logger.log(" Note: This will become blocking in future. Fix coverage now.\n");
|
|
283
|
+
return { ok: true, mode, duration, message: "Coverage warning" };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
logger.log(`
|
|
287
|
+
${EMOJI.SUCCESS} Coverage gate passed
|
|
288
|
+
`);
|
|
289
|
+
return { ok: true, mode, duration, message: "Coverage OK" };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/gates-graceful-degradation.ts
|
|
293
|
+
import { readFileSync as readFileSync2, existsSync as existsSync3 } from "fs";
|
|
294
|
+
import path2 from "path";
|
|
295
|
+
var NON_SKIPPABLE_GATES = ["invariants"];
|
|
296
|
+
function checkScriptExists(scriptName, scripts) {
|
|
297
|
+
if (!scripts) return false;
|
|
298
|
+
return Object.prototype.hasOwnProperty.call(scripts, scriptName);
|
|
299
|
+
}
|
|
300
|
+
function loadPackageJsonScripts(projectRoot) {
|
|
301
|
+
const packageJsonPath = path2.join(projectRoot, "package.json");
|
|
302
|
+
if (!existsSync3(packageJsonPath)) return void 0;
|
|
303
|
+
try {
|
|
304
|
+
const content = readFileSync2(packageJsonPath, "utf8");
|
|
305
|
+
const pkg = JSON.parse(content);
|
|
306
|
+
return pkg.scripts;
|
|
307
|
+
} catch {
|
|
308
|
+
return void 0;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function buildMissingScriptWarning(scriptName) {
|
|
312
|
+
const suggestions = {
|
|
313
|
+
"format:check": '"format:check": "prettier --check ."',
|
|
314
|
+
lint: '"lint": "eslint ."',
|
|
315
|
+
typecheck: '"typecheck": "tsc --noEmit"',
|
|
316
|
+
"spec:linter": '"spec:linter": "node tools/spec-linter.js"'
|
|
317
|
+
};
|
|
318
|
+
const suggestion = suggestions[scriptName] ?? `"${scriptName}": "<your-command>"`;
|
|
319
|
+
return [
|
|
320
|
+
`Warning: "${scriptName}" script not found in package.json - skipping gate.`,
|
|
321
|
+
` To enable this gate, add to your package.json scripts:`,
|
|
322
|
+
` ${suggestion}`
|
|
323
|
+
].join("\n");
|
|
324
|
+
}
|
|
325
|
+
function resolveGateAction(gateName, scriptName, scripts, strict) {
|
|
326
|
+
if (NON_SKIPPABLE_GATES.includes(gateName)) {
|
|
327
|
+
return "run";
|
|
328
|
+
}
|
|
329
|
+
if (!scriptName) {
|
|
330
|
+
return "run";
|
|
331
|
+
}
|
|
332
|
+
if (checkScriptExists(scriptName, scripts)) {
|
|
333
|
+
return "run";
|
|
334
|
+
}
|
|
335
|
+
if (strict) {
|
|
336
|
+
return "fail";
|
|
337
|
+
}
|
|
338
|
+
return "skip";
|
|
339
|
+
}
|
|
340
|
+
function formatGateSummary(results) {
|
|
341
|
+
if (results.length === 0) {
|
|
342
|
+
return "No gates were executed.";
|
|
343
|
+
}
|
|
344
|
+
const passed = results.filter((r) => r.status === "passed");
|
|
345
|
+
const skipped = results.filter((r) => r.status === "skipped");
|
|
346
|
+
const failed = results.filter((r) => r.status === "failed");
|
|
347
|
+
const warned = results.filter((r) => r.status === "warned");
|
|
348
|
+
const lines = [];
|
|
349
|
+
lines.push("Gate Summary:");
|
|
350
|
+
lines.push("");
|
|
351
|
+
for (const result of results) {
|
|
352
|
+
const statusIcon = result.status === "passed" ? "PASS" : result.status === "skipped" ? "SKIP" : result.status === "warned" ? "WARN" : "FAIL";
|
|
353
|
+
const duration = result.durationMs > 0 ? ` (${result.durationMs}ms)` : "";
|
|
354
|
+
const reason = result.reason ? ` - ${result.reason}` : "";
|
|
355
|
+
lines.push(` [${statusIcon}] ${result.name}${duration}${reason}`);
|
|
356
|
+
}
|
|
357
|
+
lines.push("");
|
|
358
|
+
const parts = [];
|
|
359
|
+
if (passed.length > 0) parts.push(`${passed.length} passed`);
|
|
360
|
+
if (skipped.length > 0) parts.push(`${skipped.length} skipped`);
|
|
361
|
+
if (warned.length > 0) parts.push(`${warned.length} warned`);
|
|
362
|
+
if (failed.length > 0) parts.push(`${failed.length} failed`);
|
|
363
|
+
lines.push(parts.join(", "));
|
|
364
|
+
return lines.join("\n");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// src/gates.ts
|
|
368
|
+
import chalk from "chalk";
|
|
369
|
+
|
|
370
|
+
// src/gate-registry.ts
|
|
371
|
+
var GateRegistry = class {
|
|
372
|
+
gates = [];
|
|
373
|
+
nameIndex = /* @__PURE__ */ new Map();
|
|
374
|
+
/**
|
|
375
|
+
* Register a single gate definition.
|
|
376
|
+
*
|
|
377
|
+
* @param gate - Gate definition to register
|
|
378
|
+
* @throws Error if a gate with the same name is already registered
|
|
379
|
+
*/
|
|
380
|
+
register(gate) {
|
|
381
|
+
if (this.nameIndex.has(gate.name)) {
|
|
382
|
+
throw createError(
|
|
383
|
+
ErrorCodes.TOOL_ALREADY_REGISTERED,
|
|
384
|
+
`Gate "${gate.name}" is already registered`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
this.nameIndex.set(gate.name, this.gates.length);
|
|
388
|
+
this.gates.push(gate);
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Register multiple gate definitions at once.
|
|
392
|
+
*
|
|
393
|
+
* @param gates - Array of gate definitions to register
|
|
394
|
+
*/
|
|
395
|
+
registerAll(gates) {
|
|
396
|
+
for (const gate of gates) {
|
|
397
|
+
this.register(gate);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Get all registered gates in insertion order.
|
|
402
|
+
*
|
|
403
|
+
* @returns Copy of the gates array
|
|
404
|
+
*/
|
|
405
|
+
getAll() {
|
|
406
|
+
return [...this.gates];
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Get a gate by name.
|
|
410
|
+
*
|
|
411
|
+
* @param name - Gate name to look up
|
|
412
|
+
* @returns Gate definition or undefined if not found
|
|
413
|
+
*/
|
|
414
|
+
get(name) {
|
|
415
|
+
const index = this.nameIndex.get(name);
|
|
416
|
+
if (index === void 0) return void 0;
|
|
417
|
+
return this.gates[index];
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Check if a gate with the given name is registered.
|
|
421
|
+
*
|
|
422
|
+
* @param name - Gate name to check
|
|
423
|
+
* @returns true if the gate exists
|
|
424
|
+
*/
|
|
425
|
+
has(name) {
|
|
426
|
+
return this.nameIndex.has(name);
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Remove all registered gates.
|
|
430
|
+
*/
|
|
431
|
+
clear() {
|
|
432
|
+
this.gates.length = 0;
|
|
433
|
+
this.nameIndex.clear();
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// src/gates-runners.ts
|
|
438
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
439
|
+
import { writeSync as writeSync2 } from "fs";
|
|
440
|
+
import { access as access2 } from "fs/promises";
|
|
441
|
+
import { createRequire } from "module";
|
|
442
|
+
import path7 from "path";
|
|
443
|
+
|
|
444
|
+
// ../core/dist/incremental-lint.js
|
|
445
|
+
function ensureTrailingSlash(value) {
|
|
446
|
+
const normalized = value.replace(/\\/g, "/");
|
|
447
|
+
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
448
|
+
}
|
|
449
|
+
function getConfiguredWorktreesDir() {
|
|
450
|
+
return ensureTrailingSlash(createWuPaths({ projectRoot: process.cwd() }).WORKTREES_DIR());
|
|
451
|
+
}
|
|
452
|
+
var LINTABLE_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
|
|
453
|
+
var IGNORED_DIRECTORIES = [
|
|
454
|
+
"node_modules/",
|
|
455
|
+
".next/",
|
|
456
|
+
".expo/",
|
|
457
|
+
"dist/",
|
|
458
|
+
"build/",
|
|
459
|
+
".turbo/",
|
|
460
|
+
"coverage/"
|
|
461
|
+
];
|
|
462
|
+
function isLintableFile(filePath) {
|
|
463
|
+
const ignoredDirectories = [...IGNORED_DIRECTORIES, getConfiguredWorktreesDir()];
|
|
464
|
+
for (const ignored of ignoredDirectories) {
|
|
465
|
+
if (filePath.includes(ignored)) {
|
|
466
|
+
return false;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
for (const ext of LINTABLE_EXTENSIONS) {
|
|
470
|
+
if (filePath.endsWith(ext)) {
|
|
471
|
+
return true;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
function parseGitFileList(output) {
|
|
477
|
+
return output.split(STRING_LITERALS.NEWLINE).map((f) => f.trim()).filter((f) => f.length > 0);
|
|
478
|
+
}
|
|
479
|
+
async function getChangedLintableFiles(options = {}) {
|
|
480
|
+
const { git = getGitForCwd(), baseBranch = GIT_REFS.ORIGIN_MAIN, filterPath } = options;
|
|
481
|
+
const mergeBase = await git.mergeBase("HEAD", baseBranch);
|
|
482
|
+
const committedOutput = await git.raw(["diff", "--name-only", `${mergeBase}...HEAD`]);
|
|
483
|
+
const committedFiles = parseGitFileList(committedOutput);
|
|
484
|
+
const unstagedOutput = await git.raw(["diff", "--name-only"]);
|
|
485
|
+
const unstagedFiles = parseGitFileList(unstagedOutput);
|
|
486
|
+
const untrackedOutput = await git.raw(["ls-files", "--others", "--exclude-standard"]);
|
|
487
|
+
const untrackedFiles = parseGitFileList(untrackedOutput);
|
|
488
|
+
const allFiles = [.../* @__PURE__ */ new Set([...committedFiles, ...unstagedFiles, ...untrackedFiles])];
|
|
489
|
+
let lintableFiles = allFiles.filter(isLintableFile);
|
|
490
|
+
if (filterPath) {
|
|
491
|
+
lintableFiles = lintableFiles.filter((f) => f.startsWith(filterPath));
|
|
492
|
+
}
|
|
493
|
+
return lintableFiles;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ../core/dist/incremental-test.js
|
|
497
|
+
var VITEST_CHANGED_EXCLUDES = Object.freeze(["**/*.integration.*", "**/golden-*.test.*"]);
|
|
498
|
+
var CODE_FILE_EXTENSIONS = Object.freeze([
|
|
499
|
+
".ts",
|
|
500
|
+
".tsx",
|
|
501
|
+
".js",
|
|
502
|
+
".jsx",
|
|
503
|
+
".js",
|
|
504
|
+
".cjs",
|
|
505
|
+
".mts",
|
|
506
|
+
".cts"
|
|
507
|
+
]);
|
|
508
|
+
function isCodeFilePath(filePath) {
|
|
509
|
+
if (!filePath || typeof filePath !== "string") {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
const normalized = filePath.replace(/\\/g, "/").toLowerCase();
|
|
513
|
+
return CODE_FILE_EXTENSIONS.some((ext) => normalized.endsWith(ext));
|
|
514
|
+
}
|
|
515
|
+
function buildVitestChangedArgs(options = {}) {
|
|
516
|
+
const { baseBranch = GIT_REFS.ORIGIN_MAIN } = options;
|
|
517
|
+
const args = [
|
|
518
|
+
"--changed",
|
|
519
|
+
baseBranch,
|
|
520
|
+
"--run",
|
|
521
|
+
"--passWithNoTests",
|
|
522
|
+
"--maxWorkers=1",
|
|
523
|
+
"--teardownTimeout=30000"
|
|
524
|
+
];
|
|
525
|
+
for (const pattern of VITEST_CHANGED_EXCLUDES) {
|
|
526
|
+
args.push(`--exclude='${pattern}'`);
|
|
527
|
+
}
|
|
528
|
+
return args;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// ../core/dist/validators/backlog-sync.js
|
|
532
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync } from "fs";
|
|
533
|
+
import path3 from "path";
|
|
534
|
+
function extractWUIDsFromBacklog(content) {
|
|
535
|
+
const wuIds = [];
|
|
536
|
+
const pattern = /WU-\d+/gi;
|
|
537
|
+
let match;
|
|
538
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
539
|
+
const wuId = match[0].toUpperCase();
|
|
540
|
+
if (!wuIds.includes(wuId)) {
|
|
541
|
+
wuIds.push(wuId);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return wuIds;
|
|
545
|
+
}
|
|
546
|
+
function getWUIDsFromFiles(wuDir) {
|
|
547
|
+
if (!existsSync4(wuDir)) {
|
|
548
|
+
return [];
|
|
549
|
+
}
|
|
550
|
+
return readdirSync(wuDir).filter((f) => f.endsWith(".yaml")).map((f) => f.replace(".yaml", "").toUpperCase());
|
|
551
|
+
}
|
|
552
|
+
async function validateBacklogSync(options = {}) {
|
|
553
|
+
const { cwd = process.cwd() } = options;
|
|
554
|
+
const errors = [];
|
|
555
|
+
const warnings = [];
|
|
556
|
+
const paths = createWuPaths({ projectRoot: cwd });
|
|
557
|
+
const backlogPath = path3.join(cwd, paths.BACKLOG());
|
|
558
|
+
const wuDir = path3.join(cwd, paths.WU_DIR());
|
|
559
|
+
if (!existsSync4(backlogPath)) {
|
|
560
|
+
errors.push(`Backlog file not found: ${backlogPath}`);
|
|
561
|
+
return { valid: false, errors, warnings, wuCount: 0, backlogCount: 0 };
|
|
562
|
+
}
|
|
563
|
+
const wuIdsFromFiles = getWUIDsFromFiles(wuDir);
|
|
564
|
+
const backlogContent = readFileSync3(backlogPath, {
|
|
565
|
+
encoding: FILE_SYSTEM.UTF8
|
|
566
|
+
});
|
|
567
|
+
const wuIdsFromBacklog = extractWUIDsFromBacklog(backlogContent);
|
|
568
|
+
for (const wuId of wuIdsFromFiles) {
|
|
569
|
+
if (!wuIdsFromBacklog.includes(wuId)) {
|
|
570
|
+
errors.push(`${wuId} not found in backlog.md (exists as ${wuId}.yaml)`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
for (const wuId of wuIdsFromBacklog) {
|
|
574
|
+
if (!wuIdsFromFiles.includes(wuId)) {
|
|
575
|
+
warnings.push(`${wuId} referenced in backlog.md but ${wuId}.yaml not found`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return {
|
|
579
|
+
valid: errors.length === 0,
|
|
580
|
+
errors,
|
|
581
|
+
warnings,
|
|
582
|
+
wuCount: wuIdsFromFiles.length,
|
|
583
|
+
backlogCount: wuIdsFromBacklog.length
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ../core/dist/validators/supabase-docs-linter.js
|
|
588
|
+
import { existsSync as existsSync5 } from "fs";
|
|
589
|
+
import path4 from "path";
|
|
590
|
+
import { pathToFileURL } from "url";
|
|
591
|
+
async function runSupabaseDocsLinter(options = {}) {
|
|
592
|
+
const { cwd = process.cwd(), logger = console } = options;
|
|
593
|
+
const linterPath = path4.join(cwd, "packages", "linters", "supabase-docs-linter.js");
|
|
594
|
+
if (!existsSync5(linterPath)) {
|
|
595
|
+
return {
|
|
596
|
+
ok: true,
|
|
597
|
+
skipped: true,
|
|
598
|
+
message: "Supabase docs linter not found; skipping."
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
const moduleUrl = pathToFileURL(linterPath).href;
|
|
602
|
+
const module = await import(moduleUrl);
|
|
603
|
+
const runFn = module.runSupabaseDocsLinter ?? module.default;
|
|
604
|
+
if (typeof runFn !== "function") {
|
|
605
|
+
return {
|
|
606
|
+
ok: false,
|
|
607
|
+
skipped: false,
|
|
608
|
+
errors: ["Supabase docs linter does not export runSupabaseDocsLinter."]
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
const result = await runFn({ cwd, logger });
|
|
612
|
+
if (result && typeof result === "object" && "ok" in result) {
|
|
613
|
+
return {
|
|
614
|
+
ok: Boolean(result.ok),
|
|
615
|
+
skipped: Boolean(result.skipped),
|
|
616
|
+
message: result.message,
|
|
617
|
+
errors: result.errors
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
ok: true,
|
|
622
|
+
skipped: false,
|
|
623
|
+
message: "Supabase docs linter completed."
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// src/gates-utils.ts
|
|
628
|
+
import { execSync, spawnSync } from "child_process";
|
|
629
|
+
import { closeSync, mkdirSync, openSync, readSync, statSync, writeSync } from "fs";
|
|
630
|
+
import { access } from "fs/promises";
|
|
631
|
+
import path5 from "path";
|
|
632
|
+
function pnpmCmd(...parts) {
|
|
633
|
+
return `${PKG_MANAGER} ${parts.join(" ")}`;
|
|
634
|
+
}
|
|
635
|
+
function pnpmRun(script, ...args) {
|
|
636
|
+
const argsStr = args.length > 0 ? ` ${args.join(" ")}` : "";
|
|
637
|
+
return `${PKG_MANAGER} ${SCRIPTS.RUN} ${script}${argsStr}`;
|
|
638
|
+
}
|
|
639
|
+
function normalizePath(filePath) {
|
|
640
|
+
return filePath.replace(/\\/g, "/");
|
|
641
|
+
}
|
|
642
|
+
function getBasename(filePath) {
|
|
643
|
+
const normalized = normalizePath(filePath);
|
|
644
|
+
const parts = normalized.split("/");
|
|
645
|
+
return parts[parts.length - 1] || normalized;
|
|
646
|
+
}
|
|
647
|
+
function quoteShellArgs(files) {
|
|
648
|
+
return files.map((file) => `"${file}"`).join(" ");
|
|
649
|
+
}
|
|
650
|
+
var PRETTIER_NON_FILE_OUTPUT_MARKERS = [
|
|
651
|
+
"code style issues found",
|
|
652
|
+
"all matched files use prettier",
|
|
653
|
+
"checking formatting",
|
|
654
|
+
"is a symbolic link"
|
|
655
|
+
];
|
|
656
|
+
function isNonFilePrettierOutputLine(line) {
|
|
657
|
+
const normalizedLine = line.toLowerCase();
|
|
658
|
+
return PRETTIER_NON_FILE_OUTPUT_MARKERS.some((marker) => normalizedLine.includes(marker));
|
|
659
|
+
}
|
|
660
|
+
function parsePrettierListOutput(output) {
|
|
661
|
+
if (!output) return [];
|
|
662
|
+
return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).map((line) => line.replace(/^\[error\]\s*/i, "").trim()).filter((line) => !isNonFilePrettierOutputLine(line));
|
|
663
|
+
}
|
|
664
|
+
function buildPrettierWriteCommand(files) {
|
|
665
|
+
const quotedFiles = files.map((file) => `"${file}"`).join(" ");
|
|
666
|
+
const base = pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_FLAGS.WRITE);
|
|
667
|
+
return quotedFiles ? `${base} ${quotedFiles}` : base;
|
|
668
|
+
}
|
|
669
|
+
function buildPrettierCheckCommand(files) {
|
|
670
|
+
const filesArg = files.length > 0 ? quoteShellArgs(files) : ".";
|
|
671
|
+
return pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_ARGS.CHECK, filesArg);
|
|
672
|
+
}
|
|
673
|
+
function formatFormatCheckGuidance(files) {
|
|
674
|
+
if (!files.length) return [];
|
|
675
|
+
const command = buildPrettierWriteCommand(files);
|
|
676
|
+
return [
|
|
677
|
+
"",
|
|
678
|
+
"\u274C format:check failed",
|
|
679
|
+
"Fix with:",
|
|
680
|
+
` ${command}`,
|
|
681
|
+
"",
|
|
682
|
+
"Affected files:",
|
|
683
|
+
...files.map((file) => ` - ${file}`),
|
|
684
|
+
""
|
|
685
|
+
];
|
|
686
|
+
}
|
|
687
|
+
function collectPrettierListDifferent(cwd, files = []) {
|
|
688
|
+
const filesArg = files.length > 0 ? quoteShellArgs(files) : ".";
|
|
689
|
+
const cmd = pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_ARGS.LIST_DIFFERENT, filesArg);
|
|
690
|
+
const result = spawnSync(cmd, [], {
|
|
691
|
+
shell: true,
|
|
692
|
+
cwd,
|
|
693
|
+
encoding: FILE_SYSTEM.ENCODING
|
|
694
|
+
});
|
|
695
|
+
const output = `${result.stdout || ""}
|
|
696
|
+
${result.stderr || ""}`;
|
|
697
|
+
return parsePrettierListOutput(output);
|
|
698
|
+
}
|
|
699
|
+
function emitFormatCheckGuidance({
|
|
700
|
+
agentLog,
|
|
701
|
+
useAgentMode,
|
|
702
|
+
files,
|
|
703
|
+
cwd
|
|
704
|
+
}) {
|
|
705
|
+
const formattedFiles = collectPrettierListDifferent(cwd, files ?? []);
|
|
706
|
+
if (!formattedFiles.length) return;
|
|
707
|
+
const lines = formatFormatCheckGuidance(formattedFiles);
|
|
708
|
+
const logLine = useAgentMode && agentLog ? (line) => writeSync(agentLog.logFd, `${line}
|
|
709
|
+
`) : (line) => console.log(line);
|
|
710
|
+
for (const line of lines) {
|
|
711
|
+
logLine(line);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
function run(cmd, {
|
|
715
|
+
agentLog,
|
|
716
|
+
cwd = process.cwd()
|
|
717
|
+
} = {}) {
|
|
718
|
+
const start = Date.now();
|
|
719
|
+
if (!agentLog) {
|
|
720
|
+
console.log(`
|
|
721
|
+
> ${cmd}
|
|
722
|
+
`);
|
|
723
|
+
try {
|
|
724
|
+
execSync(cmd, {
|
|
725
|
+
stdio: "inherit",
|
|
726
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
727
|
+
cwd
|
|
728
|
+
});
|
|
729
|
+
return { ok: true, duration: Date.now() - start };
|
|
730
|
+
} catch {
|
|
731
|
+
return { ok: false, duration: Date.now() - start };
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
writeSync(agentLog.logFd, `
|
|
735
|
+
> ${cmd}
|
|
736
|
+
|
|
737
|
+
`);
|
|
738
|
+
const result = spawnSync(cmd, [], {
|
|
739
|
+
shell: true,
|
|
740
|
+
stdio: ["ignore", agentLog.logFd, agentLog.logFd],
|
|
741
|
+
cwd,
|
|
742
|
+
encoding: FILE_SYSTEM.ENCODING
|
|
743
|
+
});
|
|
744
|
+
return { ok: result.status === EXIT_CODES.SUCCESS, duration: Date.now() - start };
|
|
745
|
+
}
|
|
746
|
+
function makeGateLogger({ agentLog, useAgentMode }) {
|
|
747
|
+
return (line) => {
|
|
748
|
+
if (!useAgentMode) {
|
|
749
|
+
console.log(line);
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (agentLog) {
|
|
753
|
+
writeSync(agentLog.logFd, `${line}
|
|
754
|
+
`);
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
function readLogTail(logPath, { maxLines = LOG_TAIL_MAX_LINES, maxBytes = LOG_TAIL_MAX_BYTES } = {}) {
|
|
759
|
+
try {
|
|
760
|
+
const stats = statSync(logPath);
|
|
761
|
+
const startPos = Math.max(0, stats.size - maxBytes);
|
|
762
|
+
const bytesToRead = stats.size - startPos;
|
|
763
|
+
const fd = openSync(logPath, "r");
|
|
764
|
+
try {
|
|
765
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
766
|
+
readSync(fd, buffer, 0, bytesToRead, startPos);
|
|
767
|
+
const text = buffer.toString(FILE_SYSTEM.ENCODING);
|
|
768
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
769
|
+
return lines.slice(-maxLines).join("\n");
|
|
770
|
+
} finally {
|
|
771
|
+
closeSync(fd);
|
|
772
|
+
}
|
|
773
|
+
} catch {
|
|
774
|
+
return "";
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
function createAgentLogContext({
|
|
778
|
+
wuId,
|
|
779
|
+
lane,
|
|
780
|
+
cwd
|
|
781
|
+
}) {
|
|
782
|
+
const logPath = buildGatesLogPath({
|
|
783
|
+
cwd,
|
|
784
|
+
env: process.env,
|
|
785
|
+
wuId: wuId ?? void 0,
|
|
786
|
+
lane: lane ?? void 0
|
|
787
|
+
});
|
|
788
|
+
mkdirSync(path5.dirname(logPath), { recursive: true });
|
|
789
|
+
const logFd = openSync(logPath, "a");
|
|
790
|
+
const header = `# gates log
|
|
791
|
+
# lane: ${lane || "unknown"}
|
|
792
|
+
# wu: ${wuId || "unknown"}
|
|
793
|
+
# started: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
794
|
+
|
|
795
|
+
`;
|
|
796
|
+
writeSync(logFd, header);
|
|
797
|
+
process.on("exit", () => {
|
|
798
|
+
try {
|
|
799
|
+
closeSync(logFd);
|
|
800
|
+
} catch {
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
return { logPath, logFd };
|
|
804
|
+
}
|
|
805
|
+
async function filterExistingFiles(files, cwd = process.cwd()) {
|
|
806
|
+
const existingFiles = await Promise.all(
|
|
807
|
+
files.map(async (file) => {
|
|
808
|
+
const filePath = path5.isAbsolute(file) ? file : path5.resolve(cwd, file);
|
|
809
|
+
try {
|
|
810
|
+
await access(filePath);
|
|
811
|
+
return file;
|
|
812
|
+
} catch {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
})
|
|
816
|
+
);
|
|
817
|
+
return existingFiles.filter((file) => Boolean(file));
|
|
818
|
+
}
|
|
819
|
+
async function getChangedFilesForIncremental({
|
|
820
|
+
git,
|
|
821
|
+
baseBranch = GIT_REFS.ORIGIN_MAIN
|
|
822
|
+
}) {
|
|
823
|
+
const mergeBase = await git.mergeBase("HEAD", baseBranch);
|
|
824
|
+
const committedOutput = await git.raw(["diff", "--name-only", `${mergeBase}...HEAD`]);
|
|
825
|
+
const committedFiles = committedOutput.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
826
|
+
const unstagedOutput = await git.raw(["diff", "--name-only"]);
|
|
827
|
+
const unstagedFiles = unstagedOutput.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
828
|
+
const untrackedOutput = await git.raw(["ls-files", "--others", "--exclude-standard"]);
|
|
829
|
+
const untrackedFiles = untrackedOutput.split("\n").map((f) => f.trim()).filter(Boolean);
|
|
830
|
+
return [.../* @__PURE__ */ new Set([...committedFiles, ...unstagedFiles, ...untrackedFiles])];
|
|
831
|
+
}
|
|
832
|
+
function parseWUFromBranchName(branchName) {
|
|
833
|
+
if (!branchName) {
|
|
834
|
+
return null;
|
|
835
|
+
}
|
|
836
|
+
const match = branchName.match(/wu-(\d+)/i);
|
|
837
|
+
if (!match) {
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
return `WU-${match[1]}`.toUpperCase();
|
|
841
|
+
}
|
|
842
|
+
async function detectCurrentWUForCwd(cwd) {
|
|
843
|
+
const workingDir = cwd ?? process.cwd();
|
|
844
|
+
try {
|
|
845
|
+
const branch = await createGitForPath(workingDir).getCurrentBranch();
|
|
846
|
+
const parsed = parseWUFromBranchName(branch);
|
|
847
|
+
if (parsed) {
|
|
848
|
+
return parsed;
|
|
849
|
+
}
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
return getCurrentWU();
|
|
853
|
+
}
|
|
854
|
+
function extractPackageFromPath(codePath) {
|
|
855
|
+
if (!codePath || typeof codePath !== "string") {
|
|
856
|
+
return null;
|
|
857
|
+
}
|
|
858
|
+
const normalized = codePath.replace(/\\/g, "/");
|
|
859
|
+
if (normalized.startsWith("packages/")) {
|
|
860
|
+
const parts = normalized.slice("packages/".length).split("/");
|
|
861
|
+
if (parts[0]?.startsWith("@") && parts[1]) {
|
|
862
|
+
return `${parts[0]}/${parts[1]}`;
|
|
863
|
+
}
|
|
864
|
+
if (parts[0]) {
|
|
865
|
+
return parts[0];
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
function extractPackagesFromCodePaths(codePaths) {
|
|
871
|
+
if (!codePaths || !Array.isArray(codePaths) || codePaths.length === 0) {
|
|
872
|
+
return [];
|
|
873
|
+
}
|
|
874
|
+
const packages = /* @__PURE__ */ new Set();
|
|
875
|
+
for (const codePath of codePaths) {
|
|
876
|
+
const pkg = extractPackageFromPath(codePath);
|
|
877
|
+
if (pkg) {
|
|
878
|
+
packages.add(pkg);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return Array.from(packages);
|
|
882
|
+
}
|
|
883
|
+
function loadCurrentWUCodePaths(options = {}) {
|
|
884
|
+
const cwd = options.cwd ?? process.cwd();
|
|
885
|
+
const wuId = getCurrentWU();
|
|
886
|
+
if (!wuId) {
|
|
887
|
+
return [];
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
const wuPaths = createWuPaths({ projectRoot: cwd });
|
|
891
|
+
const wuYamlPath = wuPaths.WU(wuId);
|
|
892
|
+
const wuDoc = readWURaw(wuYamlPath);
|
|
893
|
+
if (wuDoc && Array.isArray(wuDoc.code_paths)) {
|
|
894
|
+
return wuDoc.code_paths.filter((p) => typeof p === "string");
|
|
895
|
+
}
|
|
896
|
+
} catch {
|
|
897
|
+
}
|
|
898
|
+
return [];
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// src/gates-plan-resolvers.ts
|
|
902
|
+
import { existsSync as existsSync6 } from "fs";
|
|
903
|
+
import path6 from "path";
|
|
904
|
+
var PRETTIER_CONFIG_FILES = /* @__PURE__ */ new Set([
|
|
905
|
+
".prettierrc",
|
|
906
|
+
".prettierrc.json",
|
|
907
|
+
".prettierrc.yaml",
|
|
908
|
+
".prettierrc.yml",
|
|
909
|
+
".prettierrc.js",
|
|
910
|
+
".prettierrc.cjs",
|
|
911
|
+
".prettierrc.ts",
|
|
912
|
+
"prettier.config.js",
|
|
913
|
+
"prettier.config.cjs",
|
|
914
|
+
"prettier.config.ts",
|
|
915
|
+
"prettier.config.mjs",
|
|
916
|
+
".prettierignore"
|
|
917
|
+
]);
|
|
918
|
+
var WORKSPACE_ARTIFACT_ROOT_PREFIXES = ["packages/", "apps/", "tools/"];
|
|
919
|
+
var DIST_ARTIFACT_ROOT_SUFFIX = "/dist";
|
|
920
|
+
var DIST_ARTIFACT_PATH_SEGMENT = "/dist/";
|
|
921
|
+
var TEST_CONFIG_BASENAMES = /* @__PURE__ */ new Set([
|
|
922
|
+
"turbo.json",
|
|
923
|
+
// Turborepo
|
|
924
|
+
"nx.json",
|
|
925
|
+
// Nx
|
|
926
|
+
"lerna.json",
|
|
927
|
+
// Lerna
|
|
928
|
+
"pnpm-lock.yaml",
|
|
929
|
+
"package-lock.json",
|
|
930
|
+
"yarn.lock",
|
|
931
|
+
"bun.lockb",
|
|
932
|
+
"package.json"
|
|
933
|
+
]);
|
|
934
|
+
var TEST_CONFIG_PATTERNS = [
|
|
935
|
+
/^vitest\.config\.(ts|mts|js|mjs|cjs)$/i,
|
|
936
|
+
/^jest\.config\.(ts|js|mjs|cjs|json)$/i,
|
|
937
|
+
/^\.mocharc\.(js|json|yaml|yml)$/i,
|
|
938
|
+
// eslint-disable-next-line security/detect-unsafe-regex -- static tsconfig pattern; no backtracking risk
|
|
939
|
+
/^tsconfig(\..+)?\.json$/i
|
|
940
|
+
];
|
|
941
|
+
function isPrettierConfigFile(filePath) {
|
|
942
|
+
if (!filePath) return false;
|
|
943
|
+
const basename = getBasename(filePath);
|
|
944
|
+
return PRETTIER_CONFIG_FILES.has(basename);
|
|
945
|
+
}
|
|
946
|
+
function trimTrailingPathSeparators(filePath) {
|
|
947
|
+
let trimmed = filePath;
|
|
948
|
+
while (trimmed.endsWith("/")) {
|
|
949
|
+
trimmed = trimmed.slice(0, -1);
|
|
950
|
+
}
|
|
951
|
+
return trimmed;
|
|
952
|
+
}
|
|
953
|
+
function isWorkspaceDistArtifactRoot(filePath) {
|
|
954
|
+
if (!filePath) {
|
|
955
|
+
return false;
|
|
956
|
+
}
|
|
957
|
+
const normalizedPath = trimTrailingPathSeparators(normalizePath(filePath));
|
|
958
|
+
if (!normalizedPath) {
|
|
959
|
+
return false;
|
|
960
|
+
}
|
|
961
|
+
const hasWorkspacePrefix = WORKSPACE_ARTIFACT_ROOT_PREFIXES.some(
|
|
962
|
+
(prefix) => normalizedPath.startsWith(prefix)
|
|
963
|
+
);
|
|
964
|
+
const isDistArtifactPath = normalizedPath.endsWith(DIST_ARTIFACT_ROOT_SUFFIX) || normalizedPath.includes(DIST_ARTIFACT_PATH_SEGMENT);
|
|
965
|
+
return hasWorkspacePrefix && isDistArtifactPath;
|
|
966
|
+
}
|
|
967
|
+
function filterFormatCheckChangedFiles(changedFiles) {
|
|
968
|
+
return changedFiles.filter((filePath) => !isWorkspaceDistArtifactRoot(filePath));
|
|
969
|
+
}
|
|
970
|
+
function filterExistingFormatCheckFiles(changedFiles, cwd) {
|
|
971
|
+
return changedFiles.filter((filePath) => {
|
|
972
|
+
const resolvedPath = path6.isAbsolute(filePath) ? filePath : path6.resolve(cwd, filePath);
|
|
973
|
+
return existsSync6(resolvedPath);
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
function isTestConfigFile(filePath) {
|
|
977
|
+
if (!filePath) return false;
|
|
978
|
+
const basename = getBasename(filePath);
|
|
979
|
+
if (TEST_CONFIG_BASENAMES.has(basename)) {
|
|
980
|
+
return true;
|
|
981
|
+
}
|
|
982
|
+
return TEST_CONFIG_PATTERNS.some((pattern) => pattern.test(basename));
|
|
983
|
+
}
|
|
984
|
+
function resolveFormatCheckPlan({
|
|
985
|
+
changedFiles,
|
|
986
|
+
fileListError = false,
|
|
987
|
+
cwd
|
|
988
|
+
}) {
|
|
989
|
+
const filteredChangedFiles = filterFormatCheckChangedFiles(changedFiles);
|
|
990
|
+
if (fileListError) {
|
|
991
|
+
return { mode: "full", files: [], reason: "file-list-error" };
|
|
992
|
+
}
|
|
993
|
+
if (filteredChangedFiles.some(isPrettierConfigFile)) {
|
|
994
|
+
return { mode: "full", files: [], reason: "prettier-config" };
|
|
995
|
+
}
|
|
996
|
+
const existingChangedFiles = cwd ? filterExistingFormatCheckFiles(filteredChangedFiles, cwd) : filteredChangedFiles;
|
|
997
|
+
if (existingChangedFiles.length === 0) {
|
|
998
|
+
return { mode: "skip", files: [] };
|
|
999
|
+
}
|
|
1000
|
+
return { mode: "incremental", files: existingChangedFiles };
|
|
1001
|
+
}
|
|
1002
|
+
function resolveLintPlan({
|
|
1003
|
+
isMainBranch,
|
|
1004
|
+
changedFiles
|
|
1005
|
+
}) {
|
|
1006
|
+
if (isMainBranch) {
|
|
1007
|
+
return { mode: "full", files: [] };
|
|
1008
|
+
}
|
|
1009
|
+
const lintTargets = changedFiles.filter((filePath) => {
|
|
1010
|
+
const normalized = normalizePath(filePath);
|
|
1011
|
+
return (normalized.startsWith("apps/") || normalized.startsWith("packages/")) && isLintableFile(normalized);
|
|
1012
|
+
});
|
|
1013
|
+
if (lintTargets.length === 0) {
|
|
1014
|
+
return { mode: "skip", files: [] };
|
|
1015
|
+
}
|
|
1016
|
+
return { mode: "incremental", files: lintTargets };
|
|
1017
|
+
}
|
|
1018
|
+
function resolveTestPlan({
|
|
1019
|
+
isMainBranch,
|
|
1020
|
+
hasUntrackedCode,
|
|
1021
|
+
hasConfigChange,
|
|
1022
|
+
fileListError
|
|
1023
|
+
}) {
|
|
1024
|
+
if (fileListError) {
|
|
1025
|
+
return { mode: "full", reason: "file-list-error" };
|
|
1026
|
+
}
|
|
1027
|
+
if (hasUntrackedCode) {
|
|
1028
|
+
return { mode: "full", reason: "untracked-code" };
|
|
1029
|
+
}
|
|
1030
|
+
if (hasConfigChange) {
|
|
1031
|
+
return { mode: "full", reason: "test-config" };
|
|
1032
|
+
}
|
|
1033
|
+
if (isMainBranch) {
|
|
1034
|
+
return { mode: "full" };
|
|
1035
|
+
}
|
|
1036
|
+
return { mode: "incremental" };
|
|
1037
|
+
}
|
|
1038
|
+
function resolveDocsOnlyTestPlan({ codePaths }) {
|
|
1039
|
+
const packages = extractPackagesFromCodePaths(codePaths);
|
|
1040
|
+
if (packages.length === 0) {
|
|
1041
|
+
return {
|
|
1042
|
+
mode: "skip",
|
|
1043
|
+
packages: [],
|
|
1044
|
+
reason: "no-code-packages"
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
return {
|
|
1048
|
+
mode: "filtered",
|
|
1049
|
+
packages
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
function formatDocsOnlySkipMessage(plan) {
|
|
1053
|
+
if (plan.mode === "skip") {
|
|
1054
|
+
return "\u{1F4DD} docs-only mode: skipping all tests (no code packages in code_paths)";
|
|
1055
|
+
}
|
|
1056
|
+
const packageList = plan.packages.join(", ");
|
|
1057
|
+
return `\u{1F4DD} docs-only mode: running tests only for packages in code_paths: ${packageList}`;
|
|
1058
|
+
}
|
|
1059
|
+
function resolveSpecLinterPlan(wuId) {
|
|
1060
|
+
if (wuId) {
|
|
1061
|
+
return {
|
|
1062
|
+
scopedWuId: wuId,
|
|
1063
|
+
runGlobal: false
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
1067
|
+
scopedWuId: null,
|
|
1068
|
+
runGlobal: true
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// src/gates-runners.ts
|
|
1073
|
+
var require2 = createRequire(import.meta.url);
|
|
1074
|
+
var micromatch = require2("micromatch");
|
|
1075
|
+
var CO_CHANGE_SEVERITY = {
|
|
1076
|
+
WARN: "warn",
|
|
1077
|
+
ERROR: "error",
|
|
1078
|
+
OFF: "off"
|
|
1079
|
+
};
|
|
1080
|
+
async function runFormatCheckGate({ agentLog, useAgentMode, cwd }) {
|
|
1081
|
+
const start = Date.now();
|
|
1082
|
+
const effectiveCwd = cwd ?? process.cwd();
|
|
1083
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
1084
|
+
let git;
|
|
1085
|
+
let isMainBranch;
|
|
1086
|
+
try {
|
|
1087
|
+
git = createGitForPath(effectiveCwd);
|
|
1088
|
+
const currentBranch = await git.getCurrentBranch();
|
|
1089
|
+
isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
logLine(`\u26A0\uFE0F Failed to determine branch for format check: ${error.message}`);
|
|
1092
|
+
const result2 = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog, cwd: effectiveCwd });
|
|
1093
|
+
return { ...result2, duration: Date.now() - start, fileCount: -1 };
|
|
1094
|
+
}
|
|
1095
|
+
if (isMainBranch) {
|
|
1096
|
+
logLine("\u{1F4CB} On main branch - running full format check");
|
|
1097
|
+
const result2 = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog, cwd: effectiveCwd });
|
|
1098
|
+
return { ...result2, duration: Date.now() - start, fileCount: -1 };
|
|
1099
|
+
}
|
|
1100
|
+
let changedFiles = [];
|
|
1101
|
+
let fileListError = false;
|
|
1102
|
+
try {
|
|
1103
|
+
changedFiles = await getChangedFilesForIncremental({ git });
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
fileListError = true;
|
|
1106
|
+
logLine(`\u26A0\uFE0F Failed to determine changed files for format check: ${error.message}`);
|
|
1107
|
+
}
|
|
1108
|
+
const plan = resolveFormatCheckPlan({ changedFiles, fileListError, cwd: effectiveCwd });
|
|
1109
|
+
if (plan.mode === "skip") {
|
|
1110
|
+
logLine("\n> format:check (incremental)\n");
|
|
1111
|
+
logLine("\u2705 No files changed - skipping format check");
|
|
1112
|
+
return { ok: true, duration: Date.now() - start, fileCount: 0, filesChecked: [] };
|
|
1113
|
+
}
|
|
1114
|
+
if (plan.mode === "full") {
|
|
1115
|
+
const reason = plan.reason === "prettier-config" ? " (prettier config changed)" : plan.reason === "file-list-error" ? " (file list unavailable)" : "";
|
|
1116
|
+
logLine(`\u{1F4CB} Running full format check${reason}`);
|
|
1117
|
+
const result2 = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog, cwd: effectiveCwd });
|
|
1118
|
+
return { ...result2, duration: Date.now() - start, fileCount: -1 };
|
|
1119
|
+
}
|
|
1120
|
+
const existingFiles = await filterExistingFiles(plan.files, effectiveCwd);
|
|
1121
|
+
if (existingFiles.length === 0) {
|
|
1122
|
+
logLine("\n> format:check (incremental)\n");
|
|
1123
|
+
logLine("\u2705 All changed files were deleted - skipping format check");
|
|
1124
|
+
return { ok: true, duration: Date.now() - start, fileCount: 0, filesChecked: [] };
|
|
1125
|
+
}
|
|
1126
|
+
logLine(`
|
|
1127
|
+
> format:check (incremental: ${existingFiles.length} files)
|
|
1128
|
+
`);
|
|
1129
|
+
const result = run(buildPrettierCheckCommand(existingFiles), { agentLog, cwd: effectiveCwd });
|
|
1130
|
+
return {
|
|
1131
|
+
...result,
|
|
1132
|
+
duration: Date.now() - start,
|
|
1133
|
+
fileCount: existingFiles.length,
|
|
1134
|
+
filesChecked: existingFiles
|
|
1135
|
+
};
|
|
1136
|
+
}
|
|
1137
|
+
async function runIncrementalLint({
|
|
1138
|
+
agentLog,
|
|
1139
|
+
cwd
|
|
1140
|
+
}) {
|
|
1141
|
+
const start = Date.now();
|
|
1142
|
+
const logLine = (line) => {
|
|
1143
|
+
if (!agentLog) {
|
|
1144
|
+
console.log(line);
|
|
1145
|
+
return;
|
|
1146
|
+
}
|
|
1147
|
+
writeSync2(agentLog.logFd, `${line}
|
|
1148
|
+
`);
|
|
1149
|
+
};
|
|
1150
|
+
try {
|
|
1151
|
+
const git = createGitForPath(cwd);
|
|
1152
|
+
const currentBranch = await git.getCurrentBranch();
|
|
1153
|
+
const isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
|
|
1154
|
+
if (isMainBranch) {
|
|
1155
|
+
logLine("\u{1F4CB} On main branch - running full lint");
|
|
1156
|
+
const result2 = run(pnpmCmd(SCRIPTS.LINT), { agentLog, cwd });
|
|
1157
|
+
return { ...result2, fileCount: -1 };
|
|
1158
|
+
}
|
|
1159
|
+
const changedFiles = await getChangedLintableFiles({ git });
|
|
1160
|
+
const plan = resolveLintPlan({ isMainBranch, changedFiles });
|
|
1161
|
+
if (plan.mode === "skip") {
|
|
1162
|
+
logLine("\n> ESLint (incremental)\n");
|
|
1163
|
+
logLine("\u2705 No lintable files changed - skipping lint");
|
|
1164
|
+
return { ok: true, duration: Date.now() - start, fileCount: 0 };
|
|
1165
|
+
}
|
|
1166
|
+
if (plan.mode === "full") {
|
|
1167
|
+
logLine("\u{1F4CB} Running full lint (incremental plan forced full)");
|
|
1168
|
+
const result2 = run(pnpmCmd(SCRIPTS.LINT), { agentLog, cwd });
|
|
1169
|
+
return { ...result2, fileCount: -1 };
|
|
1170
|
+
}
|
|
1171
|
+
const existingFiles = await filterExistingFiles(plan.files, cwd);
|
|
1172
|
+
if (existingFiles.length === 0) {
|
|
1173
|
+
logLine("\n> ESLint (incremental)\n");
|
|
1174
|
+
logLine("\u2705 All changed files were deleted - skipping lint");
|
|
1175
|
+
return { ok: true, duration: Date.now() - start, fileCount: 0 };
|
|
1176
|
+
}
|
|
1177
|
+
logLine(`
|
|
1178
|
+
> ESLint (incremental: ${existingFiles.length} files)
|
|
1179
|
+
`);
|
|
1180
|
+
logLine(`Files to lint:
|
|
1181
|
+
${existingFiles.join("\n ")}
|
|
1182
|
+
`);
|
|
1183
|
+
const result = spawnSync2(
|
|
1184
|
+
PKG_MANAGER,
|
|
1185
|
+
[
|
|
1186
|
+
ESLINT_COMMANDS.ESLINT,
|
|
1187
|
+
ESLINT_FLAGS.MAX_WARNINGS,
|
|
1188
|
+
ESLINT_DEFAULTS.MAX_WARNINGS,
|
|
1189
|
+
ESLINT_FLAGS.NO_WARN_IGNORED,
|
|
1190
|
+
ESLINT_FLAGS.CACHE,
|
|
1191
|
+
ESLINT_FLAGS.CACHE_STRATEGY,
|
|
1192
|
+
CACHE_STRATEGIES.CONTENT,
|
|
1193
|
+
ESLINT_FLAGS.CACHE_LOCATION,
|
|
1194
|
+
".eslintcache",
|
|
1195
|
+
ESLINT_FLAGS.PASS_ON_UNPRUNED,
|
|
1196
|
+
...existingFiles
|
|
1197
|
+
],
|
|
1198
|
+
agentLog ? {
|
|
1199
|
+
stdio: ["ignore", agentLog.logFd, agentLog.logFd],
|
|
1200
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
1201
|
+
cwd
|
|
1202
|
+
} : {
|
|
1203
|
+
stdio: "inherit",
|
|
1204
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
1205
|
+
cwd
|
|
1206
|
+
}
|
|
1207
|
+
);
|
|
1208
|
+
const duration = Date.now() - start;
|
|
1209
|
+
return {
|
|
1210
|
+
ok: result.status === EXIT_CODES.SUCCESS,
|
|
1211
|
+
duration,
|
|
1212
|
+
fileCount: existingFiles.length
|
|
1213
|
+
};
|
|
1214
|
+
} catch (error) {
|
|
1215
|
+
console.error(
|
|
1216
|
+
"\u26A0\uFE0F Incremental lint failed, falling back to full lint:",
|
|
1217
|
+
error.message
|
|
1218
|
+
);
|
|
1219
|
+
const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog, cwd });
|
|
1220
|
+
return { ...result, fileCount: -1 };
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
var DEFAULT_INCREMENTAL_BASE_BRANCH = GIT_REFS.ORIGIN_MAIN;
|
|
1224
|
+
function buildStableVitestIncrementalCommand(baseBranch = DEFAULT_INCREMENTAL_BASE_BRANCH) {
|
|
1225
|
+
return pnpmCmd("vitest", "run", ...buildVitestChangedArgs({ baseBranch }));
|
|
1226
|
+
}
|
|
1227
|
+
function resolveIncrementalTestCommand({
|
|
1228
|
+
testRunner,
|
|
1229
|
+
configuredIncrementalCommand,
|
|
1230
|
+
baseBranch = DEFAULT_INCREMENTAL_BASE_BRANCH
|
|
1231
|
+
}) {
|
|
1232
|
+
const normalizedConfiguredCommand = configuredIncrementalCommand?.trim();
|
|
1233
|
+
if (testRunner === "vitest") {
|
|
1234
|
+
if (!normalizedConfiguredCommand) {
|
|
1235
|
+
return buildStableVitestIncrementalCommand(baseBranch);
|
|
1236
|
+
}
|
|
1237
|
+
const isVitestChangedCommand = normalizedConfiguredCommand.includes("vitest") && normalizedConfiguredCommand.includes("--changed");
|
|
1238
|
+
if (isVitestChangedCommand) {
|
|
1239
|
+
return buildStableVitestIncrementalCommand(baseBranch);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return normalizedConfiguredCommand ?? null;
|
|
1243
|
+
}
|
|
1244
|
+
async function runChangedTests({
|
|
1245
|
+
agentLog,
|
|
1246
|
+
cwd,
|
|
1247
|
+
scopedTestPaths = []
|
|
1248
|
+
}) {
|
|
1249
|
+
const start = Date.now();
|
|
1250
|
+
const logLine = (line) => {
|
|
1251
|
+
if (!agentLog) {
|
|
1252
|
+
console.log(line);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
writeSync2(agentLog.logFd, `${line}
|
|
1256
|
+
`);
|
|
1257
|
+
};
|
|
1258
|
+
const gatesCommands = resolveGatesCommands(cwd);
|
|
1259
|
+
const testRunner = resolveTestRunner(cwd);
|
|
1260
|
+
const normalizedScopedTestPaths = scopedTestPaths.filter((testPath) => typeof testPath === "string").map((testPath) => testPath.trim()).filter(Boolean);
|
|
1261
|
+
try {
|
|
1262
|
+
if (normalizedScopedTestPaths.length > 0) {
|
|
1263
|
+
const testPathsArg = quoteShellArgs(normalizedScopedTestPaths);
|
|
1264
|
+
logLine(
|
|
1265
|
+
`
|
|
1266
|
+
> Running scoped tests from WU tests.unit (${normalizedScopedTestPaths.length})
|
|
1267
|
+
`
|
|
1268
|
+
);
|
|
1269
|
+
const result2 = run(pnpmCmd("vitest", "run", testPathsArg, "--passWithNoTests"), {
|
|
1270
|
+
agentLog,
|
|
1271
|
+
cwd
|
|
1272
|
+
});
|
|
1273
|
+
return { ...result2, duration: Date.now() - start, isIncremental: true };
|
|
1274
|
+
}
|
|
1275
|
+
const git = createGitForPath(cwd);
|
|
1276
|
+
const currentBranch = await git.getCurrentBranch();
|
|
1277
|
+
const isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
|
|
1278
|
+
if (isMainBranch) {
|
|
1279
|
+
logLine("\u{1F4CB} On main branch - running full test suite");
|
|
1280
|
+
const result2 = run(gatesCommands.test_full, { agentLog, cwd });
|
|
1281
|
+
return { ...result2, isIncremental: false };
|
|
1282
|
+
}
|
|
1283
|
+
let changedFiles = [];
|
|
1284
|
+
let fileListError = false;
|
|
1285
|
+
try {
|
|
1286
|
+
changedFiles = await getChangedFilesForIncremental({ git });
|
|
1287
|
+
} catch (error) {
|
|
1288
|
+
fileListError = true;
|
|
1289
|
+
logLine(`\u26A0\uFE0F Failed to determine changed files for tests: ${error.message}`);
|
|
1290
|
+
}
|
|
1291
|
+
const hasConfigChange = !fileListError && changedFiles.some(isTestConfigFile);
|
|
1292
|
+
const untrackedOutput = await git.raw(["ls-files", "--others", "--exclude-standard"]);
|
|
1293
|
+
const untrackedFiles = untrackedOutput.split(/\r?\n/).map((f) => f.trim()).filter(Boolean);
|
|
1294
|
+
const untrackedCodeFiles = untrackedFiles.filter(isCodeFilePath);
|
|
1295
|
+
const hasUntrackedCode = untrackedCodeFiles.length > 0;
|
|
1296
|
+
const plan = resolveTestPlan({
|
|
1297
|
+
isMainBranch,
|
|
1298
|
+
hasUntrackedCode,
|
|
1299
|
+
hasConfigChange,
|
|
1300
|
+
fileListError
|
|
1301
|
+
});
|
|
1302
|
+
if (plan.mode === "full") {
|
|
1303
|
+
if (plan.reason === "untracked-code") {
|
|
1304
|
+
const preview = untrackedCodeFiles.slice(0, 5).join(", ");
|
|
1305
|
+
logLine(
|
|
1306
|
+
`\u26A0\uFE0F Untracked code files detected (${untrackedCodeFiles.length}): ${preview}${untrackedCodeFiles.length > 5 ? "..." : ""}`
|
|
1307
|
+
);
|
|
1308
|
+
} else if (plan.reason === "test-config") {
|
|
1309
|
+
logLine("\u26A0\uFE0F Test config changes detected - running full test suite");
|
|
1310
|
+
} else if (plan.reason === "file-list-error") {
|
|
1311
|
+
logLine("\u26A0\uFE0F Changed file list unavailable - running full test suite");
|
|
1312
|
+
}
|
|
1313
|
+
logLine("\u{1F4CB} Running full test suite to avoid missing coverage");
|
|
1314
|
+
const result2 = run(gatesCommands.test_full, { agentLog, cwd });
|
|
1315
|
+
return { ...result2, duration: Date.now() - start, isIncremental: false };
|
|
1316
|
+
}
|
|
1317
|
+
logLine(`
|
|
1318
|
+
> Running tests (${testRunner} --changed)
|
|
1319
|
+
`);
|
|
1320
|
+
const incrementalCommand = resolveIncrementalTestCommand({
|
|
1321
|
+
testRunner,
|
|
1322
|
+
configuredIncrementalCommand: gatesCommands.test_incremental
|
|
1323
|
+
});
|
|
1324
|
+
if (incrementalCommand) {
|
|
1325
|
+
if (testRunner === "vitest" && incrementalCommand !== gatesCommands.test_incremental?.trim()) {
|
|
1326
|
+
logLine("\u2139\uFE0F Using hardened vitest incremental command for worker stability");
|
|
1327
|
+
}
|
|
1328
|
+
const result2 = run(incrementalCommand, { agentLog, cwd });
|
|
1329
|
+
return { ...result2, duration: Date.now() - start, isIncremental: true };
|
|
1330
|
+
}
|
|
1331
|
+
logLine("\u26A0\uFE0F No incremental test command configured, running full suite");
|
|
1332
|
+
const result = run(gatesCommands.test_full, { agentLog, cwd });
|
|
1333
|
+
return { ...result, duration: Date.now() - start, isIncremental: false };
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
console.error("\u26A0\uFE0F Changed tests failed, falling back to full suite:", error.message);
|
|
1336
|
+
const result = run(gatesCommands.test_full, { agentLog, cwd });
|
|
1337
|
+
return { ...result, isIncremental: false };
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
var SAFETY_CRITICAL_TEST_FILES = [
|
|
1341
|
+
// Privacy detection tests
|
|
1342
|
+
"src/lib/llm/__tests__/privacyDetector.test.ts",
|
|
1343
|
+
// Escalation trigger tests
|
|
1344
|
+
"src/lib/llm/__tests__/escalationTrigger.test.ts",
|
|
1345
|
+
"src/components/escalation/__tests__/EscalationHistory.test.tsx",
|
|
1346
|
+
// Constitutional enforcer tests
|
|
1347
|
+
"src/lib/llm/__tests__/constitutionalEnforcer.test.ts",
|
|
1348
|
+
// Safe prompt wrapper tests
|
|
1349
|
+
"src/lib/llm/__tests__/safePromptWrapper.test.ts",
|
|
1350
|
+
// Crisis/emergency handling tests
|
|
1351
|
+
"src/lib/prompts/__tests__/golden-crisis.test.ts"
|
|
1352
|
+
];
|
|
1353
|
+
async function runSafetyCriticalTests({
|
|
1354
|
+
agentLog,
|
|
1355
|
+
cwd
|
|
1356
|
+
}) {
|
|
1357
|
+
const start = Date.now();
|
|
1358
|
+
const logLine = (line) => {
|
|
1359
|
+
if (!agentLog) {
|
|
1360
|
+
console.log(line);
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
writeSync2(agentLog.logFd, `${line}
|
|
1364
|
+
`);
|
|
1365
|
+
};
|
|
1366
|
+
const webDir = path7.join(cwd, DIRECTORIES.APPS_WEB);
|
|
1367
|
+
try {
|
|
1368
|
+
await access2(webDir);
|
|
1369
|
+
} catch {
|
|
1370
|
+
logLine("\n> Safety-critical tests skipped (apps/web not present)\n");
|
|
1371
|
+
return { ok: true, duration: Date.now() - start, testCount: 0 };
|
|
1372
|
+
}
|
|
1373
|
+
try {
|
|
1374
|
+
logLine("\n> Safety-critical tests (always run)\n");
|
|
1375
|
+
logLine(`Test files: ${SAFETY_CRITICAL_TEST_FILES.length} files
|
|
1376
|
+
`);
|
|
1377
|
+
const result = spawnSync2(
|
|
1378
|
+
PKG_MANAGER,
|
|
1379
|
+
[
|
|
1380
|
+
"vitest",
|
|
1381
|
+
"run",
|
|
1382
|
+
"--project",
|
|
1383
|
+
PACKAGES.WEB,
|
|
1384
|
+
"--reporter=verbose",
|
|
1385
|
+
...SAFETY_CRITICAL_TEST_FILES,
|
|
1386
|
+
"--passWithNoTests"
|
|
1387
|
+
],
|
|
1388
|
+
agentLog ? {
|
|
1389
|
+
stdio: ["ignore", agentLog.logFd, agentLog.logFd],
|
|
1390
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
1391
|
+
cwd
|
|
1392
|
+
} : {
|
|
1393
|
+
stdio: "inherit",
|
|
1394
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
1395
|
+
cwd
|
|
1396
|
+
}
|
|
1397
|
+
);
|
|
1398
|
+
const duration = Date.now() - start;
|
|
1399
|
+
return {
|
|
1400
|
+
ok: result.status === EXIT_CODES.SUCCESS,
|
|
1401
|
+
duration,
|
|
1402
|
+
testCount: SAFETY_CRITICAL_TEST_FILES.length
|
|
1403
|
+
};
|
|
1404
|
+
} catch (error) {
|
|
1405
|
+
console.error("\u26A0\uFE0F Safety-critical tests failed:", error.message);
|
|
1406
|
+
return { ok: false, duration: Date.now() - start, testCount: 0 };
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
async function runIntegrationTests({
|
|
1410
|
+
agentLog,
|
|
1411
|
+
cwd
|
|
1412
|
+
}) {
|
|
1413
|
+
const start = Date.now();
|
|
1414
|
+
const logLine = (line) => {
|
|
1415
|
+
if (!agentLog) {
|
|
1416
|
+
console.log(line);
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
writeSync2(agentLog.logFd, `${line}
|
|
1420
|
+
`);
|
|
1421
|
+
};
|
|
1422
|
+
try {
|
|
1423
|
+
logLine("\n> Integration tests (high-risk changes detected)\n");
|
|
1424
|
+
const result = run(
|
|
1425
|
+
`RUN_INTEGRATION_TESTS=1 ${pnpmCmd(
|
|
1426
|
+
"vitest",
|
|
1427
|
+
"run",
|
|
1428
|
+
"'**/*.integration.*'",
|
|
1429
|
+
"'**/golden-*.test.*'"
|
|
1430
|
+
)}`,
|
|
1431
|
+
{ agentLog, cwd }
|
|
1432
|
+
);
|
|
1433
|
+
const duration = Date.now() - start;
|
|
1434
|
+
return {
|
|
1435
|
+
ok: result.ok,
|
|
1436
|
+
duration
|
|
1437
|
+
};
|
|
1438
|
+
} catch (error) {
|
|
1439
|
+
console.error("\u26A0\uFE0F Integration tests failed:", error.message);
|
|
1440
|
+
return { ok: false, duration: Date.now() - start };
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
var CoChangeRulesConfigSchema = CoChangeRuleConfigSchema.array();
|
|
1444
|
+
function hasPatternMatch(changedFiles, patterns) {
|
|
1445
|
+
return changedFiles.some((filePath) => micromatch.isMatch(filePath, patterns));
|
|
1446
|
+
}
|
|
1447
|
+
function evaluateCoChangeRules({
|
|
1448
|
+
changedFiles,
|
|
1449
|
+
rules
|
|
1450
|
+
}) {
|
|
1451
|
+
const errors = [];
|
|
1452
|
+
const warnings = [];
|
|
1453
|
+
for (const rule of rules) {
|
|
1454
|
+
if (rule.severity === CO_CHANGE_SEVERITY.OFF) {
|
|
1455
|
+
continue;
|
|
1456
|
+
}
|
|
1457
|
+
const triggerMatched = hasPatternMatch(changedFiles, rule.trigger_patterns);
|
|
1458
|
+
if (!triggerMatched) {
|
|
1459
|
+
continue;
|
|
1460
|
+
}
|
|
1461
|
+
const requireMatched = hasPatternMatch(changedFiles, rule.require_patterns);
|
|
1462
|
+
if (requireMatched) {
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
const message = `co-change "${rule.name}" violated: trigger matched (${rule.trigger_patterns.join(", ")}) but required patterns missing (${rule.require_patterns.join(", ")})`;
|
|
1466
|
+
if (rule.severity === CO_CHANGE_SEVERITY.WARN) {
|
|
1467
|
+
warnings.push(message);
|
|
1468
|
+
continue;
|
|
1469
|
+
}
|
|
1470
|
+
errors.push(message);
|
|
1471
|
+
}
|
|
1472
|
+
return { errors, warnings };
|
|
1473
|
+
}
|
|
1474
|
+
async function runCoChangeGate({ agentLog, useAgentMode, cwd }) {
|
|
1475
|
+
const start = Date.now();
|
|
1476
|
+
const effectiveCwd = cwd ?? process.cwd();
|
|
1477
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
1478
|
+
logLine("\n> Co-change check\n");
|
|
1479
|
+
const gatesSection = getGatesSection(effectiveCwd);
|
|
1480
|
+
const parsedRules = CoChangeRulesConfigSchema.safeParse(gatesSection?.co_change ?? []);
|
|
1481
|
+
if (!parsedRules.success) {
|
|
1482
|
+
logLine("\u26A0\uFE0F Invalid gates.co_change config; skipping check.");
|
|
1483
|
+
return { ok: true, duration: Date.now() - start };
|
|
1484
|
+
}
|
|
1485
|
+
const rules = parsedRules.data;
|
|
1486
|
+
if (rules.length === 0) {
|
|
1487
|
+
logLine("\u2139\uFE0F No co-change rules configured; skipping.");
|
|
1488
|
+
return { ok: true, duration: Date.now() - start };
|
|
1489
|
+
}
|
|
1490
|
+
const git = createGitForPath(effectiveCwd);
|
|
1491
|
+
const changedFiles = await getChangedFilesForIncremental({ git });
|
|
1492
|
+
if (changedFiles.length === 0) {
|
|
1493
|
+
logLine("\u2139\uFE0F No changed files detected; skipping co-change checks.");
|
|
1494
|
+
return { ok: true, duration: Date.now() - start };
|
|
1495
|
+
}
|
|
1496
|
+
const evaluation = evaluateCoChangeRules({ changedFiles, rules });
|
|
1497
|
+
for (const warning of evaluation.warnings) {
|
|
1498
|
+
logLine(`\u26A0\uFE0F ${warning}`);
|
|
1499
|
+
}
|
|
1500
|
+
for (const error of evaluation.errors) {
|
|
1501
|
+
logLine(`\u274C ${error}`);
|
|
1502
|
+
}
|
|
1503
|
+
if (evaluation.errors.length > 0) {
|
|
1504
|
+
logLine("co-change check failed.");
|
|
1505
|
+
return { ok: false, duration: Date.now() - start };
|
|
1506
|
+
}
|
|
1507
|
+
logLine("co-change check passed.");
|
|
1508
|
+
return { ok: true, duration: Date.now() - start };
|
|
1509
|
+
}
|
|
1510
|
+
async function runSpecLinterGate({ agentLog, useAgentMode, cwd }) {
|
|
1511
|
+
const start = Date.now();
|
|
1512
|
+
const wuId = await detectCurrentWUForCwd(cwd);
|
|
1513
|
+
const plan = resolveSpecLinterPlan(wuId);
|
|
1514
|
+
if (plan.scopedWuId) {
|
|
1515
|
+
const scopedCmd = pnpmCmd("wu:validate", "--id", plan.scopedWuId);
|
|
1516
|
+
const scopedResult = run(scopedCmd, { agentLog, cwd });
|
|
1517
|
+
if (!scopedResult.ok) {
|
|
1518
|
+
return { ok: false, duration: Date.now() - start };
|
|
1519
|
+
}
|
|
1520
|
+
return { ok: true, duration: Date.now() - start };
|
|
1521
|
+
}
|
|
1522
|
+
if (!useAgentMode) {
|
|
1523
|
+
console.log("\u26A0\uFE0F Unable to detect current WU; skipping scoped validation.");
|
|
1524
|
+
} else if (agentLog) {
|
|
1525
|
+
writeSync2(
|
|
1526
|
+
agentLog.logFd,
|
|
1527
|
+
"\u26A0\uFE0F Unable to detect current WU; skipping scoped validation.\n"
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
if (!plan.runGlobal) {
|
|
1531
|
+
return { ok: true, duration: Date.now() - start };
|
|
1532
|
+
}
|
|
1533
|
+
const fallbackResult = run(pnpmRun(SCRIPTS.SPEC_LINTER), { agentLog, cwd });
|
|
1534
|
+
return { ok: fallbackResult.ok, duration: Date.now() - start };
|
|
1535
|
+
}
|
|
1536
|
+
async function runClaimValidationGate({ agentLog, useAgentMode, cwd }) {
|
|
1537
|
+
const start = Date.now();
|
|
1538
|
+
const effectiveCwd = cwd ?? process.cwd();
|
|
1539
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
1540
|
+
logLine("\n> Claim validation\n");
|
|
1541
|
+
const wuId = await detectCurrentWUForCwd(effectiveCwd);
|
|
1542
|
+
if (!wuId) {
|
|
1543
|
+
logLine("\u26A0\uFE0F Unable to detect current WU; skipping claim validation.");
|
|
1544
|
+
return { ok: true, duration: Date.now() - start };
|
|
1545
|
+
}
|
|
1546
|
+
const result = await validateClaimValidation({ cwd: effectiveCwd, wuId });
|
|
1547
|
+
if (result.warnings.length > 0) {
|
|
1548
|
+
for (const warning of result.warnings) {
|
|
1549
|
+
logLine(`\u26A0\uFE0F ${warning}`);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
if (result.ok) {
|
|
1553
|
+
logLine(
|
|
1554
|
+
`Claim validation passed (${result.checkedClaims} checked claim${result.checkedClaims === 1 ? "" : "s"}).`
|
|
1555
|
+
);
|
|
1556
|
+
return { ok: true, duration: Date.now() - start };
|
|
1557
|
+
}
|
|
1558
|
+
logLine("\u274C Claim validation mismatches detected:");
|
|
1559
|
+
for (const mismatch of result.mismatches) {
|
|
1560
|
+
const specPath = path7.relative(effectiveCwd, mismatch.specReference.filePath).replaceAll(path7.sep, "/");
|
|
1561
|
+
logLine(` - Claim (${mismatch.claimId}): ${mismatch.claimText}`);
|
|
1562
|
+
logLine(
|
|
1563
|
+
` Spec: ${specPath}:${mismatch.specReference.line} [${mismatch.specReference.id} ${mismatch.specReference.section}]`
|
|
1564
|
+
);
|
|
1565
|
+
for (const evidence of mismatch.evidence) {
|
|
1566
|
+
logLine(` Evidence: ${evidence.filePath}:${evidence.line} ${evidence.lineText}`);
|
|
1567
|
+
}
|
|
1568
|
+
logLine(` Hint: ${mismatch.remediationHint}`);
|
|
1569
|
+
}
|
|
1570
|
+
return { ok: false, duration: Date.now() - start };
|
|
1571
|
+
}
|
|
1572
|
+
async function runBacklogSyncGate({ agentLog, useAgentMode, cwd }) {
|
|
1573
|
+
const start = Date.now();
|
|
1574
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
1575
|
+
logLine("\n> Backlog sync\n");
|
|
1576
|
+
const result = await validateBacklogSync({ cwd });
|
|
1577
|
+
if (result.errors.length > 0) {
|
|
1578
|
+
logLine("\u274C Backlog sync errors:");
|
|
1579
|
+
result.errors.forEach((error) => logLine(` - ${error}`));
|
|
1580
|
+
}
|
|
1581
|
+
if (result.warnings.length > 0) {
|
|
1582
|
+
logLine("\u26A0\uFE0F Backlog sync warnings:");
|
|
1583
|
+
result.warnings.forEach((warning) => logLine(` - ${warning}`));
|
|
1584
|
+
}
|
|
1585
|
+
logLine(`Backlog sync summary: WU files=${result.wuCount}, Backlog refs=${result.backlogCount}`);
|
|
1586
|
+
return { ok: result.valid, duration: Date.now() - start };
|
|
1587
|
+
}
|
|
1588
|
+
async function runSupabaseDocsGate({ agentLog, useAgentMode, cwd }) {
|
|
1589
|
+
const start = Date.now();
|
|
1590
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
1591
|
+
logLine("\n> Supabase docs linter\n");
|
|
1592
|
+
const result = await runSupabaseDocsLinter({ cwd, logger: { log: logLine } });
|
|
1593
|
+
if (result.skipped) {
|
|
1594
|
+
logLine(`\u26A0\uFE0F ${result.message ?? "Supabase docs linter skipped."}`);
|
|
1595
|
+
} else if (!result.ok) {
|
|
1596
|
+
logLine("\u274C Supabase docs linter failed.");
|
|
1597
|
+
(result.errors ?? []).forEach((error) => logLine(` - ${error}`));
|
|
1598
|
+
} else {
|
|
1599
|
+
logLine(result.message ?? "Supabase docs linter passed.");
|
|
1600
|
+
}
|
|
1601
|
+
return { ok: result.ok, duration: Date.now() - start };
|
|
1602
|
+
}
|
|
1603
|
+
async function runLaneHealthGate({
|
|
1604
|
+
agentLog,
|
|
1605
|
+
useAgentMode,
|
|
1606
|
+
mode,
|
|
1607
|
+
cwd
|
|
1608
|
+
}) {
|
|
1609
|
+
const start = Date.now();
|
|
1610
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
1611
|
+
if (mode === "off") {
|
|
1612
|
+
logLine("\n> Lane health check (skipped - mode: off)\n");
|
|
1613
|
+
return { ok: true, duration: Date.now() - start };
|
|
1614
|
+
}
|
|
1615
|
+
logLine(`
|
|
1616
|
+
> Lane health check (mode: ${mode})
|
|
1617
|
+
`);
|
|
1618
|
+
const report = runLaneHealthCheck({ projectRoot: cwd });
|
|
1619
|
+
if (!report.healthy) {
|
|
1620
|
+
logLine("\u26A0\uFE0F Lane health issues detected:");
|
|
1621
|
+
if (report.overlaps.hasOverlaps) {
|
|
1622
|
+
logLine(` - ${report.overlaps.overlaps.length} overlapping code_paths`);
|
|
1623
|
+
}
|
|
1624
|
+
if (report.gaps.hasGaps) {
|
|
1625
|
+
logLine(` - ${report.gaps.uncoveredFiles.length} uncovered files`);
|
|
1626
|
+
}
|
|
1627
|
+
logLine(` Run 'pnpm lane:health' for full report.`);
|
|
1628
|
+
if (mode === "error") {
|
|
1629
|
+
return { ok: false, duration: Date.now() - start };
|
|
1630
|
+
}
|
|
1631
|
+
logLine(" (mode: warn - not blocking)");
|
|
1632
|
+
} else {
|
|
1633
|
+
logLine("Lane health check passed.");
|
|
1634
|
+
}
|
|
1635
|
+
return { ok: true, duration: Date.now() - start };
|
|
1636
|
+
}
|
|
1637
|
+
async function runDocsOnlyFilteredTests({
|
|
1638
|
+
packages,
|
|
1639
|
+
agentLog,
|
|
1640
|
+
cwd = process.cwd()
|
|
1641
|
+
}) {
|
|
1642
|
+
const start = Date.now();
|
|
1643
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode: !!agentLog, cwd });
|
|
1644
|
+
if (packages.length === 0) {
|
|
1645
|
+
logLine("\u{1F4DD} docs-only mode: no packages to test, skipping");
|
|
1646
|
+
return { ok: true, duration: Date.now() - start };
|
|
1647
|
+
}
|
|
1648
|
+
logLine(`
|
|
1649
|
+
> Tests (docs-only filtered: ${packages.join(", ")})
|
|
1650
|
+
`);
|
|
1651
|
+
const gatesCommands = resolveGatesCommands(cwd);
|
|
1652
|
+
if (gatesCommands.test_docs_only) {
|
|
1653
|
+
const result2 = run(gatesCommands.test_docs_only, { agentLog, cwd });
|
|
1654
|
+
return { ok: result2.ok, duration: Date.now() - start };
|
|
1655
|
+
}
|
|
1656
|
+
const filterArgs = packages.map((pkg) => `--filter=${pkg}`);
|
|
1657
|
+
const baseCmd = gatesCommands.test_full;
|
|
1658
|
+
const filteredCmd = `${baseCmd} ${filterArgs.join(" ")}`;
|
|
1659
|
+
const result = run(filteredCmd, { agentLog });
|
|
1660
|
+
return { ok: result.ok, duration: Date.now() - start };
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
// src/gate-defaults.ts
|
|
1664
|
+
function registerDocsOnlyGates(registry, options) {
|
|
1665
|
+
const { laneHealthMode, testsRequired, docsOnlyTestPlan } = options;
|
|
1666
|
+
registry.register({
|
|
1667
|
+
name: GATE_NAMES.INVARIANTS,
|
|
1668
|
+
cmd: GATE_COMMANDS.INVARIANTS
|
|
1669
|
+
});
|
|
1670
|
+
registry.register({
|
|
1671
|
+
name: GATE_NAMES.FORMAT_CHECK,
|
|
1672
|
+
scriptName: SCRIPTS.FORMAT_CHECK,
|
|
1673
|
+
// run function is injected by the gate runner (runFormatCheckGate)
|
|
1674
|
+
// We use a sentinel to indicate this gate needs its run function set
|
|
1675
|
+
cmd: void 0,
|
|
1676
|
+
run: void 0
|
|
1677
|
+
});
|
|
1678
|
+
registry.register({
|
|
1679
|
+
name: GATE_NAMES.SPEC_LINTER,
|
|
1680
|
+
scriptName: SCRIPTS.SPEC_LINTER
|
|
1681
|
+
});
|
|
1682
|
+
registry.register({
|
|
1683
|
+
name: GATE_NAMES.BACKLOG_SYNC
|
|
1684
|
+
});
|
|
1685
|
+
registry.register({
|
|
1686
|
+
name: GATE_NAMES.CLAIM_VALIDATION
|
|
1687
|
+
});
|
|
1688
|
+
registry.register({
|
|
1689
|
+
name: GATE_NAMES.LANE_HEALTH,
|
|
1690
|
+
warnOnly: laneHealthMode !== "error"
|
|
1691
|
+
});
|
|
1692
|
+
registry.register({
|
|
1693
|
+
name: GATE_NAMES.ONBOARDING_SMOKE_TEST,
|
|
1694
|
+
cmd: GATE_COMMANDS.ONBOARDING_SMOKE_TEST
|
|
1695
|
+
});
|
|
1696
|
+
if (docsOnlyTestPlan && docsOnlyTestPlan.mode === "filtered") {
|
|
1697
|
+
registry.register({
|
|
1698
|
+
name: GATE_NAMES.TEST,
|
|
1699
|
+
warnOnly: !testsRequired
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
function registerCodeGates(registry, options) {
|
|
1704
|
+
const {
|
|
1705
|
+
isFullLint,
|
|
1706
|
+
isFullTests,
|
|
1707
|
+
isFullCoverage,
|
|
1708
|
+
laneHealthMode,
|
|
1709
|
+
testsRequired,
|
|
1710
|
+
shouldRunIntegration,
|
|
1711
|
+
configuredTestFullCmd
|
|
1712
|
+
} = options;
|
|
1713
|
+
registry.register({
|
|
1714
|
+
name: GATE_NAMES.INVARIANTS,
|
|
1715
|
+
cmd: GATE_COMMANDS.INVARIANTS
|
|
1716
|
+
});
|
|
1717
|
+
registry.register({
|
|
1718
|
+
name: GATE_NAMES.FORMAT_CHECK,
|
|
1719
|
+
scriptName: SCRIPTS.FORMAT_CHECK
|
|
1720
|
+
});
|
|
1721
|
+
registry.register({
|
|
1722
|
+
name: GATE_NAMES.LINT,
|
|
1723
|
+
cmd: isFullLint ? `pnpm ${SCRIPTS.LINT}` : GATE_COMMANDS.INCREMENTAL,
|
|
1724
|
+
scriptName: SCRIPTS.LINT
|
|
1725
|
+
});
|
|
1726
|
+
registry.register({
|
|
1727
|
+
name: GATE_NAMES.CO_CHANGE,
|
|
1728
|
+
run: runCoChangeGate
|
|
1729
|
+
});
|
|
1730
|
+
registry.register({
|
|
1731
|
+
name: GATE_NAMES.TYPECHECK,
|
|
1732
|
+
cmd: `pnpm ${SCRIPTS.TYPECHECK}`,
|
|
1733
|
+
scriptName: SCRIPTS.TYPECHECK
|
|
1734
|
+
});
|
|
1735
|
+
registry.register({
|
|
1736
|
+
name: GATE_NAMES.SPEC_LINTER,
|
|
1737
|
+
scriptName: SCRIPTS.SPEC_LINTER
|
|
1738
|
+
});
|
|
1739
|
+
registry.register({
|
|
1740
|
+
name: GATE_NAMES.BACKLOG_SYNC
|
|
1741
|
+
});
|
|
1742
|
+
registry.register({
|
|
1743
|
+
name: GATE_NAMES.CLAIM_VALIDATION
|
|
1744
|
+
});
|
|
1745
|
+
registry.register({
|
|
1746
|
+
name: GATE_NAMES.SUPABASE_DOCS_LINTER
|
|
1747
|
+
});
|
|
1748
|
+
registry.register({
|
|
1749
|
+
name: GATE_NAMES.LANE_HEALTH,
|
|
1750
|
+
warnOnly: laneHealthMode !== "error"
|
|
1751
|
+
});
|
|
1752
|
+
registry.register({
|
|
1753
|
+
name: GATE_NAMES.ONBOARDING_SMOKE_TEST,
|
|
1754
|
+
cmd: GATE_COMMANDS.ONBOARDING_SMOKE_TEST
|
|
1755
|
+
});
|
|
1756
|
+
registry.register({
|
|
1757
|
+
name: GATE_NAMES.SAFETY_CRITICAL_TEST,
|
|
1758
|
+
cmd: GATE_COMMANDS.SAFETY_CRITICAL_TEST,
|
|
1759
|
+
warnOnly: !testsRequired
|
|
1760
|
+
});
|
|
1761
|
+
registry.register({
|
|
1762
|
+
name: GATE_NAMES.TEST,
|
|
1763
|
+
cmd: isFullTests || isFullCoverage ? configuredTestFullCmd : GATE_COMMANDS.INCREMENTAL_TEST,
|
|
1764
|
+
warnOnly: !testsRequired
|
|
1765
|
+
});
|
|
1766
|
+
if (shouldRunIntegration) {
|
|
1767
|
+
registry.register({
|
|
1768
|
+
name: GATE_NAMES.INTEGRATION_TEST,
|
|
1769
|
+
cmd: GATE_COMMANDS.TIERED_TEST,
|
|
1770
|
+
warnOnly: !testsRequired
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
registry.register({
|
|
1774
|
+
name: GATE_NAMES.COVERAGE,
|
|
1775
|
+
cmd: GATE_COMMANDS.COVERAGE_GATE
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
// src/onboarding-smoke-test.ts
|
|
1780
|
+
import * as fs from "fs";
|
|
1781
|
+
import * as path8 from "path";
|
|
1782
|
+
import * as os from "os";
|
|
1783
|
+
import * as yaml from "yaml";
|
|
1784
|
+
import { execFileSync } from "child_process";
|
|
1785
|
+
var PACKAGE_JSON_FILE = "package.json";
|
|
1786
|
+
var WORKSPACE_CONFIG_FILE = WORKSPACE_CONFIG_FILE_NAME;
|
|
1787
|
+
var SOFTWARE_DELIVERY_KEY = WORKSPACE_V2_KEYS.SOFTWARE_DELIVERY;
|
|
1788
|
+
var GIT_BINARY = "git";
|
|
1789
|
+
var REQUIRED_SCRIPTS = ["wu:claim", "wu:done", "wu:create", "gates"];
|
|
1790
|
+
function asRecord(value) {
|
|
1791
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1792
|
+
}
|
|
1793
|
+
function validateInitScripts(options) {
|
|
1794
|
+
const { projectDir } = options;
|
|
1795
|
+
const packageJsonPath = path8.join(projectDir, PACKAGE_JSON_FILE);
|
|
1796
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
1797
|
+
return {
|
|
1798
|
+
valid: false,
|
|
1799
|
+
missingScripts: [],
|
|
1800
|
+
invalidScripts: [],
|
|
1801
|
+
error: `${PACKAGE_JSON_FILE} not found in ${projectDir}`
|
|
1802
|
+
};
|
|
1803
|
+
}
|
|
1804
|
+
let packageJson;
|
|
1805
|
+
try {
|
|
1806
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
return {
|
|
1809
|
+
valid: false,
|
|
1810
|
+
missingScripts: [],
|
|
1811
|
+
invalidScripts: [],
|
|
1812
|
+
error: `Failed to parse ${PACKAGE_JSON_FILE}: ${err instanceof Error ? err.message : String(err)}`
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
const scripts = packageJson.scripts ?? {};
|
|
1816
|
+
const missingScripts = [];
|
|
1817
|
+
const invalidScripts = [];
|
|
1818
|
+
for (const script of REQUIRED_SCRIPTS) {
|
|
1819
|
+
if (!scripts[script]) {
|
|
1820
|
+
missingScripts.push(script);
|
|
1821
|
+
} else {
|
|
1822
|
+
const value = scripts[script];
|
|
1823
|
+
if (value.includes("pnpm exec") || value.includes("npx lumenflow")) {
|
|
1824
|
+
invalidScripts.push(script);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
return {
|
|
1829
|
+
valid: missingScripts.length === 0 && invalidScripts.length === 0,
|
|
1830
|
+
missingScripts,
|
|
1831
|
+
invalidScripts
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
function validateWorkspaceLaneScaffold(options) {
|
|
1835
|
+
const { projectDir } = options;
|
|
1836
|
+
const configPath = path8.join(projectDir, WORKSPACE_CONFIG_FILE);
|
|
1837
|
+
if (!fs.existsSync(configPath)) {
|
|
1838
|
+
return {
|
|
1839
|
+
valid: false,
|
|
1840
|
+
errors: [],
|
|
1841
|
+
error: `${WORKSPACE_CONFIG_FILE} not found in ${projectDir}`
|
|
1842
|
+
};
|
|
1843
|
+
}
|
|
1844
|
+
const legacyLaneInferencePath = path8.join(projectDir, ".lumenflow.lane-inference.yaml");
|
|
1845
|
+
if (fs.existsSync(legacyLaneInferencePath)) {
|
|
1846
|
+
return {
|
|
1847
|
+
valid: false,
|
|
1848
|
+
errors: [],
|
|
1849
|
+
error: `.lumenflow.lane-inference.yaml should not be scaffolded in ${projectDir}`
|
|
1850
|
+
};
|
|
1851
|
+
}
|
|
1852
|
+
let content;
|
|
1853
|
+
try {
|
|
1854
|
+
const rawContent = fs.readFileSync(configPath, "utf-8");
|
|
1855
|
+
content = yaml.parse(rawContent) ?? {};
|
|
1856
|
+
} catch (err) {
|
|
1857
|
+
return {
|
|
1858
|
+
valid: false,
|
|
1859
|
+
errors: [],
|
|
1860
|
+
error: `Failed to parse ${WORKSPACE_CONFIG_FILE}: ${err instanceof Error ? err.message : String(err)}`
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
const errors = [];
|
|
1864
|
+
const lifecycleStatus = content?.software_delivery?.lanes?.lifecycle?.status;
|
|
1865
|
+
if (lifecycleStatus !== "unconfigured") {
|
|
1866
|
+
errors.push(
|
|
1867
|
+
`Expected ${WORKSPACE_CONFIG_FILE} lane lifecycle to remain "unconfigured" after init.`
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
return {
|
|
1871
|
+
valid: errors.length === 0,
|
|
1872
|
+
errors
|
|
1873
|
+
};
|
|
1874
|
+
}
|
|
1875
|
+
function initializeGitRepo(projectDir) {
|
|
1876
|
+
execFileSync(GIT_BINARY, ["init"], { cwd: projectDir, stdio: "pipe" });
|
|
1877
|
+
execFileSync(GIT_BINARY, ["config", "user.email", "test@example.com"], {
|
|
1878
|
+
cwd: projectDir,
|
|
1879
|
+
stdio: "pipe"
|
|
1880
|
+
});
|
|
1881
|
+
execFileSync(GIT_BINARY, ["config", "user.name", "Test User"], {
|
|
1882
|
+
cwd: projectDir,
|
|
1883
|
+
stdio: "pipe"
|
|
1884
|
+
});
|
|
1885
|
+
execFileSync(GIT_BINARY, ["add", "-A"], { cwd: projectDir, stdio: "pipe" });
|
|
1886
|
+
execFileSync(GIT_BINARY, ["commit", "-m", "Initial commit", "--allow-empty"], {
|
|
1887
|
+
cwd: projectDir,
|
|
1888
|
+
stdio: "pipe"
|
|
1889
|
+
});
|
|
1890
|
+
}
|
|
1891
|
+
function createSampleWuYaml(projectDir) {
|
|
1892
|
+
const wuDir = path8.join(projectDir, createWuPaths({ projectRoot: projectDir }).WU_DIR());
|
|
1893
|
+
fs.mkdirSync(wuDir, { recursive: true });
|
|
1894
|
+
const wuYaml = `id: WU-TEST-001
|
|
1895
|
+
title: Test WU
|
|
1896
|
+
lane: 'Framework: Core'
|
|
1897
|
+
type: feature
|
|
1898
|
+
status: ready
|
|
1899
|
+
priority: P3
|
|
1900
|
+
created: 2026-02-02
|
|
1901
|
+
code_paths:
|
|
1902
|
+
- 'src/**'
|
|
1903
|
+
acceptance:
|
|
1904
|
+
- Test passes
|
|
1905
|
+
`;
|
|
1906
|
+
fs.writeFileSync(path8.join(wuDir, "WU-TEST-001.yaml"), wuYaml);
|
|
1907
|
+
}
|
|
1908
|
+
async function validateWuCreate(options) {
|
|
1909
|
+
const { projectDir } = options;
|
|
1910
|
+
const configPath = path8.join(projectDir, WORKSPACE_CONFIG_FILE);
|
|
1911
|
+
const existingWorkspace = fs.existsSync(configPath) ? asRecord(yaml.parse(fs.readFileSync(configPath, "utf-8"))) : null;
|
|
1912
|
+
const workspace = existingWorkspace ?? {};
|
|
1913
|
+
const softwareDelivery = asRecord(workspace[SOFTWARE_DELIVERY_KEY]) ?? {};
|
|
1914
|
+
const gitConfig = asRecord(softwareDelivery.git) ?? {};
|
|
1915
|
+
gitConfig.requireRemote = false;
|
|
1916
|
+
softwareDelivery.git = gitConfig;
|
|
1917
|
+
workspace[SOFTWARE_DELIVERY_KEY] = softwareDelivery;
|
|
1918
|
+
fs.writeFileSync(configPath, yaml.stringify(workspace));
|
|
1919
|
+
try {
|
|
1920
|
+
initializeGitRepo(projectDir);
|
|
1921
|
+
} catch (err) {
|
|
1922
|
+
return {
|
|
1923
|
+
success: false,
|
|
1924
|
+
error: `Failed to initialize git repo: ${err instanceof Error ? err.message : String(err)}`
|
|
1925
|
+
};
|
|
1926
|
+
}
|
|
1927
|
+
createSampleWuYaml(projectDir);
|
|
1928
|
+
return { success: true };
|
|
1929
|
+
}
|
|
1930
|
+
function collectScriptsErrors(scriptsResult) {
|
|
1931
|
+
const errors = [];
|
|
1932
|
+
if (scriptsResult.error) {
|
|
1933
|
+
errors.push(`Init scripts validation error: ${scriptsResult.error}`);
|
|
1934
|
+
}
|
|
1935
|
+
if (scriptsResult.missingScripts.length > 0) {
|
|
1936
|
+
errors.push(`Missing scripts: ${scriptsResult.missingScripts.join(", ")}`);
|
|
1937
|
+
}
|
|
1938
|
+
if (scriptsResult.invalidScripts.length > 0) {
|
|
1939
|
+
errors.push(`Invalid script format: ${scriptsResult.invalidScripts.join(", ")}`);
|
|
1940
|
+
}
|
|
1941
|
+
return errors;
|
|
1942
|
+
}
|
|
1943
|
+
function collectLaneErrors(laneResult) {
|
|
1944
|
+
const errors = [];
|
|
1945
|
+
if (laneResult.error) {
|
|
1946
|
+
errors.push(`Workspace lane validation error: ${laneResult.error}`);
|
|
1947
|
+
}
|
|
1948
|
+
errors.push(...laneResult.errors);
|
|
1949
|
+
return errors;
|
|
1950
|
+
}
|
|
1951
|
+
async function runValidations(tempDir, skipWuCreate) {
|
|
1952
|
+
const errors = [];
|
|
1953
|
+
await scaffoldProject(tempDir, { force: true, full: true });
|
|
1954
|
+
const scriptsResult = validateInitScripts({ projectDir: tempDir });
|
|
1955
|
+
if (!scriptsResult.valid) {
|
|
1956
|
+
errors.push(...collectScriptsErrors(scriptsResult));
|
|
1957
|
+
}
|
|
1958
|
+
const laneResult = validateWorkspaceLaneScaffold({ projectDir: tempDir });
|
|
1959
|
+
if (!laneResult.valid) {
|
|
1960
|
+
errors.push(...collectLaneErrors(laneResult));
|
|
1961
|
+
}
|
|
1962
|
+
let wuResult;
|
|
1963
|
+
if (!skipWuCreate) {
|
|
1964
|
+
wuResult = await validateWuCreate({ projectDir: tempDir });
|
|
1965
|
+
if (!wuResult.success && wuResult.error) {
|
|
1966
|
+
errors.push(`wu:create validation error: ${wuResult.error}`);
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
return { scriptsResult, laneResult, wuResult, errors };
|
|
1970
|
+
}
|
|
1971
|
+
function cleanupTempDir(tempDir) {
|
|
1972
|
+
if (tempDir && fs.existsSync(tempDir)) {
|
|
1973
|
+
try {
|
|
1974
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
1975
|
+
} catch {
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
async function runOnboardingSmokeTest(options = {}) {
|
|
1980
|
+
const { cleanup = true, skipWuCreate = false } = options;
|
|
1981
|
+
let { tempDir } = options;
|
|
1982
|
+
const result = {
|
|
1983
|
+
success: false,
|
|
1984
|
+
errors: []
|
|
1985
|
+
};
|
|
1986
|
+
if (!tempDir) {
|
|
1987
|
+
tempDir = fs.mkdtempSync(path8.join(os.tmpdir(), "lumenflow-smoke-test-"));
|
|
1988
|
+
result.tempDir = tempDir;
|
|
1989
|
+
}
|
|
1990
|
+
if (!fs.existsSync(tempDir)) {
|
|
1991
|
+
try {
|
|
1992
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
return {
|
|
1995
|
+
success: false,
|
|
1996
|
+
errors: [
|
|
1997
|
+
`Failed to create temp directory: ${err instanceof Error ? err.message : String(err)}`
|
|
1998
|
+
]
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
try {
|
|
2003
|
+
const { scriptsResult, laneResult, wuResult, errors } = await runValidations(
|
|
2004
|
+
tempDir,
|
|
2005
|
+
skipWuCreate
|
|
2006
|
+
);
|
|
2007
|
+
result.initScriptsValidation = scriptsResult;
|
|
2008
|
+
result.workspaceLaneValidation = laneResult;
|
|
2009
|
+
result.wuCreateValidation = wuResult;
|
|
2010
|
+
result.errors = errors;
|
|
2011
|
+
result.success = errors.length === 0;
|
|
2012
|
+
} catch (err) {
|
|
2013
|
+
result.errors = [`Smoke test failed: ${err instanceof Error ? err.message : String(err)}`];
|
|
2014
|
+
result.success = false;
|
|
2015
|
+
} finally {
|
|
2016
|
+
if (cleanup) {
|
|
2017
|
+
cleanupTempDir(tempDir);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
return result;
|
|
2021
|
+
}
|
|
2022
|
+
async function runOnboardingSmokeTestGate(options) {
|
|
2023
|
+
const start = Date.now();
|
|
2024
|
+
const logger = options.logger ?? console;
|
|
2025
|
+
logger.log("Running onboarding smoke test...");
|
|
2026
|
+
const result = await runOnboardingSmokeTest({ cleanup: true });
|
|
2027
|
+
if (result.success) {
|
|
2028
|
+
logger.log("Onboarding smoke test passed.");
|
|
2029
|
+
} else {
|
|
2030
|
+
logger.log("Onboarding smoke test failed:");
|
|
2031
|
+
for (const error of result.errors) {
|
|
2032
|
+
logger.log(` - ${error}`);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
return {
|
|
2036
|
+
ok: result.success,
|
|
2037
|
+
duration: Date.now() - start
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// src/gates.ts
|
|
2042
|
+
var GATES_OPTIONS = {
|
|
2043
|
+
docsOnly: {
|
|
2044
|
+
name: "docsOnly",
|
|
2045
|
+
flags: "--docs-only",
|
|
2046
|
+
description: "Run docs-only gates (format, spec-linter, backlog-sync)"
|
|
2047
|
+
},
|
|
2048
|
+
fullLint: {
|
|
2049
|
+
name: "fullLint",
|
|
2050
|
+
flags: "--full-lint",
|
|
2051
|
+
description: "Run full lint instead of incremental"
|
|
2052
|
+
},
|
|
2053
|
+
fullTests: {
|
|
2054
|
+
name: "fullTests",
|
|
2055
|
+
flags: "--full-tests",
|
|
2056
|
+
description: "Run full test suite instead of incremental"
|
|
2057
|
+
},
|
|
2058
|
+
fullCoverage: {
|
|
2059
|
+
name: "fullCoverage",
|
|
2060
|
+
flags: "--full-coverage",
|
|
2061
|
+
description: "Force full test suite and coverage gate (implies --full-tests)"
|
|
2062
|
+
},
|
|
2063
|
+
coverageMode: {
|
|
2064
|
+
name: "coverageMode",
|
|
2065
|
+
flags: "--coverage-mode <mode>",
|
|
2066
|
+
description: 'Coverage gate mode: "warn" logs warnings, "block" fails gate',
|
|
2067
|
+
default: "block"
|
|
2068
|
+
},
|
|
2069
|
+
verbose: {
|
|
2070
|
+
name: "verbose",
|
|
2071
|
+
flags: "--verbose",
|
|
2072
|
+
description: "Stream output in agent mode instead of logging to file"
|
|
2073
|
+
},
|
|
2074
|
+
// WU-1520: --strict flag makes missing scripts a hard failure for CI
|
|
2075
|
+
strict: {
|
|
2076
|
+
name: "strict",
|
|
2077
|
+
flags: "--strict",
|
|
2078
|
+
description: "Fail on missing gate scripts instead of skipping (for CI enforcement)"
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
function parseGatesOptions() {
|
|
2082
|
+
const originalArgv = process.argv;
|
|
2083
|
+
const filteredArgv = originalArgv.filter((arg, index, arr) => {
|
|
2084
|
+
if (arg === "--") {
|
|
2085
|
+
const nextArg = arr[index + 1];
|
|
2086
|
+
return Boolean(nextArg && !nextArg.startsWith("-"));
|
|
2087
|
+
}
|
|
2088
|
+
return true;
|
|
2089
|
+
});
|
|
2090
|
+
process.argv = filteredArgv;
|
|
2091
|
+
try {
|
|
2092
|
+
const opts = createWUParser({
|
|
2093
|
+
name: "gates",
|
|
2094
|
+
description: "Run quality gates with support for docs-only mode, incremental linting, and tiered testing",
|
|
2095
|
+
options: Object.values(GATES_OPTIONS)
|
|
2096
|
+
});
|
|
2097
|
+
return {
|
|
2098
|
+
docsOnly: opts.docsOnly,
|
|
2099
|
+
fullLint: opts.fullLint,
|
|
2100
|
+
fullTests: opts.fullTests,
|
|
2101
|
+
fullCoverage: opts.fullCoverage,
|
|
2102
|
+
coverageMode: opts.coverageMode ?? "block",
|
|
2103
|
+
verbose: opts.verbose,
|
|
2104
|
+
strict: opts.strict
|
|
2105
|
+
};
|
|
2106
|
+
} finally {
|
|
2107
|
+
process.argv = originalArgv;
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
async function runGates(options = {}) {
|
|
2111
|
+
try {
|
|
2112
|
+
return await executeGates({
|
|
2113
|
+
...options,
|
|
2114
|
+
cwd: options.cwd ?? process.cwd(),
|
|
2115
|
+
coverageMode: options.coverageMode ?? COVERAGE_GATE_MODES.BLOCK
|
|
2116
|
+
});
|
|
2117
|
+
} catch {
|
|
2118
|
+
return false;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
async function syncGatesTelemetryToCloud(input = {}) {
|
|
2122
|
+
return syncNdjsonTelemetryToCloud({
|
|
2123
|
+
workspaceRoot: input.cwd ?? process.cwd(),
|
|
2124
|
+
fetchFn: input.fetchFn,
|
|
2125
|
+
logger: input.logger,
|
|
2126
|
+
now: input.now,
|
|
2127
|
+
environment: input.environment
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
async function executeGates(opts) {
|
|
2131
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
2132
|
+
const argv = opts.argv ?? process.argv.slice(2);
|
|
2133
|
+
const wu_id = getCurrentWU();
|
|
2134
|
+
const lane = getCurrentLane();
|
|
2135
|
+
const useAgentMode = shouldUseGatesAgentMode({ argv, env: process.env });
|
|
2136
|
+
const agentLog = useAgentMode ? createAgentLogContext({ wuId: wu_id, lane, cwd }) : null;
|
|
2137
|
+
if (useAgentMode && !agentLog) {
|
|
2138
|
+
die("Failed to initialize agent-mode gate log context");
|
|
2139
|
+
}
|
|
2140
|
+
const isDocsOnly = opts.docsOnly || false;
|
|
2141
|
+
const isFullLint = opts.fullLint || false;
|
|
2142
|
+
const isFullTests = opts.fullTests || false;
|
|
2143
|
+
const isFullCoverage = opts.fullCoverage || false;
|
|
2144
|
+
const resolvedTestPolicy = resolveTestPolicy(cwd);
|
|
2145
|
+
const coverageMode = opts.coverageMode || resolvedTestPolicy.mode || COVERAGE_GATE_MODES.BLOCK;
|
|
2146
|
+
const coverageThreshold = resolvedTestPolicy.threshold;
|
|
2147
|
+
const testsRequired = resolvedTestPolicy.tests_required;
|
|
2148
|
+
const laneHealthMode = loadLaneHealthConfig(cwd);
|
|
2149
|
+
const configuredGatesCommands = resolveGatesCommands(cwd);
|
|
2150
|
+
const isStrict = opts.strict || false;
|
|
2151
|
+
const packageJsonScripts = loadPackageJsonScripts(cwd);
|
|
2152
|
+
const gateResults = [];
|
|
2153
|
+
const telemetrySyncLogger = {
|
|
2154
|
+
warn: (message) => {
|
|
2155
|
+
if (useAgentMode) {
|
|
2156
|
+
writeSync3(agentLog.logFd, `${message}
|
|
2157
|
+
`);
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
console.warn(message);
|
|
2161
|
+
}
|
|
2162
|
+
};
|
|
2163
|
+
async function flushTelemetryToCloud() {
|
|
2164
|
+
try {
|
|
2165
|
+
await syncGatesTelemetryToCloud({
|
|
2166
|
+
cwd,
|
|
2167
|
+
logger: telemetrySyncLogger
|
|
2168
|
+
});
|
|
2169
|
+
} catch (error) {
|
|
2170
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2171
|
+
telemetrySyncLogger.warn(`[gates] cloud telemetry sync failed unexpectedly: ${message}`);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
if (useAgentMode) {
|
|
2175
|
+
console.log(
|
|
2176
|
+
`\u{1F9FE} gates (agent mode): output -> ${agentLog.logPath} (use --verbose for streaming)
|
|
2177
|
+
`
|
|
2178
|
+
);
|
|
2179
|
+
}
|
|
2180
|
+
let riskTier = null;
|
|
2181
|
+
if (!isDocsOnly) {
|
|
2182
|
+
riskTier = {
|
|
2183
|
+
tier: "standard",
|
|
2184
|
+
isDocsOnly: false,
|
|
2185
|
+
shouldRunIntegration: false,
|
|
2186
|
+
highRiskPaths: []
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
const effectiveDocsOnly = isDocsOnly || riskTier && riskTier.isDocsOnly;
|
|
2190
|
+
let docsOnlyTestPlan = null;
|
|
2191
|
+
if (effectiveDocsOnly) {
|
|
2192
|
+
const codePaths = loadCurrentWUCodePaths({ cwd });
|
|
2193
|
+
docsOnlyTestPlan = resolveDocsOnlyTestPlan({ codePaths });
|
|
2194
|
+
}
|
|
2195
|
+
const gateRegistry = new GateRegistry();
|
|
2196
|
+
if (effectiveDocsOnly) {
|
|
2197
|
+
registerDocsOnlyGates(gateRegistry, {
|
|
2198
|
+
laneHealthMode,
|
|
2199
|
+
testsRequired,
|
|
2200
|
+
docsOnlyTestPlan
|
|
2201
|
+
});
|
|
2202
|
+
} else {
|
|
2203
|
+
registerCodeGates(gateRegistry, {
|
|
2204
|
+
isFullLint,
|
|
2205
|
+
isFullTests,
|
|
2206
|
+
isFullCoverage,
|
|
2207
|
+
laneHealthMode,
|
|
2208
|
+
testsRequired,
|
|
2209
|
+
shouldRunIntegration: !!(riskTier && riskTier.shouldRunIntegration),
|
|
2210
|
+
configuredTestFullCmd: configuredGatesCommands.test_full
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
const gateRunFunctions = {
|
|
2214
|
+
[GATE_NAMES.FORMAT_CHECK]: runFormatCheckGate,
|
|
2215
|
+
[GATE_NAMES.SPEC_LINTER]: runSpecLinterGate,
|
|
2216
|
+
[GATE_NAMES.CLAIM_VALIDATION]: runClaimValidationGate,
|
|
2217
|
+
[GATE_NAMES.BACKLOG_SYNC]: runBacklogSyncGate,
|
|
2218
|
+
[GATE_NAMES.SUPABASE_DOCS_LINTER]: runSupabaseDocsGate,
|
|
2219
|
+
[GATE_NAMES.LANE_HEALTH]: (ctx) => runLaneHealthGate({ ...ctx, mode: laneHealthMode })
|
|
2220
|
+
};
|
|
2221
|
+
if (docsOnlyTestPlan && docsOnlyTestPlan.mode === "filtered") {
|
|
2222
|
+
gateRunFunctions[GATE_NAMES.TEST] = (ctx) => {
|
|
2223
|
+
const pkgs = docsOnlyTestPlan.packages;
|
|
2224
|
+
return runDocsOnlyFilteredTests({
|
|
2225
|
+
packages: pkgs,
|
|
2226
|
+
agentLog: ctx.agentLog,
|
|
2227
|
+
cwd: ctx.cwd
|
|
2228
|
+
});
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
const gates = gateRegistry.getAll().map((gate) => {
|
|
2232
|
+
const runFn = gateRunFunctions[gate.name];
|
|
2233
|
+
if (runFn && !gate.run) {
|
|
2234
|
+
return { ...gate, run: runFn };
|
|
2235
|
+
}
|
|
2236
|
+
return gate;
|
|
2237
|
+
});
|
|
2238
|
+
if (effectiveDocsOnly) {
|
|
2239
|
+
const docsOnlyMessage = docsOnlyTestPlan && docsOnlyTestPlan.mode === "filtered" ? formatDocsOnlySkipMessage(docsOnlyTestPlan) : "\u{1F4DD} Docs-only mode: skipping lint, typecheck, and all tests (no code packages in code_paths)";
|
|
2240
|
+
if (!useAgentMode) {
|
|
2241
|
+
console.log(`${docsOnlyMessage}
|
|
2242
|
+
`);
|
|
2243
|
+
} else {
|
|
2244
|
+
writeSync3(agentLog.logFd, `${docsOnlyMessage}
|
|
2245
|
+
`);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
let lastTestResult = null;
|
|
2249
|
+
let lastFormatCheckFiles = null;
|
|
2250
|
+
for (const gate of gates) {
|
|
2251
|
+
let result;
|
|
2252
|
+
const gateScriptName = gate.scriptName ?? null;
|
|
2253
|
+
const gateAction = resolveGateAction(gate.name, gateScriptName, packageJsonScripts, isStrict);
|
|
2254
|
+
if (gateAction === "skip") {
|
|
2255
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode, cwd });
|
|
2256
|
+
const warningMsg = buildMissingScriptWarning(gateScriptName);
|
|
2257
|
+
logLine(`
|
|
2258
|
+
${warningMsg}
|
|
2259
|
+
`);
|
|
2260
|
+
gateResults.push({
|
|
2261
|
+
name: gate.name,
|
|
2262
|
+
status: "skipped",
|
|
2263
|
+
durationMs: 0,
|
|
2264
|
+
reason: "script not found in package.json"
|
|
2265
|
+
});
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
if (gateAction === "fail") {
|
|
2269
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode, cwd });
|
|
2270
|
+
logLine(`
|
|
2271
|
+
\u274C "${gateScriptName}" script not found in package.json (--strict mode)
|
|
2272
|
+
`);
|
|
2273
|
+
gateResults.push({
|
|
2274
|
+
name: gate.name,
|
|
2275
|
+
status: "failed",
|
|
2276
|
+
durationMs: 0,
|
|
2277
|
+
reason: "script not found in package.json (strict mode)"
|
|
2278
|
+
});
|
|
2279
|
+
die(
|
|
2280
|
+
`${gate.name} failed: missing script "${gateScriptName}" in package.json (--strict mode requires all gate scripts)`
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
if (gate.run) {
|
|
2284
|
+
result = await gate.run({ agentLog, useAgentMode, cwd });
|
|
2285
|
+
if (gate.name === GATE_NAMES.FORMAT_CHECK) {
|
|
2286
|
+
lastFormatCheckFiles = result.filesChecked ?? null;
|
|
2287
|
+
}
|
|
2288
|
+
} else if (gate.cmd === GATE_COMMANDS.INVARIANTS) {
|
|
2289
|
+
const logLine = useAgentMode ? (line) => writeSync3(agentLog.logFd, `${line}
|
|
2290
|
+
`) : (line) => console.log(line);
|
|
2291
|
+
logLine("\n> Invariants check\n");
|
|
2292
|
+
const invariantsResult = runInvariants({ baseDir: cwd, silent: false });
|
|
2293
|
+
result = {
|
|
2294
|
+
ok: invariantsResult.success,
|
|
2295
|
+
duration: 0
|
|
2296
|
+
// runInvariants doesn't track duration
|
|
2297
|
+
};
|
|
2298
|
+
if (!result.ok) {
|
|
2299
|
+
logLine("");
|
|
2300
|
+
logLine(invariantsResult.formatted);
|
|
2301
|
+
}
|
|
2302
|
+
} else if (gate.cmd === GATE_COMMANDS.INCREMENTAL) {
|
|
2303
|
+
result = await runIncrementalLint({ agentLog, cwd });
|
|
2304
|
+
} else if (gate.cmd === GATE_COMMANDS.SAFETY_CRITICAL_TEST) {
|
|
2305
|
+
result = await runSafetyCriticalTests({ agentLog, cwd });
|
|
2306
|
+
} else if (gate.cmd === GATE_COMMANDS.INCREMENTAL_TEST) {
|
|
2307
|
+
result = await runChangedTests({
|
|
2308
|
+
agentLog,
|
|
2309
|
+
cwd,
|
|
2310
|
+
scopedTestPaths: opts.scopedTestPaths
|
|
2311
|
+
});
|
|
2312
|
+
lastTestResult = result;
|
|
2313
|
+
} else if (gate.cmd === GATE_COMMANDS.TIERED_TEST) {
|
|
2314
|
+
result = await runIntegrationTests({ agentLog, cwd });
|
|
2315
|
+
} else if (gate.cmd === GATE_COMMANDS.COVERAGE_GATE) {
|
|
2316
|
+
if (!isFullCoverage && lastTestResult?.isIncremental) {
|
|
2317
|
+
const msg = "\u23ED\uFE0F Skipping coverage gate (changed tests - coverage is partial)";
|
|
2318
|
+
if (!useAgentMode) {
|
|
2319
|
+
console.log(`
|
|
2320
|
+
${msg}
|
|
2321
|
+
`);
|
|
2322
|
+
} else {
|
|
2323
|
+
writeSync3(agentLog.logFd, `
|
|
2324
|
+
${msg}
|
|
2325
|
+
|
|
2326
|
+
`);
|
|
2327
|
+
}
|
|
2328
|
+
gateResults.push({
|
|
2329
|
+
name: gate.name,
|
|
2330
|
+
status: "skipped",
|
|
2331
|
+
durationMs: 0,
|
|
2332
|
+
reason: "changed tests - coverage is partial"
|
|
2333
|
+
});
|
|
2334
|
+
continue;
|
|
2335
|
+
}
|
|
2336
|
+
if (!useAgentMode) {
|
|
2337
|
+
console.log(
|
|
2338
|
+
`
|
|
2339
|
+
> Coverage gate (mode: ${coverageMode}, threshold: ${coverageThreshold}%)
|
|
2340
|
+
`
|
|
2341
|
+
);
|
|
2342
|
+
} else {
|
|
2343
|
+
writeSync3(
|
|
2344
|
+
agentLog.logFd,
|
|
2345
|
+
`
|
|
2346
|
+
> Coverage gate (mode: ${coverageMode}, threshold: ${coverageThreshold}%)
|
|
2347
|
+
|
|
2348
|
+
`
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
result = await runCoverageGate({
|
|
2352
|
+
mode: coverageMode,
|
|
2353
|
+
// WU-1262: Pass resolved threshold from methodology policy
|
|
2354
|
+
threshold: coverageThreshold,
|
|
2355
|
+
logger: useAgentMode ? {
|
|
2356
|
+
log: (msg) => {
|
|
2357
|
+
writeSync3(agentLog.logFd, `${msg}
|
|
2358
|
+
`);
|
|
2359
|
+
}
|
|
2360
|
+
} : console
|
|
2361
|
+
});
|
|
2362
|
+
} else if (gate.cmd === GATE_COMMANDS.ONBOARDING_SMOKE_TEST) {
|
|
2363
|
+
const logLine = useAgentMode ? (line) => writeSync3(agentLog.logFd, `${line}
|
|
2364
|
+
`) : (line) => console.log(line);
|
|
2365
|
+
logLine("\n> Onboarding smoke test\n");
|
|
2366
|
+
result = await runOnboardingSmokeTestGate({
|
|
2367
|
+
logger: { log: logLine }
|
|
2368
|
+
});
|
|
2369
|
+
} else {
|
|
2370
|
+
if (!gate.cmd) {
|
|
2371
|
+
die(`${gate.name} failed: gate command is not configured`);
|
|
2372
|
+
}
|
|
2373
|
+
result = run(gate.cmd, { agentLog, cwd });
|
|
2374
|
+
}
|
|
2375
|
+
emitGateEvent({
|
|
2376
|
+
wu_id,
|
|
2377
|
+
lane,
|
|
2378
|
+
gate_name: gate.name,
|
|
2379
|
+
passed: result.ok,
|
|
2380
|
+
duration_ms: result.duration
|
|
2381
|
+
});
|
|
2382
|
+
if (!result.ok) {
|
|
2383
|
+
if (gate.warnOnly) {
|
|
2384
|
+
const warnMsg = `\u26A0\uFE0F ${gate.name} failed (warn-only, not blocking)`;
|
|
2385
|
+
if (!useAgentMode) {
|
|
2386
|
+
console.log(`
|
|
2387
|
+
${warnMsg}
|
|
2388
|
+
`);
|
|
2389
|
+
} else {
|
|
2390
|
+
writeSync3(agentLog.logFd, `
|
|
2391
|
+
${warnMsg}
|
|
2392
|
+
|
|
2393
|
+
`);
|
|
2394
|
+
}
|
|
2395
|
+
gateResults.push({
|
|
2396
|
+
name: gate.name,
|
|
2397
|
+
status: "warned",
|
|
2398
|
+
durationMs: result.duration
|
|
2399
|
+
});
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
if (gate.name === GATE_NAMES.FORMAT_CHECK) {
|
|
2403
|
+
emitFormatCheckGuidance({ agentLog, useAgentMode, files: lastFormatCheckFiles, cwd });
|
|
2404
|
+
}
|
|
2405
|
+
gateResults.push({
|
|
2406
|
+
name: gate.name,
|
|
2407
|
+
status: "failed",
|
|
2408
|
+
durationMs: result.duration
|
|
2409
|
+
});
|
|
2410
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode, cwd });
|
|
2411
|
+
logLine(`
|
|
2412
|
+
${formatGateSummary(gateResults)}
|
|
2413
|
+
`);
|
|
2414
|
+
if (useAgentMode) {
|
|
2415
|
+
const tail = readLogTail(agentLog.logPath);
|
|
2416
|
+
console.error(`
|
|
2417
|
+
\u274C ${gate.name} failed (agent mode). Log: ${agentLog.logPath}
|
|
2418
|
+
`);
|
|
2419
|
+
if (tail) {
|
|
2420
|
+
console.error(`Last log lines:
|
|
2421
|
+
${tail}
|
|
2422
|
+
`);
|
|
2423
|
+
}
|
|
2424
|
+
}
|
|
2425
|
+
await flushTelemetryToCloud();
|
|
2426
|
+
die(`${gate.name} failed`);
|
|
2427
|
+
}
|
|
2428
|
+
gateResults.push({
|
|
2429
|
+
name: gate.name,
|
|
2430
|
+
status: "passed",
|
|
2431
|
+
durationMs: result.duration
|
|
2432
|
+
});
|
|
2433
|
+
}
|
|
2434
|
+
if (agentLog) {
|
|
2435
|
+
updateGatesLatestSymlink({ logPath: agentLog.logPath, cwd, env: process.env });
|
|
2436
|
+
}
|
|
2437
|
+
const summaryLogLine = makeGateLogger({ agentLog, useAgentMode, cwd });
|
|
2438
|
+
summaryLogLine(`
|
|
2439
|
+
${formatGateSummary(gateResults)}`);
|
|
2440
|
+
if (!useAgentMode) {
|
|
2441
|
+
console.log(`
|
|
2442
|
+
${chalk.green("\u2705 All gates passed!")}
|
|
2443
|
+
`);
|
|
2444
|
+
} else {
|
|
2445
|
+
console.log(
|
|
2446
|
+
`${chalk.green("\u2705 All gates passed")} (agent mode). Log: ${agentLog.logPath}
|
|
2447
|
+
`
|
|
2448
|
+
);
|
|
2449
|
+
}
|
|
2450
|
+
await flushTelemetryToCloud();
|
|
2451
|
+
return true;
|
|
2452
|
+
}
|
|
2453
|
+
async function main() {
|
|
2454
|
+
const opts = parseGatesOptions();
|
|
2455
|
+
const ok = await executeGates({ ...opts, argv: process.argv.slice(2) });
|
|
2456
|
+
if (!ok) {
|
|
2457
|
+
process.exit(EXIT_CODES.ERROR);
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
if (import.meta.main) {
|
|
2461
|
+
void runCLI(main);
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
export {
|
|
2465
|
+
resolveWuDonePreCommitGateDecision,
|
|
2466
|
+
parsePrettierListOutput,
|
|
2467
|
+
buildPrettierWriteCommand,
|
|
2468
|
+
formatFormatCheckGuidance,
|
|
2469
|
+
parseWUFromBranchName,
|
|
2470
|
+
extractPackagesFromCodePaths,
|
|
2471
|
+
loadCurrentWUCodePaths,
|
|
2472
|
+
isPrettierConfigFile,
|
|
2473
|
+
isTestConfigFile,
|
|
2474
|
+
resolveFormatCheckPlan,
|
|
2475
|
+
resolveLintPlan,
|
|
2476
|
+
resolveTestPlan,
|
|
2477
|
+
resolveDocsOnlyTestPlan,
|
|
2478
|
+
formatDocsOnlySkipMessage,
|
|
2479
|
+
resolveSpecLinterPlan,
|
|
2480
|
+
GATES_OPTIONS,
|
|
2481
|
+
parseGatesOptions,
|
|
2482
|
+
runGates,
|
|
2483
|
+
syncGatesTelemetryToCloud,
|
|
2484
|
+
main
|
|
2485
|
+
};
|