@lumenflow/cli 1.0.0
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/LICENSE +190 -0
- package/README.md +116 -0
- package/dist/gates.d.ts +41 -0
- package/dist/gates.d.ts.map +1 -0
- package/dist/gates.js +684 -0
- package/dist/gates.js.map +1 -0
- package/dist/initiative-add-wu.d.ts +22 -0
- package/dist/initiative-add-wu.d.ts.map +1 -0
- package/dist/initiative-add-wu.js +234 -0
- package/dist/initiative-add-wu.js.map +1 -0
- package/dist/initiative-create.d.ts +28 -0
- package/dist/initiative-create.d.ts.map +1 -0
- package/dist/initiative-create.js +172 -0
- package/dist/initiative-create.js.map +1 -0
- package/dist/initiative-edit.d.ts +34 -0
- package/dist/initiative-edit.d.ts.map +1 -0
- package/dist/initiative-edit.js +440 -0
- package/dist/initiative-edit.js.map +1 -0
- package/dist/initiative-list.d.ts +12 -0
- package/dist/initiative-list.d.ts.map +1 -0
- package/dist/initiative-list.js +101 -0
- package/dist/initiative-list.js.map +1 -0
- package/dist/initiative-status.d.ts +11 -0
- package/dist/initiative-status.d.ts.map +1 -0
- package/dist/initiative-status.js +221 -0
- package/dist/initiative-status.js.map +1 -0
- package/dist/mem-checkpoint.d.ts +16 -0
- package/dist/mem-checkpoint.d.ts.map +1 -0
- package/dist/mem-checkpoint.js +237 -0
- package/dist/mem-checkpoint.js.map +1 -0
- package/dist/mem-cleanup.d.ts +29 -0
- package/dist/mem-cleanup.d.ts.map +1 -0
- package/dist/mem-cleanup.js +267 -0
- package/dist/mem-cleanup.js.map +1 -0
- package/dist/mem-create.d.ts +17 -0
- package/dist/mem-create.d.ts.map +1 -0
- package/dist/mem-create.js +265 -0
- package/dist/mem-create.js.map +1 -0
- package/dist/mem-inbox.d.ts +35 -0
- package/dist/mem-inbox.d.ts.map +1 -0
- package/dist/mem-inbox.js +373 -0
- package/dist/mem-inbox.js.map +1 -0
- package/dist/mem-init.d.ts +15 -0
- package/dist/mem-init.d.ts.map +1 -0
- package/dist/mem-init.js +146 -0
- package/dist/mem-init.js.map +1 -0
- package/dist/mem-ready.d.ts +16 -0
- package/dist/mem-ready.d.ts.map +1 -0
- package/dist/mem-ready.js +224 -0
- package/dist/mem-ready.js.map +1 -0
- package/dist/mem-signal.d.ts +16 -0
- package/dist/mem-signal.d.ts.map +1 -0
- package/dist/mem-signal.js +204 -0
- package/dist/mem-signal.js.map +1 -0
- package/dist/mem-start.d.ts +16 -0
- package/dist/mem-start.d.ts.map +1 -0
- package/dist/mem-start.js +158 -0
- package/dist/mem-start.js.map +1 -0
- package/dist/mem-summarize.d.ts +22 -0
- package/dist/mem-summarize.d.ts.map +1 -0
- package/dist/mem-summarize.js +213 -0
- package/dist/mem-summarize.js.map +1 -0
- package/dist/mem-triage.d.ts +22 -0
- package/dist/mem-triage.d.ts.map +1 -0
- package/dist/mem-triage.js +328 -0
- package/dist/mem-triage.js.map +1 -0
- package/dist/spawn-list.d.ts +16 -0
- package/dist/spawn-list.d.ts.map +1 -0
- package/dist/spawn-list.js +140 -0
- package/dist/spawn-list.js.map +1 -0
- package/dist/wu-block.d.ts +16 -0
- package/dist/wu-block.d.ts.map +1 -0
- package/dist/wu-block.js +241 -0
- package/dist/wu-block.js.map +1 -0
- package/dist/wu-claim.d.ts +32 -0
- package/dist/wu-claim.d.ts.map +1 -0
- package/dist/wu-claim.js +1106 -0
- package/dist/wu-claim.js.map +1 -0
- package/dist/wu-cleanup.d.ts +17 -0
- package/dist/wu-cleanup.d.ts.map +1 -0
- package/dist/wu-cleanup.js +194 -0
- package/dist/wu-cleanup.js.map +1 -0
- package/dist/wu-create.d.ts +38 -0
- package/dist/wu-create.d.ts.map +1 -0
- package/dist/wu-create.js +520 -0
- package/dist/wu-create.js.map +1 -0
- package/dist/wu-deps.d.ts +13 -0
- package/dist/wu-deps.d.ts.map +1 -0
- package/dist/wu-deps.js +119 -0
- package/dist/wu-deps.js.map +1 -0
- package/dist/wu-done.d.ts +153 -0
- package/dist/wu-done.d.ts.map +1 -0
- package/dist/wu-done.js +2096 -0
- package/dist/wu-done.js.map +1 -0
- package/dist/wu-edit.d.ts +29 -0
- package/dist/wu-edit.d.ts.map +1 -0
- package/dist/wu-edit.js +852 -0
- package/dist/wu-edit.js.map +1 -0
- package/dist/wu-infer-lane.d.ts +17 -0
- package/dist/wu-infer-lane.d.ts.map +1 -0
- package/dist/wu-infer-lane.js +135 -0
- package/dist/wu-infer-lane.js.map +1 -0
- package/dist/wu-preflight.d.ts +47 -0
- package/dist/wu-preflight.d.ts.map +1 -0
- package/dist/wu-preflight.js +167 -0
- package/dist/wu-preflight.js.map +1 -0
- package/dist/wu-prune.d.ts +16 -0
- package/dist/wu-prune.d.ts.map +1 -0
- package/dist/wu-prune.js +259 -0
- package/dist/wu-prune.js.map +1 -0
- package/dist/wu-repair.d.ts +60 -0
- package/dist/wu-repair.d.ts.map +1 -0
- package/dist/wu-repair.js +226 -0
- package/dist/wu-repair.js.map +1 -0
- package/dist/wu-spawn-completion.d.ts +10 -0
- package/dist/wu-spawn-completion.js +30 -0
- package/dist/wu-spawn.d.ts +168 -0
- package/dist/wu-spawn.d.ts.map +1 -0
- package/dist/wu-spawn.js +1327 -0
- package/dist/wu-spawn.js.map +1 -0
- package/dist/wu-unblock.d.ts +16 -0
- package/dist/wu-unblock.d.ts.map +1 -0
- package/dist/wu-unblock.js +234 -0
- package/dist/wu-unblock.js.map +1 -0
- package/dist/wu-validate.d.ts +16 -0
- package/dist/wu-validate.d.ts.map +1 -0
- package/dist/wu-validate.js +193 -0
- package/dist/wu-validate.js.map +1 -0
- package/package.json +92 -0
package/dist/gates.js
ADDED
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Quality Gates Runner
|
|
4
|
+
*
|
|
5
|
+
* Runs quality gates with support for docs-only mode and incremental linting.
|
|
6
|
+
*
|
|
7
|
+
* WU-1304: Optimise ESLint gates performance
|
|
8
|
+
* - Uses incremental linting (only files changed since branching from main)
|
|
9
|
+
* - Full lint coverage maintained via CI workflow
|
|
10
|
+
*
|
|
11
|
+
* WU-1433: Coverage gate with mode flag
|
|
12
|
+
* - Checks coverage thresholds for hex core files (ā„90% for application layer)
|
|
13
|
+
* - Mode: block (default) fails the gate, warn logs warnings only
|
|
14
|
+
* WU-2334: Changed default from warn to block for TDD enforcement
|
|
15
|
+
*
|
|
16
|
+
* WU-1610: Supabase docs linter
|
|
17
|
+
* - Verifies every table in migrations is documented in schema.md
|
|
18
|
+
* - Fails if any table is missing documentation
|
|
19
|
+
*
|
|
20
|
+
* For type:documentation WUs:
|
|
21
|
+
* - ā
Run: format:check, spec:linter, prompts:lint, backlog-sync
|
|
22
|
+
* - ā Skip: lint, typecheck, supabase-docs:linter, tests, coverage (no code changed)
|
|
23
|
+
*
|
|
24
|
+
* WU-1920: Incremental test execution
|
|
25
|
+
* - Uses Vitest's --changed flag to run only tests for changed files
|
|
26
|
+
* - Full test suite maintained via CI workflow and --full-tests flag
|
|
27
|
+
*
|
|
28
|
+
* WU-2062: Tiered test execution for faster wu:done
|
|
29
|
+
* - Safety-critical tests (PHI, escalation, red-flag) ALWAYS run
|
|
30
|
+
* - Docs-only WUs: lint/typecheck only (auto-detected or --docs-only flag)
|
|
31
|
+
* - High-risk WUs (auth, PHI, RLS, migrations): run integration tests
|
|
32
|
+
* - Standard WUs: changed tests + safety-critical tests
|
|
33
|
+
*
|
|
34
|
+
* Usage:
|
|
35
|
+
* node tools/gates.mjs # Tiered gates (default)
|
|
36
|
+
* node tools/gates.mjs --docs-only # Docs-only gates
|
|
37
|
+
* node tools/gates.mjs --full-lint # Full lint (bypass incremental)
|
|
38
|
+
* node tools/gates.mjs --full-tests # Full tests (bypass incremental)
|
|
39
|
+
* node tools/gates.mjs --coverage-mode=block # Coverage gate in block mode
|
|
40
|
+
*/
|
|
41
|
+
import { execSync, spawnSync } from 'node:child_process';
|
|
42
|
+
import { closeSync, mkdirSync, openSync, readSync, statSync, writeSync } from 'node:fs';
|
|
43
|
+
import { access } from 'node:fs/promises';
|
|
44
|
+
import path from 'node:path';
|
|
45
|
+
import { emitGateEvent, getCurrentWU, getCurrentLane } from '@lumenflow/core/dist/telemetry.js';
|
|
46
|
+
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
47
|
+
import { getChangedLintableFiles, convertToPackageRelativePaths, } from '@lumenflow/core/dist/incremental-lint.js';
|
|
48
|
+
import { buildVitestChangedArgs, isCodeFilePath } from '@lumenflow/core/dist/incremental-test.js';
|
|
49
|
+
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
50
|
+
import { runCoverageGate, COVERAGE_GATE_MODES } from '@lumenflow/core/dist/coverage-gate.js';
|
|
51
|
+
import { buildGatesLogPath, shouldUseGatesAgentMode, updateGatesLatestSymlink, } from '@lumenflow/core/dist/gates-agent-mode.js';
|
|
52
|
+
// WU-2062: Import risk detector for tiered test execution
|
|
53
|
+
// eslint-disable-next-line no-unused-vars -- Pre-existing: SAFETY_CRITICAL_TEST_PATTERNS imported for future use
|
|
54
|
+
import { detectRiskTier, RISK_TIERS, } from '@lumenflow/core/dist/risk-detector.js';
|
|
55
|
+
// WU-2252: Import invariants runner for first-check validation
|
|
56
|
+
import { runInvariants } from '@lumenflow/core/dist/invariants-runner.js';
|
|
57
|
+
import { Command } from 'commander';
|
|
58
|
+
import { BRANCHES, PACKAGES, PKG_MANAGER, PKG_FLAGS, ESLINT_FLAGS, ESLINT_COMMANDS, ESLINT_DEFAULTS, SCRIPTS, CACHE_STRATEGIES, DIRECTORIES, GATE_NAMES, GATE_COMMANDS, TOOL_PATHS, CLI_MODES, EXIT_CODES, FILE_SYSTEM, } from '@lumenflow/core/dist/wu-constants.js';
|
|
59
|
+
// WU-2457: Add Commander.js for --help support
|
|
60
|
+
// WU-2465: Pre-filter argv to handle pnpm's `--` separator
|
|
61
|
+
// When invoked via `pnpm gates -- --docs-only`, pnpm passes ["--", "--docs-only"]
|
|
62
|
+
// Commander treats `--` as "everything after is positional", causing errors.
|
|
63
|
+
// Solution: Remove standalone `--` from argv before parsing.
|
|
64
|
+
const filteredArgv = process.argv.filter((arg, index, arr) => {
|
|
65
|
+
// Keep `--` only if it's followed by a non-option (actual positional arg)
|
|
66
|
+
// Remove it if it's followed by an option (starts with -)
|
|
67
|
+
if (arg === '--') {
|
|
68
|
+
const nextArg = arr[index + 1];
|
|
69
|
+
return nextArg && !nextArg.startsWith('-');
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
const program = new Command()
|
|
74
|
+
.name('gates')
|
|
75
|
+
.description('Run quality gates with support for docs-only mode, incremental linting, and tiered testing')
|
|
76
|
+
.option('--docs-only', 'Run docs-only gates (format, spec-linter, prompts-lint, backlog-sync)')
|
|
77
|
+
.option('--full-lint', 'Run full lint instead of incremental')
|
|
78
|
+
.option('--full-tests', 'Run full test suite instead of incremental')
|
|
79
|
+
.option('--full-coverage', 'Force full test suite and coverage gate (implies --full-tests)')
|
|
80
|
+
.option('--coverage-mode <mode>', 'Coverage gate mode: "warn" logs warnings, "block" fails gate (default)', 'block')
|
|
81
|
+
.option('--verbose', 'Stream output in agent mode instead of logging to file')
|
|
82
|
+
.helpOption('-h, --help', 'Display help for command');
|
|
83
|
+
program.parse(filteredArgv);
|
|
84
|
+
const opts = program.opts();
|
|
85
|
+
// Parse command line arguments (now via Commander)
|
|
86
|
+
const isDocsOnly = opts.docsOnly || false;
|
|
87
|
+
const isFullLint = opts.fullLint || false;
|
|
88
|
+
const isFullTests = opts.fullTests || false;
|
|
89
|
+
// WU-2244: Full coverage flag forces full test suite and coverage gate (deterministic)
|
|
90
|
+
const isFullCoverage = opts.fullCoverage || false;
|
|
91
|
+
// WU-1433: Coverage gate mode (warn or block)
|
|
92
|
+
// WU-2334: Default changed from WARN to BLOCK for TDD enforcement
|
|
93
|
+
const coverageMode = opts.coverageMode || COVERAGE_GATE_MODES.BLOCK;
|
|
94
|
+
/**
|
|
95
|
+
* Build a pnpm command string
|
|
96
|
+
*/
|
|
97
|
+
function pnpmCmd(...parts) {
|
|
98
|
+
return `${PKG_MANAGER} ${parts.join(' ')}`;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Build a pnpm run command string
|
|
102
|
+
*/
|
|
103
|
+
function pnpmRun(script, ...args) {
|
|
104
|
+
const argsStr = args.length > 0 ? ` ${args.join(' ')}` : '';
|
|
105
|
+
return `${PKG_MANAGER} ${SCRIPTS.RUN} ${script}${argsStr}`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Build a pnpm --filter command string
|
|
109
|
+
*/
|
|
110
|
+
function pnpmFilter(pkg, script) {
|
|
111
|
+
return `${PKG_MANAGER} ${PKG_FLAGS.FILTER} ${pkg} ${script}`;
|
|
112
|
+
}
|
|
113
|
+
function readLogTail(logPath, { maxLines = 40, maxBytes = 64 * 1024 } = {}) {
|
|
114
|
+
try {
|
|
115
|
+
const stats = statSync(logPath);
|
|
116
|
+
const startPos = Math.max(0, stats.size - maxBytes);
|
|
117
|
+
const bytesToRead = stats.size - startPos;
|
|
118
|
+
const fd = openSync(logPath, 'r');
|
|
119
|
+
try {
|
|
120
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
121
|
+
readSync(fd, buffer, 0, bytesToRead, startPos);
|
|
122
|
+
const text = buffer.toString(FILE_SYSTEM.ENCODING);
|
|
123
|
+
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
124
|
+
return lines.slice(-maxLines).join('\n');
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
closeSync(fd);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function createAgentLogContext({ wuId, lane }) {
|
|
135
|
+
const cwd = process.cwd();
|
|
136
|
+
const logPath = buildGatesLogPath({ cwd, env: process.env, wuId, lane });
|
|
137
|
+
mkdirSync(path.dirname(logPath), { recursive: true });
|
|
138
|
+
const logFd = openSync(logPath, 'a');
|
|
139
|
+
const header = `# gates log\n# lane: ${lane || 'unknown'}\n# wu: ${wuId || 'unknown'}\n# started: ${new Date().toISOString()}\n\n`;
|
|
140
|
+
writeSync(logFd, header);
|
|
141
|
+
// Ensure we close the FD even if gates exit via die().
|
|
142
|
+
process.on('exit', () => {
|
|
143
|
+
try {
|
|
144
|
+
closeSync(logFd);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// ignore
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return { logPath, logFd };
|
|
151
|
+
}
|
|
152
|
+
function run(cmd, { agentLog } = {}) {
|
|
153
|
+
const start = Date.now();
|
|
154
|
+
if (!agentLog) {
|
|
155
|
+
console.log(`\n> ${cmd}\n`);
|
|
156
|
+
try {
|
|
157
|
+
execSync(cmd, { stdio: 'inherit', encoding: FILE_SYSTEM.ENCODING });
|
|
158
|
+
return { ok: true, duration: Date.now() - start };
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return { ok: false, duration: Date.now() - start };
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
writeSync(agentLog.logFd, `\n> ${cmd}\n\n`);
|
|
165
|
+
const result = spawnSync(cmd, [], {
|
|
166
|
+
shell: true,
|
|
167
|
+
stdio: ['ignore', agentLog.logFd, agentLog.logFd],
|
|
168
|
+
cwd: process.cwd(),
|
|
169
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
170
|
+
});
|
|
171
|
+
return { ok: result.status === EXIT_CODES.SUCCESS, duration: Date.now() - start };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Run incremental ESLint on changed files only
|
|
175
|
+
* Falls back to full lint if on main branch or if incremental fails
|
|
176
|
+
* @returns {{ ok: boolean, duration: number, fileCount: number }}
|
|
177
|
+
*/
|
|
178
|
+
async function runIncrementalLint({ agentLog, } = {}) {
|
|
179
|
+
const start = Date.now();
|
|
180
|
+
const logLine = (line) => {
|
|
181
|
+
if (!agentLog) {
|
|
182
|
+
console.log(line);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
writeSync(agentLog.logFd, `${line}\n`);
|
|
186
|
+
};
|
|
187
|
+
try {
|
|
188
|
+
// Check if we're on main branch
|
|
189
|
+
const git = getGitForCwd();
|
|
190
|
+
const currentBranch = await git.getCurrentBranch();
|
|
191
|
+
if (currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER) {
|
|
192
|
+
logLine('š On main branch - running full lint');
|
|
193
|
+
const result = run(pnpmFilter(PACKAGES.WEB, SCRIPTS.LINT), { agentLog });
|
|
194
|
+
return { ...result, fileCount: -1 };
|
|
195
|
+
}
|
|
196
|
+
// Get changed files in apps/web
|
|
197
|
+
const changedFiles = await getChangedLintableFiles({
|
|
198
|
+
git,
|
|
199
|
+
filterPath: DIRECTORIES.APPS_WEB,
|
|
200
|
+
});
|
|
201
|
+
if (changedFiles.length === 0) {
|
|
202
|
+
logLine('\n> ESLint (incremental)\n');
|
|
203
|
+
logLine('ā
No lintable files changed - skipping lint');
|
|
204
|
+
return { ok: true, duration: Date.now() - start, fileCount: 0 };
|
|
205
|
+
}
|
|
206
|
+
// Filter to files that still exist (in case of deletions)
|
|
207
|
+
const existingFiles = (await Promise.all(changedFiles.map(async (f) => {
|
|
208
|
+
try {
|
|
209
|
+
await access(f);
|
|
210
|
+
return f;
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}))).filter(Boolean);
|
|
216
|
+
if (existingFiles.length === 0) {
|
|
217
|
+
logLine('\n> ESLint (incremental)\n');
|
|
218
|
+
logLine('ā
All changed files were deleted - skipping lint');
|
|
219
|
+
return { ok: true, duration: Date.now() - start, fileCount: 0 };
|
|
220
|
+
}
|
|
221
|
+
// WU-2571: Convert repo-relative paths to package-relative paths
|
|
222
|
+
// ESLint runs from apps/web/ where repo-relative paths don't exist
|
|
223
|
+
const packageRelativeFiles = convertToPackageRelativePaths(existingFiles, DIRECTORIES.APPS_WEB);
|
|
224
|
+
logLine(`\n> ESLint (incremental: ${packageRelativeFiles.length} files)\n`);
|
|
225
|
+
logLine(`Files to lint:\n ${packageRelativeFiles.join('\n ')}\n`);
|
|
226
|
+
// WU-2571: Run ESLint from apps/web directory with package-relative paths
|
|
227
|
+
const webDir = path.join(process.cwd(), DIRECTORIES.APPS_WEB);
|
|
228
|
+
const result = spawnSync(PKG_MANAGER, [
|
|
229
|
+
ESLINT_COMMANDS.ESLINT,
|
|
230
|
+
ESLINT_FLAGS.MAX_WARNINGS,
|
|
231
|
+
ESLINT_DEFAULTS.MAX_WARNINGS,
|
|
232
|
+
ESLINT_FLAGS.NO_WARN_IGNORED,
|
|
233
|
+
ESLINT_FLAGS.CACHE,
|
|
234
|
+
ESLINT_FLAGS.CACHE_STRATEGY,
|
|
235
|
+
CACHE_STRATEGIES.CONTENT,
|
|
236
|
+
ESLINT_FLAGS.CACHE_LOCATION,
|
|
237
|
+
'.eslintcache',
|
|
238
|
+
ESLINT_FLAGS.PASS_ON_UNPRUNED,
|
|
239
|
+
...packageRelativeFiles,
|
|
240
|
+
], agentLog
|
|
241
|
+
? {
|
|
242
|
+
stdio: ['ignore', agentLog.logFd, agentLog.logFd],
|
|
243
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
244
|
+
cwd: webDir,
|
|
245
|
+
}
|
|
246
|
+
: {
|
|
247
|
+
stdio: 'inherit',
|
|
248
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
249
|
+
cwd: webDir,
|
|
250
|
+
});
|
|
251
|
+
const duration = Date.now() - start;
|
|
252
|
+
return {
|
|
253
|
+
ok: result.status === EXIT_CODES.SUCCESS,
|
|
254
|
+
duration,
|
|
255
|
+
fileCount: packageRelativeFiles.length,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
console.error('ā ļø Incremental lint failed, falling back to full lint:', error.message);
|
|
260
|
+
const result = run(pnpmFilter(PACKAGES.WEB, SCRIPTS.LINT), { agentLog });
|
|
261
|
+
return { ...result, fileCount: -1 };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Run changed tests using Vitest's --changed flag from the repo root.
|
|
266
|
+
* Falls back to full test suite if on main branch or if the run fails.
|
|
267
|
+
*
|
|
268
|
+
* @returns {{ ok: boolean, duration: number, isIncremental: boolean }}
|
|
269
|
+
*/
|
|
270
|
+
async function runChangedTests({ agentLog, } = {}) {
|
|
271
|
+
const start = Date.now();
|
|
272
|
+
// eslint-disable-next-line sonarjs/no-identical-functions -- Pre-existing: logLine helper duplicated across gate runners
|
|
273
|
+
const logLine = (line) => {
|
|
274
|
+
if (!agentLog) {
|
|
275
|
+
console.log(line);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
writeSync(agentLog.logFd, `${line}\n`);
|
|
279
|
+
};
|
|
280
|
+
try {
|
|
281
|
+
const git = getGitForCwd();
|
|
282
|
+
const currentBranch = await git.getCurrentBranch();
|
|
283
|
+
if (currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER) {
|
|
284
|
+
logLine('š On main branch - running full test suite');
|
|
285
|
+
const result = run(pnpmCmd('turbo', 'run', 'test'), { agentLog });
|
|
286
|
+
return { ...result, isIncremental: false };
|
|
287
|
+
}
|
|
288
|
+
const untrackedOutput = await git.raw(['ls-files', '--others', '--exclude-standard']);
|
|
289
|
+
const untrackedFiles = untrackedOutput
|
|
290
|
+
.split(/\r?\n/)
|
|
291
|
+
.map((f) => f.trim())
|
|
292
|
+
.filter(Boolean);
|
|
293
|
+
const untrackedCodeFiles = untrackedFiles.filter(isCodeFilePath);
|
|
294
|
+
if (untrackedCodeFiles.length > 0) {
|
|
295
|
+
const preview = untrackedCodeFiles.slice(0, 5).join(', ');
|
|
296
|
+
logLine(`ā ļø Untracked code files detected (${untrackedCodeFiles.length}): ${preview}${untrackedCodeFiles.length > 5 ? '...' : ''}`);
|
|
297
|
+
logLine('š Running full test suite to avoid missing coverage');
|
|
298
|
+
const result = run(pnpmCmd('turbo', 'run', 'test'), { agentLog });
|
|
299
|
+
return { ...result, duration: Date.now() - start, isIncremental: false };
|
|
300
|
+
}
|
|
301
|
+
logLine('\n> Vitest (changed: tools project)\n');
|
|
302
|
+
const toolsArgs = ['--project', 'tools', ...buildVitestChangedArgs()];
|
|
303
|
+
const toolsResult = run(pnpmCmd('vitest', ...toolsArgs), { agentLog });
|
|
304
|
+
if (!toolsResult.ok) {
|
|
305
|
+
return { ...toolsResult, duration: Date.now() - start, isIncremental: true };
|
|
306
|
+
}
|
|
307
|
+
logLine('\n> Vitest (changed: turbo --affected)\n');
|
|
308
|
+
const result = run(pnpmCmd('turbo', 'run', 'test:changed', '--affected'), { agentLog });
|
|
309
|
+
return { ...result, duration: Date.now() - start, isIncremental: true };
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
console.error('ā ļø Changed tests failed, falling back to full suite:', error.message);
|
|
313
|
+
const result = run(pnpmCmd('turbo', 'run', 'test'), { agentLog });
|
|
314
|
+
return { ...result, isIncremental: false };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Safety-critical test file patterns (relative to apps/web).
|
|
319
|
+
* These patterns are passed as positional arguments to vitest run.
|
|
320
|
+
* Must match the vitest include patterns in the workspace config.
|
|
321
|
+
* @type {string[]}
|
|
322
|
+
*/
|
|
323
|
+
const SAFETY_CRITICAL_TEST_FILES = [
|
|
324
|
+
// PHI protection tests
|
|
325
|
+
'src/components/ui/__tests__/PHIGuard.test.tsx',
|
|
326
|
+
'src/components/ui/__tests__/WidgetPHIConsentDialog.test.tsx',
|
|
327
|
+
'src/components/ui/__tests__/Composer.phi.test.tsx',
|
|
328
|
+
// Privacy detection tests
|
|
329
|
+
'src/lib/llm/__tests__/privacyDetector.test.ts',
|
|
330
|
+
// Escalation trigger tests
|
|
331
|
+
'src/lib/llm/__tests__/escalationTrigger.test.ts',
|
|
332
|
+
'src/components/escalation/__tests__/EscalationHistory.test.tsx',
|
|
333
|
+
// Constitutional enforcer tests
|
|
334
|
+
'src/lib/llm/__tests__/constitutionalEnforcer.test.ts',
|
|
335
|
+
// Safe prompt wrapper tests
|
|
336
|
+
'src/lib/llm/__tests__/safePromptWrapper.test.ts',
|
|
337
|
+
// Crisis/emergency handling tests
|
|
338
|
+
'src/lib/prompts/__tests__/golden-crisis.test.ts',
|
|
339
|
+
];
|
|
340
|
+
/**
|
|
341
|
+
* WU-2062: Run safety-critical tests
|
|
342
|
+
* These tests ALWAYS run regardless of which files changed.
|
|
343
|
+
* Includes: PHI, escalation, privacy, red-flag, constitutional enforcer tests
|
|
344
|
+
*
|
|
345
|
+
* Runs from apps/web directory with explicit file paths to ensure
|
|
346
|
+
* compatibility with vitest workspace include patterns.
|
|
347
|
+
*
|
|
348
|
+
* @param {object} options - Options
|
|
349
|
+
* @param {object} [options.agentLog] - Agent log context
|
|
350
|
+
* @returns {Promise<{ ok: boolean, duration: number, testCount: number }>}
|
|
351
|
+
*/
|
|
352
|
+
async function runSafetyCriticalTests({ agentLog, } = {}) {
|
|
353
|
+
const start = Date.now();
|
|
354
|
+
// eslint-disable-next-line sonarjs/no-identical-functions -- Pre-existing: logLine helper duplicated across gate runners
|
|
355
|
+
const logLine = (line) => {
|
|
356
|
+
if (!agentLog) {
|
|
357
|
+
console.log(line);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
writeSync(agentLog.logFd, `${line}\n`);
|
|
361
|
+
};
|
|
362
|
+
try {
|
|
363
|
+
logLine('\n> Safety-critical tests (always run)\n');
|
|
364
|
+
logLine(`Test files: ${SAFETY_CRITICAL_TEST_FILES.length} files\n`);
|
|
365
|
+
// Run vitest with --project web to target the web workspace
|
|
366
|
+
// Using explicit file paths for compatibility with workspace include patterns
|
|
367
|
+
const result = spawnSync(PKG_MANAGER, [
|
|
368
|
+
'vitest',
|
|
369
|
+
'run',
|
|
370
|
+
'--project',
|
|
371
|
+
PACKAGES.WEB,
|
|
372
|
+
'--reporter=verbose',
|
|
373
|
+
...SAFETY_CRITICAL_TEST_FILES,
|
|
374
|
+
'--passWithNoTests', // Don't fail if some files don't exist
|
|
375
|
+
], agentLog
|
|
376
|
+
? {
|
|
377
|
+
stdio: ['ignore', agentLog.logFd, agentLog.logFd],
|
|
378
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
379
|
+
cwd: process.cwd(),
|
|
380
|
+
}
|
|
381
|
+
: {
|
|
382
|
+
stdio: 'inherit',
|
|
383
|
+
encoding: FILE_SYSTEM.ENCODING,
|
|
384
|
+
cwd: process.cwd(),
|
|
385
|
+
});
|
|
386
|
+
const duration = Date.now() - start;
|
|
387
|
+
return {
|
|
388
|
+
ok: result.status === EXIT_CODES.SUCCESS,
|
|
389
|
+
duration,
|
|
390
|
+
testCount: SAFETY_CRITICAL_TEST_FILES.length,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
console.error('ā ļø Safety-critical tests failed:', error.message);
|
|
395
|
+
return { ok: false, duration: Date.now() - start, testCount: 0 };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* WU-2062: Run integration tests for high-risk changes
|
|
400
|
+
* Only runs when auth, PHI, RLS, or migration files are modified.
|
|
401
|
+
*
|
|
402
|
+
* @param {object} options - Options
|
|
403
|
+
* @param {object} [options.agentLog] - Agent log context
|
|
404
|
+
* @returns {Promise<{ ok: boolean, duration: number }>}
|
|
405
|
+
*/
|
|
406
|
+
async function runIntegrationTests({ agentLog, } = {}) {
|
|
407
|
+
const start = Date.now();
|
|
408
|
+
// eslint-disable-next-line sonarjs/no-identical-functions -- Pre-existing: logLine helper duplicated across gate runners
|
|
409
|
+
const logLine = (line) => {
|
|
410
|
+
if (!agentLog) {
|
|
411
|
+
console.log(line);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
writeSync(agentLog.logFd, `${line}\n`);
|
|
415
|
+
};
|
|
416
|
+
try {
|
|
417
|
+
logLine('\n> Integration tests (high-risk changes detected)\n');
|
|
418
|
+
const result = run(`RUN_INTEGRATION_TESTS=1 ${pnpmCmd('vitest', 'run', "--include='**/*.integration.*'", "--include='**/golden-*.test.*'")}`, { agentLog });
|
|
419
|
+
const duration = Date.now() - start;
|
|
420
|
+
return {
|
|
421
|
+
ok: result.ok,
|
|
422
|
+
duration,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
console.error('ā ļø Integration tests failed:', error.message);
|
|
427
|
+
return { ok: false, duration: Date.now() - start };
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async function getAllChangedFiles(options = {}) {
|
|
431
|
+
const { git = getGitForCwd() } = options;
|
|
432
|
+
try {
|
|
433
|
+
// Get merge base
|
|
434
|
+
const mergeBase = await git.mergeBase('HEAD', 'origin/main');
|
|
435
|
+
// Get committed changes
|
|
436
|
+
const committedOutput = await git.raw(['diff', '--name-only', `${mergeBase}...HEAD`]);
|
|
437
|
+
const committedFiles = committedOutput
|
|
438
|
+
.split('\n')
|
|
439
|
+
.map((f) => f.trim())
|
|
440
|
+
.filter(Boolean);
|
|
441
|
+
// Get unstaged changes
|
|
442
|
+
const unstagedOutput = await git.raw(['diff', '--name-only']);
|
|
443
|
+
const unstagedFiles = unstagedOutput
|
|
444
|
+
.split('\n')
|
|
445
|
+
.map((f) => f.trim())
|
|
446
|
+
.filter(Boolean);
|
|
447
|
+
// Get untracked files
|
|
448
|
+
const untrackedOutput = await git.raw(['ls-files', '--others', '--exclude-standard']);
|
|
449
|
+
const untrackedFiles = untrackedOutput
|
|
450
|
+
.split('\n')
|
|
451
|
+
.map((f) => f.trim())
|
|
452
|
+
.filter(Boolean);
|
|
453
|
+
// Combine and deduplicate
|
|
454
|
+
return [...new Set([...committedFiles, ...unstagedFiles, ...untrackedFiles])];
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
console.error('ā ļø Failed to get changed files:', error.message);
|
|
458
|
+
return [];
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Get context for telemetry
|
|
462
|
+
const wu_id = getCurrentWU();
|
|
463
|
+
const lane = getCurrentLane();
|
|
464
|
+
const useAgentMode = shouldUseGatesAgentMode({ argv: process.argv.slice(2), env: process.env });
|
|
465
|
+
const agentLog = useAgentMode ? createAgentLogContext({ wuId: wu_id, lane }) : null;
|
|
466
|
+
// Main execution
|
|
467
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing: main() orchestrates multi-step gate workflow
|
|
468
|
+
async function main() {
|
|
469
|
+
if (useAgentMode) {
|
|
470
|
+
console.log(`š§¾ gates (agent mode): output -> ${agentLog.logPath} (use --verbose for streaming)\n`);
|
|
471
|
+
}
|
|
472
|
+
// WU-2062: Detect risk tier from changed files (unless explicit --docs-only flag)
|
|
473
|
+
let riskTier = null;
|
|
474
|
+
let changedFiles = [];
|
|
475
|
+
if (!isDocsOnly) {
|
|
476
|
+
try {
|
|
477
|
+
changedFiles = await getAllChangedFiles();
|
|
478
|
+
riskTier = detectRiskTier({ changedFiles });
|
|
479
|
+
const logLine = useAgentMode
|
|
480
|
+
? (line) => writeSync(agentLog.logFd, `${line}\n`)
|
|
481
|
+
: (line) => console.log(line);
|
|
482
|
+
logLine(`\nšÆ Risk tier detected: ${riskTier.tier}`);
|
|
483
|
+
if (riskTier.highRiskPaths.length > 0) {
|
|
484
|
+
logLine(` High-risk paths: ${riskTier.highRiskPaths.slice(0, 3).join(', ')}${riskTier.highRiskPaths.length > 3 ? '...' : ''}`);
|
|
485
|
+
}
|
|
486
|
+
logLine('');
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
console.error('ā ļø Risk detection failed, defaulting to standard tier:', error.message);
|
|
490
|
+
riskTier = {
|
|
491
|
+
tier: RISK_TIERS.STANDARD,
|
|
492
|
+
isDocsOnly: false,
|
|
493
|
+
shouldRunIntegration: false,
|
|
494
|
+
highRiskPaths: [],
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Determine effective docs-only mode (explicit flag OR detected from changed files)
|
|
499
|
+
const effectiveDocsOnly = isDocsOnly || (riskTier && riskTier.isDocsOnly);
|
|
500
|
+
// Determine which gates to run
|
|
501
|
+
// WU-2252: Invariants gate runs FIRST and is included in both docs-only and regular modes
|
|
502
|
+
const gates = effectiveDocsOnly
|
|
503
|
+
? [
|
|
504
|
+
// WU-2252: Invariants check runs first (non-bypassable)
|
|
505
|
+
{ name: GATE_NAMES.INVARIANTS, cmd: GATE_COMMANDS.INVARIANTS },
|
|
506
|
+
{ name: GATE_NAMES.FORMAT_CHECK, cmd: pnpmCmd(SCRIPTS.FORMAT_CHECK) },
|
|
507
|
+
{ name: GATE_NAMES.SPEC_LINTER, cmd: pnpmRun(SCRIPTS.SPEC_LINTER) },
|
|
508
|
+
{
|
|
509
|
+
name: GATE_NAMES.PROMPTS_LINT,
|
|
510
|
+
cmd: pnpmRun(SCRIPTS.PROMPTS_LINT, CLI_MODES.LOCAL, '--quiet'),
|
|
511
|
+
},
|
|
512
|
+
{ name: GATE_NAMES.BACKLOG_SYNC, cmd: TOOL_PATHS.VALIDATE_BACKLOG_SYNC },
|
|
513
|
+
// WU-2315: System map validation (warn-only until orphan docs are indexed)
|
|
514
|
+
{
|
|
515
|
+
name: GATE_NAMES.SYSTEM_MAP_VALIDATE,
|
|
516
|
+
cmd: TOOL_PATHS.SYSTEM_MAP_VALIDATE,
|
|
517
|
+
warnOnly: true,
|
|
518
|
+
},
|
|
519
|
+
]
|
|
520
|
+
: [
|
|
521
|
+
// WU-2252: Invariants check runs first (non-bypassable)
|
|
522
|
+
{ name: GATE_NAMES.INVARIANTS, cmd: GATE_COMMANDS.INVARIANTS },
|
|
523
|
+
{ name: GATE_NAMES.FORMAT_CHECK, cmd: pnpmCmd(SCRIPTS.FORMAT_CHECK) },
|
|
524
|
+
{
|
|
525
|
+
name: GATE_NAMES.LINT,
|
|
526
|
+
cmd: isFullLint ? pnpmFilter(PACKAGES.WEB, SCRIPTS.LINT) : GATE_COMMANDS.INCREMENTAL,
|
|
527
|
+
},
|
|
528
|
+
{ name: GATE_NAMES.TYPECHECK, cmd: pnpmCmd(SCRIPTS.TYPECHECK) },
|
|
529
|
+
{ name: GATE_NAMES.SPEC_LINTER, cmd: pnpmRun(SCRIPTS.SPEC_LINTER) },
|
|
530
|
+
{
|
|
531
|
+
name: GATE_NAMES.PROMPTS_LINT,
|
|
532
|
+
cmd: pnpmRun(SCRIPTS.PROMPTS_LINT, CLI_MODES.LOCAL, '--quiet'),
|
|
533
|
+
},
|
|
534
|
+
{ name: GATE_NAMES.BACKLOG_SYNC, cmd: TOOL_PATHS.VALIDATE_BACKLOG_SYNC },
|
|
535
|
+
{ name: GATE_NAMES.SUPABASE_DOCS_LINTER, cmd: TOOL_PATHS.SUPABASE_DOCS_LINTER },
|
|
536
|
+
// WU-2315: System map validation (warn-only until orphan docs are indexed)
|
|
537
|
+
{
|
|
538
|
+
name: GATE_NAMES.SYSTEM_MAP_VALIDATE,
|
|
539
|
+
cmd: TOOL_PATHS.SYSTEM_MAP_VALIDATE,
|
|
540
|
+
warnOnly: true,
|
|
541
|
+
},
|
|
542
|
+
// WU-2062: Safety-critical tests ALWAYS run
|
|
543
|
+
{ name: GATE_NAMES.SAFETY_CRITICAL_TEST, cmd: GATE_COMMANDS.SAFETY_CRITICAL_TEST },
|
|
544
|
+
// WU-1920: Use changed tests by default, full suite with --full-tests
|
|
545
|
+
// WU-2244: --full-coverage implies --full-tests for accurate coverage
|
|
546
|
+
{
|
|
547
|
+
name: GATE_NAMES.TEST,
|
|
548
|
+
cmd: isFullTests || isFullCoverage
|
|
549
|
+
? pnpmCmd('turbo', 'run', 'test')
|
|
550
|
+
: GATE_COMMANDS.INCREMENTAL_TEST,
|
|
551
|
+
},
|
|
552
|
+
// WU-2062: Integration tests only for high-risk changes
|
|
553
|
+
...(riskTier && riskTier.shouldRunIntegration
|
|
554
|
+
? [{ name: GATE_NAMES.INTEGRATION_TEST, cmd: GATE_COMMANDS.TIERED_TEST }]
|
|
555
|
+
: []),
|
|
556
|
+
// WU-1433: Coverage gate with configurable mode (warn/block)
|
|
557
|
+
{ name: GATE_NAMES.COVERAGE, cmd: GATE_COMMANDS.COVERAGE_GATE },
|
|
558
|
+
];
|
|
559
|
+
if (effectiveDocsOnly) {
|
|
560
|
+
if (!useAgentMode) {
|
|
561
|
+
console.log('š Docs-only mode: skipping lint, typecheck, and tests\n');
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
writeSync(agentLog.logFd, 'š Docs-only mode: skipping lint, typecheck, and tests\n');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// Run all gates sequentially
|
|
568
|
+
// WU-1920: Track last test result to skip coverage gate on changed tests
|
|
569
|
+
let lastTestResult = null;
|
|
570
|
+
for (const gate of gates) {
|
|
571
|
+
let result;
|
|
572
|
+
if (gate.cmd === GATE_COMMANDS.INVARIANTS) {
|
|
573
|
+
// WU-2252: Invariants check runs first (non-bypassable)
|
|
574
|
+
const logLine = useAgentMode
|
|
575
|
+
? (line) => writeSync(agentLog.logFd, `${line}\n`)
|
|
576
|
+
: (line) => console.log(line);
|
|
577
|
+
logLine('\n> Invariants check\n');
|
|
578
|
+
const invariantsResult = runInvariants({ baseDir: process.cwd(), silent: false });
|
|
579
|
+
result = {
|
|
580
|
+
ok: invariantsResult.success,
|
|
581
|
+
duration: 0, // runInvariants doesn't track duration
|
|
582
|
+
};
|
|
583
|
+
if (!result.ok) {
|
|
584
|
+
logLine('');
|
|
585
|
+
logLine(invariantsResult.formatted);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
else if (gate.cmd === GATE_COMMANDS.INCREMENTAL) {
|
|
589
|
+
// Special handling for incremental lint
|
|
590
|
+
result = await runIncrementalLint({ agentLog });
|
|
591
|
+
}
|
|
592
|
+
else if (gate.cmd === GATE_COMMANDS.SAFETY_CRITICAL_TEST) {
|
|
593
|
+
// WU-2062: Safety-critical tests always run
|
|
594
|
+
result = await runSafetyCriticalTests({ agentLog });
|
|
595
|
+
}
|
|
596
|
+
else if (gate.cmd === GATE_COMMANDS.INCREMENTAL_TEST) {
|
|
597
|
+
// WU-1920: Special handling for changed tests
|
|
598
|
+
result = await runChangedTests({ agentLog });
|
|
599
|
+
lastTestResult = result;
|
|
600
|
+
}
|
|
601
|
+
else if (gate.cmd === GATE_COMMANDS.TIERED_TEST) {
|
|
602
|
+
// WU-2062: Integration tests for high-risk changes
|
|
603
|
+
result = await runIntegrationTests({ agentLog });
|
|
604
|
+
}
|
|
605
|
+
else if (gate.cmd === GATE_COMMANDS.COVERAGE_GATE) {
|
|
606
|
+
// WU-1920: Skip coverage gate when tests were changed (partial coverage)
|
|
607
|
+
// WU-2244: --full-coverage overrides incremental skip behavior
|
|
608
|
+
if (!isFullCoverage && lastTestResult?.isIncremental) {
|
|
609
|
+
const msg = 'āļø Skipping coverage gate (changed tests - coverage is partial)';
|
|
610
|
+
if (!useAgentMode) {
|
|
611
|
+
console.log(`\n${msg}\n`);
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
writeSync(agentLog.logFd, `\n${msg}\n\n`);
|
|
615
|
+
}
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
// WU-1433: Special handling for coverage gate
|
|
619
|
+
if (!useAgentMode) {
|
|
620
|
+
console.log(`\n> Coverage gate (mode: ${coverageMode})\n`);
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
writeSync(agentLog.logFd, `\n> Coverage gate (mode: ${coverageMode})\n\n`);
|
|
624
|
+
}
|
|
625
|
+
result = await runCoverageGate({
|
|
626
|
+
mode: coverageMode,
|
|
627
|
+
logger: useAgentMode
|
|
628
|
+
? {
|
|
629
|
+
log: (msg) => {
|
|
630
|
+
writeSync(agentLog.logFd, `${msg}\n`);
|
|
631
|
+
},
|
|
632
|
+
}
|
|
633
|
+
: console,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
else {
|
|
637
|
+
result = run(gate.cmd, { agentLog });
|
|
638
|
+
}
|
|
639
|
+
// Emit telemetry event
|
|
640
|
+
emitGateEvent({
|
|
641
|
+
wu_id,
|
|
642
|
+
lane,
|
|
643
|
+
gate_name: gate.name,
|
|
644
|
+
passed: result.ok,
|
|
645
|
+
duration_ms: result.duration,
|
|
646
|
+
});
|
|
647
|
+
if (!result.ok) {
|
|
648
|
+
// WU-2315: Warn-only gates log warning but don't block
|
|
649
|
+
if (gate.warnOnly) {
|
|
650
|
+
const warnMsg = `ā ļø ${gate.name} failed (warn-only, not blocking)`;
|
|
651
|
+
if (!useAgentMode) {
|
|
652
|
+
console.log(`\n${warnMsg}\n`);
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
writeSync(agentLog.logFd, `\n${warnMsg}\n\n`);
|
|
656
|
+
}
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
if (useAgentMode) {
|
|
660
|
+
const tail = readLogTail(agentLog.logPath);
|
|
661
|
+
console.error(`\nā ${gate.name} failed (agent mode). Log: ${agentLog.logPath}\n`);
|
|
662
|
+
if (tail) {
|
|
663
|
+
console.error(`Last log lines:\n${tail}\n`);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
die(`${gate.name} failed`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
// WU-2064: Create/update gates-latest.log symlink for easy agent access
|
|
670
|
+
if (agentLog) {
|
|
671
|
+
updateGatesLatestSymlink({ logPath: agentLog.logPath, cwd: process.cwd(), env: process.env });
|
|
672
|
+
}
|
|
673
|
+
if (!useAgentMode) {
|
|
674
|
+
console.log('\nā
All gates passed!\n');
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
console.log(`ā
All gates passed (agent mode). Log: ${agentLog.logPath}\n`);
|
|
678
|
+
}
|
|
679
|
+
process.exit(EXIT_CODES.SUCCESS);
|
|
680
|
+
}
|
|
681
|
+
main().catch((error) => {
|
|
682
|
+
console.error('Gates failed:', error);
|
|
683
|
+
process.exit(EXIT_CODES.ERROR);
|
|
684
|
+
});
|