@lumenflow/cli 2.2.1 → 2.3.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 +147 -57
- package/dist/__tests__/agent-log-issue.test.js +56 -0
- package/dist/__tests__/cli-entry-point.test.js +66 -17
- package/dist/__tests__/cli-subprocess.test.js +25 -0
- package/dist/__tests__/init.test.js +298 -0
- package/dist/__tests__/initiative-plan.test.js +340 -0
- package/dist/__tests__/mem-cleanup-execution.test.js +19 -0
- package/dist/__tests__/merge-block.test.js +220 -0
- package/dist/__tests__/release.test.js +28 -0
- package/dist/__tests__/safe-git.test.js +191 -0
- package/dist/__tests__/state-doctor.test.js +274 -0
- package/dist/__tests__/wu-done.test.js +36 -0
- package/dist/__tests__/wu-edit.test.js +119 -0
- package/dist/__tests__/wu-prep.test.js +108 -0
- package/dist/agent-issues-query.js +4 -3
- package/dist/agent-log-issue.js +25 -4
- package/dist/backlog-prune.js +5 -4
- package/dist/cli-entry-point.js +11 -1
- package/dist/doctor.js +368 -0
- package/dist/flow-bottlenecks.js +6 -5
- package/dist/flow-report.js +4 -3
- package/dist/gates.js +468 -116
- package/dist/guard-locked.js +4 -3
- package/dist/guard-worktree-commit.js +4 -3
- package/dist/init.js +508 -86
- package/dist/initiative-add-wu.js +4 -3
- package/dist/initiative-bulk-assign-wus.js +8 -5
- package/dist/initiative-create.js +73 -37
- package/dist/initiative-edit.js +37 -21
- package/dist/initiative-list.js +4 -3
- package/dist/initiative-plan.js +337 -0
- package/dist/initiative-status.js +4 -3
- package/dist/lane-health.js +377 -0
- package/dist/lane-suggest.js +382 -0
- package/dist/mem-checkpoint.js +2 -2
- package/dist/mem-cleanup.js +2 -2
- package/dist/mem-context.js +306 -0
- package/dist/mem-create.js +2 -2
- package/dist/mem-delete.js +293 -0
- package/dist/mem-inbox.js +2 -2
- package/dist/mem-index.js +211 -0
- package/dist/mem-init.js +1 -1
- package/dist/mem-profile.js +207 -0
- package/dist/mem-promote.js +254 -0
- package/dist/mem-ready.js +2 -2
- package/dist/mem-signal.js +2 -2
- package/dist/mem-start.js +2 -2
- package/dist/mem-summarize.js +2 -2
- package/dist/mem-triage.js +2 -2
- package/dist/merge-block.js +222 -0
- package/dist/metrics-cli.js +7 -4
- package/dist/metrics-snapshot.js +4 -3
- package/dist/orchestrate-initiative.js +10 -4
- package/dist/orchestrate-monitor.js +379 -31
- package/dist/signal-cleanup.js +296 -0
- package/dist/spawn-list.js +6 -5
- package/dist/state-bootstrap.js +5 -4
- package/dist/state-cleanup.js +360 -0
- package/dist/state-doctor-fix.js +196 -0
- package/dist/state-doctor.js +501 -0
- package/dist/validate-agent-skills.js +4 -3
- package/dist/validate-agent-sync.js +4 -3
- package/dist/validate-backlog-sync.js +7 -84
- package/dist/validate-skills-spec.js +4 -3
- package/dist/validate.js +7 -107
- package/dist/wu-block.js +3 -3
- package/dist/wu-claim.js +208 -98
- package/dist/wu-cleanup.js +5 -4
- package/dist/wu-create.js +71 -46
- package/dist/wu-delete.js +88 -60
- package/dist/wu-deps.js +6 -5
- package/dist/wu-done-check.js +34 -0
- package/dist/wu-done.js +60 -24
- package/dist/wu-edit.js +63 -28
- package/dist/wu-infer-lane.js +7 -6
- package/dist/wu-preflight.js +23 -81
- package/dist/wu-prep.js +125 -0
- package/dist/wu-prune.js +4 -3
- package/dist/wu-recover.js +88 -22
- package/dist/wu-repair.js +7 -6
- package/dist/wu-spawn.js +226 -270
- package/dist/wu-status.js +4 -3
- package/dist/wu-unblock.js +5 -5
- package/dist/wu-unlock-lane.js +4 -3
- package/dist/wu-validate.js +5 -4
- package/package.json +16 -7
- package/templates/core/.lumenflow/constraints.md.template +192 -0
- package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
- package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
- package/templates/core/AGENTS.md.template +60 -0
- package/templates/core/LUMENFLOW.md.template +255 -0
- package/templates/core/UPGRADING.md.template +121 -0
- package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
- package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
- package/templates/core/ai/onboarding/release-process.md.template +362 -0
- package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
- package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
- package/templates/vendors/aider/.aider.conf.yml.template +27 -0
- package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
- package/templates/vendors/claude/.claude/settings.json.template +49 -0
- package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
- package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
- package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
- package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
- package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
- package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
- package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
- package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
- package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
- package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
- package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
- package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
- package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
- package/templates/vendors/cline/.clinerules.template +53 -0
- package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
- package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
- package/templates/vendors/windsurf/.windsurf/rules/lumenflow.md.template +34 -0
package/dist/gates.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console -- Gates runner uses console for status output; refactoring to logger is tracked for future work */
|
|
2
3
|
/**
|
|
3
4
|
* Quality Gates Runner
|
|
4
5
|
*
|
|
@@ -32,11 +33,11 @@
|
|
|
32
33
|
* - Standard WUs: changed tests + safety-critical tests
|
|
33
34
|
*
|
|
34
35
|
* Usage:
|
|
35
|
-
* node tools/gates.
|
|
36
|
-
* node tools/gates.
|
|
37
|
-
* node tools/gates.
|
|
38
|
-
* node tools/gates.
|
|
39
|
-
* node tools/gates.
|
|
36
|
+
* node tools/gates.ts # Tiered gates (default)
|
|
37
|
+
* node tools/gates.ts --docs-only # Docs-only gates
|
|
38
|
+
* node tools/gates.ts --full-lint # Full lint (bypass incremental)
|
|
39
|
+
* node tools/gates.ts --full-tests # Full tests (bypass incremental)
|
|
40
|
+
* node tools/gates.ts --coverage-mode=block # Coverage gate in block mode
|
|
40
41
|
*/
|
|
41
42
|
import { execSync, spawnSync } from 'node:child_process';
|
|
42
43
|
import { closeSync, mkdirSync, openSync, readSync, statSync, writeSync } from 'node:fs';
|
|
@@ -44,18 +45,27 @@ import { access } from 'node:fs/promises';
|
|
|
44
45
|
import path from 'node:path';
|
|
45
46
|
import { emitGateEvent, getCurrentWU, getCurrentLane } from '@lumenflow/core/dist/telemetry.js';
|
|
46
47
|
import { die } from '@lumenflow/core/dist/error-handler.js';
|
|
47
|
-
import { getChangedLintableFiles,
|
|
48
|
-
import { isCodeFilePath } from '@lumenflow/core/dist/incremental-test.js';
|
|
48
|
+
import { getChangedLintableFiles, isLintableFile } from '@lumenflow/core/dist/incremental-lint.js';
|
|
49
|
+
import { buildVitestChangedArgs, isCodeFilePath } from '@lumenflow/core/dist/incremental-test.js';
|
|
49
50
|
import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
|
|
50
51
|
import { runCoverageGate, COVERAGE_GATE_MODES } from '@lumenflow/core/dist/coverage-gate.js';
|
|
51
52
|
import { buildGatesLogPath, shouldUseGatesAgentMode, updateGatesLatestSymlink, } from '@lumenflow/core/dist/gates-agent-mode.js';
|
|
52
53
|
// WU-2062: Import risk detector for tiered test execution
|
|
53
|
-
|
|
54
|
-
import { detectRiskTier, RISK_TIERS, } from '@lumenflow/core/dist/risk-detector.js';
|
|
54
|
+
import { detectRiskTier, RISK_TIERS } from '@lumenflow/core/dist/risk-detector.js';
|
|
55
55
|
// WU-2252: Import invariants runner for first-check validation
|
|
56
56
|
import { runInvariants } from '@lumenflow/core/dist/invariants-runner.js';
|
|
57
57
|
import { createWUParser } from '@lumenflow/core/dist/arg-parser.js';
|
|
58
|
-
import {
|
|
58
|
+
import { validateBacklogSync } from '@lumenflow/core/dist/validators/backlog-sync.js';
|
|
59
|
+
import { runSupabaseDocsLinter } from '@lumenflow/core/dist/validators/supabase-docs-linter.js';
|
|
60
|
+
import { runSystemMapValidation } from '@lumenflow/core/dist/system-map-validator.js';
|
|
61
|
+
// WU-1067: Config-driven gates support (partial implementation - unused imports removed)
|
|
62
|
+
// WU-1191: Lane health gate configuration
|
|
63
|
+
// WU-1262: Coverage config from methodology policy
|
|
64
|
+
// WU-1280: Test policy for tests_required (warn vs block on test failures)
|
|
65
|
+
import { loadLaneHealthConfig, resolveTestPolicy, } from '@lumenflow/core/dist/gates-config.js';
|
|
66
|
+
// WU-1191: Lane health check
|
|
67
|
+
import { runLaneHealthCheck } from './lane-health.js';
|
|
68
|
+
import { BRANCHES, PACKAGES, PKG_MANAGER, ESLINT_FLAGS, ESLINT_COMMANDS, ESLINT_DEFAULTS, SCRIPTS, CACHE_STRATEGIES, DIRECTORIES, GATE_NAMES, GATE_COMMANDS, CLI_MODES, EXIT_CODES, FILE_SYSTEM, PRETTIER_ARGS, PRETTIER_FLAGS, } from '@lumenflow/core/dist/wu-constants.js';
|
|
59
69
|
/**
|
|
60
70
|
* WU-1087: Gates-specific option definitions for createWUParser.
|
|
61
71
|
* Exported for testing and consistency with other CLI commands.
|
|
@@ -138,6 +148,7 @@ export function parseGatesOptions() {
|
|
|
138
148
|
* @deprecated Use parseGatesOptions() instead (WU-1087)
|
|
139
149
|
* Kept for backward compatibility during migration.
|
|
140
150
|
*/
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Pre-existing: argv kept for backwards compatibility
|
|
141
152
|
function parseGatesArgs(argv = process.argv) {
|
|
142
153
|
return parseGatesOptions();
|
|
143
154
|
}
|
|
@@ -154,6 +165,91 @@ function pnpmRun(script, ...args) {
|
|
|
154
165
|
const argsStr = args.length > 0 ? ` ${args.join(' ')}` : '';
|
|
155
166
|
return `${PKG_MANAGER} ${SCRIPTS.RUN} ${script}${argsStr}`;
|
|
156
167
|
}
|
|
168
|
+
const PRETTIER_CONFIG_FILES = new Set([
|
|
169
|
+
'.prettierrc',
|
|
170
|
+
'.prettierrc.json',
|
|
171
|
+
'.prettierrc.yaml',
|
|
172
|
+
'.prettierrc.yml',
|
|
173
|
+
'.prettierrc.js',
|
|
174
|
+
'.prettierrc.cjs',
|
|
175
|
+
'.prettierrc.ts',
|
|
176
|
+
'prettier.config.js',
|
|
177
|
+
'prettier.config.cjs',
|
|
178
|
+
'prettier.config.ts',
|
|
179
|
+
'prettier.config.mjs',
|
|
180
|
+
'.prettierignore',
|
|
181
|
+
]);
|
|
182
|
+
const TEST_CONFIG_BASENAMES = new Set(['turbo.json', 'pnpm-lock.yaml', 'package.json']);
|
|
183
|
+
const TEST_CONFIG_PATTERNS = [/^vitest\.config\.(ts|mts|js|mjs|cjs)$/i, /^tsconfig(\..+)?\.json$/i];
|
|
184
|
+
function normalizePath(filePath) {
|
|
185
|
+
return filePath.replace(/\\/g, '/');
|
|
186
|
+
}
|
|
187
|
+
function getBasename(filePath) {
|
|
188
|
+
const normalized = normalizePath(filePath);
|
|
189
|
+
const parts = normalized.split('/');
|
|
190
|
+
return parts[parts.length - 1] || normalized;
|
|
191
|
+
}
|
|
192
|
+
function quoteShellArgs(files) {
|
|
193
|
+
return files.map((file) => `"${file}"`).join(' ');
|
|
194
|
+
}
|
|
195
|
+
export function isPrettierConfigFile(filePath) {
|
|
196
|
+
if (!filePath)
|
|
197
|
+
return false;
|
|
198
|
+
const basename = getBasename(filePath);
|
|
199
|
+
return PRETTIER_CONFIG_FILES.has(basename);
|
|
200
|
+
}
|
|
201
|
+
export function isTestConfigFile(filePath) {
|
|
202
|
+
if (!filePath)
|
|
203
|
+
return false;
|
|
204
|
+
const basename = getBasename(filePath);
|
|
205
|
+
if (TEST_CONFIG_BASENAMES.has(basename)) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
return TEST_CONFIG_PATTERNS.some((pattern) => pattern.test(basename));
|
|
209
|
+
}
|
|
210
|
+
/* eslint-disable sonarjs/no-duplicate-string -- Pre-existing: format check reasons are intentionally distinct string literals */
|
|
211
|
+
export function resolveFormatCheckPlan({ changedFiles, fileListError = false, }) {
|
|
212
|
+
if (fileListError) {
|
|
213
|
+
return { mode: 'full', files: [], reason: 'file-list-error' };
|
|
214
|
+
}
|
|
215
|
+
if (changedFiles.some(isPrettierConfigFile)) {
|
|
216
|
+
return { mode: 'full', files: [], reason: 'prettier-config' };
|
|
217
|
+
}
|
|
218
|
+
if (changedFiles.length === 0) {
|
|
219
|
+
return { mode: 'skip', files: [] };
|
|
220
|
+
}
|
|
221
|
+
return { mode: 'incremental', files: changedFiles };
|
|
222
|
+
}
|
|
223
|
+
export function resolveLintPlan({ isMainBranch, changedFiles, }) {
|
|
224
|
+
if (isMainBranch) {
|
|
225
|
+
return { mode: 'full', files: [] };
|
|
226
|
+
}
|
|
227
|
+
const lintTargets = changedFiles.filter((filePath) => {
|
|
228
|
+
const normalized = normalizePath(filePath);
|
|
229
|
+
return ((normalized.startsWith('apps/') || normalized.startsWith('packages/')) &&
|
|
230
|
+
isLintableFile(normalized));
|
|
231
|
+
});
|
|
232
|
+
if (lintTargets.length === 0) {
|
|
233
|
+
return { mode: 'skip', files: [] };
|
|
234
|
+
}
|
|
235
|
+
return { mode: 'incremental', files: lintTargets };
|
|
236
|
+
}
|
|
237
|
+
/* eslint-enable sonarjs/no-duplicate-string */
|
|
238
|
+
export function resolveTestPlan({ isMainBranch, hasUntrackedCode, hasConfigChange, fileListError, }) {
|
|
239
|
+
if (fileListError) {
|
|
240
|
+
return { mode: 'full', reason: 'file-list-error' };
|
|
241
|
+
}
|
|
242
|
+
if (hasUntrackedCode) {
|
|
243
|
+
return { mode: 'full', reason: 'untracked-code' };
|
|
244
|
+
}
|
|
245
|
+
if (hasConfigChange) {
|
|
246
|
+
return { mode: 'full', reason: 'test-config' };
|
|
247
|
+
}
|
|
248
|
+
if (isMainBranch) {
|
|
249
|
+
return { mode: 'full' };
|
|
250
|
+
}
|
|
251
|
+
return { mode: 'incremental' };
|
|
252
|
+
}
|
|
157
253
|
export function parsePrettierListOutput(output) {
|
|
158
254
|
if (!output)
|
|
159
255
|
return [];
|
|
@@ -171,6 +267,10 @@ export function buildPrettierWriteCommand(files) {
|
|
|
171
267
|
const base = pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_FLAGS.WRITE);
|
|
172
268
|
return quotedFiles ? `${base} ${quotedFiles}` : base;
|
|
173
269
|
}
|
|
270
|
+
function buildPrettierCheckCommand(files) {
|
|
271
|
+
const filesArg = files.length > 0 ? quoteShellArgs(files) : '.';
|
|
272
|
+
return pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_ARGS.CHECK, filesArg);
|
|
273
|
+
}
|
|
174
274
|
export function formatFormatCheckGuidance(files) {
|
|
175
275
|
if (!files.length)
|
|
176
276
|
return [];
|
|
@@ -186,8 +286,10 @@ export function formatFormatCheckGuidance(files) {
|
|
|
186
286
|
'',
|
|
187
287
|
];
|
|
188
288
|
}
|
|
189
|
-
function collectPrettierListDifferent(cwd) {
|
|
190
|
-
const
|
|
289
|
+
function collectPrettierListDifferent(cwd, files = []) {
|
|
290
|
+
const filesArg = files.length > 0 ? quoteShellArgs(files) : '.';
|
|
291
|
+
const cmd = pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_ARGS.LIST_DIFFERENT, filesArg);
|
|
292
|
+
// eslint-disable-next-line sonarjs/os-command -- Pre-existing: executes trusted pnpm prettier command
|
|
191
293
|
const result = spawnSync(cmd, [], {
|
|
192
294
|
shell: true,
|
|
193
295
|
cwd,
|
|
@@ -196,11 +298,11 @@ function collectPrettierListDifferent(cwd) {
|
|
|
196
298
|
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
197
299
|
return parsePrettierListOutput(output);
|
|
198
300
|
}
|
|
199
|
-
function emitFormatCheckGuidance({ agentLog, useAgentMode, }) {
|
|
200
|
-
const
|
|
201
|
-
if (!
|
|
301
|
+
function emitFormatCheckGuidance({ agentLog, useAgentMode, files, }) {
|
|
302
|
+
const formattedFiles = collectPrettierListDifferent(process.cwd(), files ?? []);
|
|
303
|
+
if (!formattedFiles.length)
|
|
202
304
|
return;
|
|
203
|
-
const lines = formatFormatCheckGuidance(
|
|
305
|
+
const lines = formatFormatCheckGuidance(formattedFiles);
|
|
204
306
|
const logLine = useAgentMode && agentLog
|
|
205
307
|
? (line) => writeSync(agentLog.logFd, `${line}\n`)
|
|
206
308
|
: (line) => console.log(line);
|
|
@@ -208,12 +310,6 @@ function emitFormatCheckGuidance({ agentLog, useAgentMode, }) {
|
|
|
208
310
|
logLine(line);
|
|
209
311
|
}
|
|
210
312
|
}
|
|
211
|
-
/**
|
|
212
|
-
* Build a pnpm --filter command string
|
|
213
|
-
*/
|
|
214
|
-
function pnpmFilter(pkg, script) {
|
|
215
|
-
return `${PKG_MANAGER} ${PKG_FLAGS.FILTER} ${pkg} ${script}`;
|
|
216
|
-
}
|
|
217
313
|
function readLogTail(logPath, { maxLines = 40, maxBytes = 64 * 1024 } = {}) {
|
|
218
314
|
try {
|
|
219
315
|
const stats = statSync(logPath);
|
|
@@ -258,6 +354,7 @@ function run(cmd, { agentLog } = {}) {
|
|
|
258
354
|
if (!agentLog) {
|
|
259
355
|
console.log(`\n> ${cmd}\n`);
|
|
260
356
|
try {
|
|
357
|
+
// eslint-disable-next-line sonarjs/os-command -- Pre-existing: cmd is built from trusted constants
|
|
261
358
|
execSync(cmd, { stdio: 'inherit', encoding: FILE_SYSTEM.ENCODING });
|
|
262
359
|
return { ok: true, duration: Date.now() - start };
|
|
263
360
|
}
|
|
@@ -266,6 +363,7 @@ function run(cmd, { agentLog } = {}) {
|
|
|
266
363
|
}
|
|
267
364
|
}
|
|
268
365
|
writeSync(agentLog.logFd, `\n> ${cmd}\n\n`);
|
|
366
|
+
// eslint-disable-next-line sonarjs/os-command -- Pre-existing: cmd is built from trusted constants
|
|
269
367
|
const result = spawnSync(cmd, [], {
|
|
270
368
|
shell: true,
|
|
271
369
|
stdio: ['ignore', agentLog.logFd, agentLog.logFd],
|
|
@@ -274,6 +372,184 @@ function run(cmd, { agentLog } = {}) {
|
|
|
274
372
|
});
|
|
275
373
|
return { ok: result.status === EXIT_CODES.SUCCESS, duration: Date.now() - start };
|
|
276
374
|
}
|
|
375
|
+
function makeGateLogger({ agentLog, useAgentMode }) {
|
|
376
|
+
return (line) => {
|
|
377
|
+
if (!useAgentMode) {
|
|
378
|
+
console.log(line);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (agentLog) {
|
|
382
|
+
writeSync(agentLog.logFd, `${line}\n`);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
async function runBacklogSyncGate({ agentLog, useAgentMode }) {
|
|
387
|
+
const start = Date.now();
|
|
388
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
389
|
+
logLine('\n> Backlog sync\n');
|
|
390
|
+
const result = await validateBacklogSync({ cwd: process.cwd() });
|
|
391
|
+
if (result.errors.length > 0) {
|
|
392
|
+
logLine('❌ Backlog sync errors:');
|
|
393
|
+
result.errors.forEach((error) => logLine(` - ${error}`));
|
|
394
|
+
}
|
|
395
|
+
if (result.warnings.length > 0) {
|
|
396
|
+
logLine('⚠️ Backlog sync warnings:');
|
|
397
|
+
result.warnings.forEach((warning) => logLine(` - ${warning}`));
|
|
398
|
+
}
|
|
399
|
+
logLine(`Backlog sync summary: WU files=${result.wuCount}, Backlog refs=${result.backlogCount}`);
|
|
400
|
+
return { ok: result.valid, duration: Date.now() - start };
|
|
401
|
+
}
|
|
402
|
+
async function runSupabaseDocsGate({ agentLog, useAgentMode }) {
|
|
403
|
+
const start = Date.now();
|
|
404
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
405
|
+
logLine('\n> Supabase docs linter\n');
|
|
406
|
+
const result = await runSupabaseDocsLinter({ cwd: process.cwd(), logger: { log: logLine } });
|
|
407
|
+
if (result.skipped) {
|
|
408
|
+
logLine(`⚠️ ${result.message ?? 'Supabase docs linter skipped.'}`);
|
|
409
|
+
}
|
|
410
|
+
else if (!result.ok) {
|
|
411
|
+
logLine('❌ Supabase docs linter failed.');
|
|
412
|
+
(result.errors ?? []).forEach((error) => logLine(` - ${error}`));
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
logLine(result.message ?? 'Supabase docs linter passed.');
|
|
416
|
+
}
|
|
417
|
+
return { ok: result.ok, duration: Date.now() - start };
|
|
418
|
+
}
|
|
419
|
+
async function runSystemMapGate({ agentLog, useAgentMode }) {
|
|
420
|
+
const start = Date.now();
|
|
421
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
422
|
+
logLine('\n> System map validation\n');
|
|
423
|
+
const result = await runSystemMapValidation({
|
|
424
|
+
cwd: process.cwd(),
|
|
425
|
+
logger: { log: logLine, warn: logLine, error: logLine },
|
|
426
|
+
});
|
|
427
|
+
if (!result.valid) {
|
|
428
|
+
logLine('❌ System map validation failed');
|
|
429
|
+
(result.pathErrors ?? []).forEach((error) => logLine(` - ${error}`));
|
|
430
|
+
(result.orphanDocs ?? []).forEach((error) => logLine(` - ${error}`));
|
|
431
|
+
(result.audienceErrors ?? []).forEach((error) => logLine(` - ${error}`));
|
|
432
|
+
(result.queryErrors ?? []).forEach((error) => logLine(` - ${error}`));
|
|
433
|
+
(result.classificationErrors ?? []).forEach((error) => logLine(` - ${error}`));
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
logLine('System map validation passed.');
|
|
437
|
+
}
|
|
438
|
+
return { ok: result.valid, duration: Date.now() - start };
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* WU-1191: Run lane health check gate
|
|
442
|
+
*
|
|
443
|
+
* Checks lane configuration for overlaps and coverage gaps.
|
|
444
|
+
* Mode is configurable via gates.lane_health in .lumenflow.config.yaml:
|
|
445
|
+
* - 'warn': Log warnings but don't fail (default)
|
|
446
|
+
* - 'error': Fail the gate if issues detected
|
|
447
|
+
* - 'off': Skip the check entirely
|
|
448
|
+
*/
|
|
449
|
+
async function runLaneHealthGate({ agentLog, useAgentMode, mode, }) {
|
|
450
|
+
const start = Date.now();
|
|
451
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
452
|
+
// Skip if mode is 'off'
|
|
453
|
+
if (mode === 'off') {
|
|
454
|
+
logLine('\n> Lane health check (skipped - mode: off)\n');
|
|
455
|
+
return { ok: true, duration: Date.now() - start };
|
|
456
|
+
}
|
|
457
|
+
logLine(`\n> Lane health check (mode: ${mode})\n`);
|
|
458
|
+
const report = runLaneHealthCheck({ projectRoot: process.cwd() });
|
|
459
|
+
if (!report.healthy) {
|
|
460
|
+
logLine('⚠️ Lane health issues detected:');
|
|
461
|
+
if (report.overlaps.hasOverlaps) {
|
|
462
|
+
logLine(` - ${report.overlaps.overlaps.length} overlapping code_paths`);
|
|
463
|
+
}
|
|
464
|
+
if (report.gaps.hasGaps) {
|
|
465
|
+
logLine(` - ${report.gaps.uncoveredFiles.length} uncovered files`);
|
|
466
|
+
}
|
|
467
|
+
logLine(` Run 'pnpm lane:health' for full report.`);
|
|
468
|
+
if (mode === 'error') {
|
|
469
|
+
return { ok: false, duration: Date.now() - start };
|
|
470
|
+
}
|
|
471
|
+
// mode === 'warn': report but don't fail
|
|
472
|
+
logLine(' (mode: warn - not blocking)');
|
|
473
|
+
}
|
|
474
|
+
else {
|
|
475
|
+
logLine('Lane health check passed.');
|
|
476
|
+
}
|
|
477
|
+
return { ok: true, duration: Date.now() - start };
|
|
478
|
+
}
|
|
479
|
+
async function filterExistingFiles(files) {
|
|
480
|
+
const existingFiles = await Promise.all(files.map(async (file) => {
|
|
481
|
+
try {
|
|
482
|
+
await access(file);
|
|
483
|
+
return file;
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}));
|
|
489
|
+
return existingFiles.filter((file) => Boolean(file));
|
|
490
|
+
}
|
|
491
|
+
async function runFormatCheckGate({ agentLog, useAgentMode }) {
|
|
492
|
+
const start = Date.now();
|
|
493
|
+
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
494
|
+
let git;
|
|
495
|
+
let isMainBranch = false;
|
|
496
|
+
try {
|
|
497
|
+
git = getGitForCwd();
|
|
498
|
+
const currentBranch = await git.getCurrentBranch();
|
|
499
|
+
isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
logLine(`⚠️ Failed to determine branch for format check: ${error.message}`);
|
|
503
|
+
const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog });
|
|
504
|
+
return { ...result, duration: Date.now() - start, fileCount: -1 };
|
|
505
|
+
}
|
|
506
|
+
if (isMainBranch) {
|
|
507
|
+
logLine('📋 On main branch - running full format check');
|
|
508
|
+
const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog });
|
|
509
|
+
return { ...result, duration: Date.now() - start, fileCount: -1 };
|
|
510
|
+
}
|
|
511
|
+
let changedFiles = [];
|
|
512
|
+
let fileListError = false;
|
|
513
|
+
try {
|
|
514
|
+
changedFiles = await getChangedFilesForIncremental({ git });
|
|
515
|
+
}
|
|
516
|
+
catch (error) {
|
|
517
|
+
fileListError = true;
|
|
518
|
+
logLine(`⚠️ Failed to determine changed files for format check: ${error.message}`);
|
|
519
|
+
}
|
|
520
|
+
const plan = resolveFormatCheckPlan({ changedFiles, fileListError });
|
|
521
|
+
if (plan.mode === 'skip') {
|
|
522
|
+
logLine('\n> format:check (incremental)\n');
|
|
523
|
+
logLine('✅ No files changed - skipping format check');
|
|
524
|
+
return { ok: true, duration: Date.now() - start, fileCount: 0, filesChecked: [] };
|
|
525
|
+
}
|
|
526
|
+
if (plan.mode === 'full') {
|
|
527
|
+
/* eslint-disable sonarjs/no-nested-conditional -- Pre-existing: simple reason mapping, readable as-is */
|
|
528
|
+
const reason = plan.reason === 'prettier-config'
|
|
529
|
+
? ' (prettier config changed)'
|
|
530
|
+
: plan.reason === 'file-list-error'
|
|
531
|
+
? ' (file list unavailable)'
|
|
532
|
+
: '';
|
|
533
|
+
/* eslint-enable sonarjs/no-nested-conditional */
|
|
534
|
+
logLine(`📋 Running full format check${reason}`);
|
|
535
|
+
const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog });
|
|
536
|
+
return { ...result, duration: Date.now() - start, fileCount: -1 };
|
|
537
|
+
}
|
|
538
|
+
const existingFiles = await filterExistingFiles(plan.files);
|
|
539
|
+
if (existingFiles.length === 0) {
|
|
540
|
+
logLine('\n> format:check (incremental)\n');
|
|
541
|
+
logLine('✅ All changed files were deleted - skipping format check');
|
|
542
|
+
return { ok: true, duration: Date.now() - start, fileCount: 0, filesChecked: [] };
|
|
543
|
+
}
|
|
544
|
+
logLine(`\n> format:check (incremental: ${existingFiles.length} files)\n`);
|
|
545
|
+
const result = run(buildPrettierCheckCommand(existingFiles), { agentLog });
|
|
546
|
+
return {
|
|
547
|
+
...result,
|
|
548
|
+
duration: Date.now() - start,
|
|
549
|
+
fileCount: existingFiles.length,
|
|
550
|
+
filesChecked: existingFiles,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
277
553
|
/**
|
|
278
554
|
* Run incremental ESLint on changed files only
|
|
279
555
|
* Falls back to full lint if on main branch or if incremental fails
|
|
@@ -288,56 +564,36 @@ async function runIncrementalLint({ agentLog, } = {}) {
|
|
|
288
564
|
}
|
|
289
565
|
writeSync(agentLog.logFd, `${line}\n`);
|
|
290
566
|
};
|
|
291
|
-
// WU-1006: Skip incremental lint if apps/web doesn't exist (repo-agnostic)
|
|
292
|
-
const webDir = path.join(process.cwd(), DIRECTORIES.APPS_WEB);
|
|
293
|
-
try {
|
|
294
|
-
await access(webDir);
|
|
295
|
-
}
|
|
296
|
-
catch {
|
|
297
|
-
logLine('\n> ESLint (incremental) skipped (apps/web not present)\n');
|
|
298
|
-
return { ok: true, duration: Date.now() - start, fileCount: 0 };
|
|
299
|
-
}
|
|
300
567
|
try {
|
|
301
568
|
// Check if we're on main branch
|
|
302
569
|
const git = getGitForCwd();
|
|
303
570
|
const currentBranch = await git.getCurrentBranch();
|
|
304
|
-
|
|
571
|
+
const isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
|
|
572
|
+
if (isMainBranch) {
|
|
305
573
|
logLine('📋 On main branch - running full lint');
|
|
306
|
-
const result = run(
|
|
574
|
+
const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog });
|
|
307
575
|
return { ...result, fileCount: -1 };
|
|
308
576
|
}
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
filterPath: DIRECTORIES.APPS_WEB,
|
|
313
|
-
});
|
|
314
|
-
if (changedFiles.length === 0) {
|
|
577
|
+
const changedFiles = await getChangedLintableFiles({ git });
|
|
578
|
+
const plan = resolveLintPlan({ isMainBranch, changedFiles });
|
|
579
|
+
if (plan.mode === 'skip') {
|
|
315
580
|
logLine('\n> ESLint (incremental)\n');
|
|
316
581
|
logLine('✅ No lintable files changed - skipping lint');
|
|
317
582
|
return { ok: true, duration: Date.now() - start, fileCount: 0 };
|
|
318
583
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
catch {
|
|
326
|
-
return null;
|
|
327
|
-
}
|
|
328
|
-
}))).filter(Boolean);
|
|
584
|
+
if (plan.mode === 'full') {
|
|
585
|
+
logLine('📋 Running full lint (incremental plan forced full)');
|
|
586
|
+
const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog });
|
|
587
|
+
return { ...result, fileCount: -1 };
|
|
588
|
+
}
|
|
589
|
+
const existingFiles = await filterExistingFiles(plan.files);
|
|
329
590
|
if (existingFiles.length === 0) {
|
|
330
591
|
logLine('\n> ESLint (incremental)\n');
|
|
331
592
|
logLine('✅ All changed files were deleted - skipping lint');
|
|
332
593
|
return { ok: true, duration: Date.now() - start, fileCount: 0 };
|
|
333
594
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const packageRelativeFiles = convertToPackageRelativePaths(existingFiles, DIRECTORIES.APPS_WEB);
|
|
337
|
-
logLine(`\n> ESLint (incremental: ${packageRelativeFiles.length} files)\n`);
|
|
338
|
-
logLine(`Files to lint:\n ${packageRelativeFiles.join('\n ')}\n`);
|
|
339
|
-
// WU-2571: Run ESLint from apps/web directory with package-relative paths
|
|
340
|
-
const webDir = path.join(process.cwd(), DIRECTORIES.APPS_WEB);
|
|
595
|
+
logLine(`\n> ESLint (incremental: ${existingFiles.length} files)\n`);
|
|
596
|
+
logLine(`Files to lint:\n ${existingFiles.join('\n ')}\n`);
|
|
341
597
|
const result = spawnSync(PKG_MANAGER, [
|
|
342
598
|
ESLINT_COMMANDS.ESLINT,
|
|
343
599
|
ESLINT_FLAGS.MAX_WARNINGS,
|
|
@@ -349,28 +605,28 @@ async function runIncrementalLint({ agentLog, } = {}) {
|
|
|
349
605
|
ESLINT_FLAGS.CACHE_LOCATION,
|
|
350
606
|
'.eslintcache',
|
|
351
607
|
ESLINT_FLAGS.PASS_ON_UNPRUNED,
|
|
352
|
-
...
|
|
608
|
+
...existingFiles,
|
|
353
609
|
], agentLog
|
|
354
610
|
? {
|
|
355
611
|
stdio: ['ignore', agentLog.logFd, agentLog.logFd],
|
|
356
612
|
encoding: FILE_SYSTEM.ENCODING,
|
|
357
|
-
cwd:
|
|
613
|
+
cwd: process.cwd(),
|
|
358
614
|
}
|
|
359
615
|
: {
|
|
360
616
|
stdio: 'inherit',
|
|
361
617
|
encoding: FILE_SYSTEM.ENCODING,
|
|
362
|
-
cwd:
|
|
618
|
+
cwd: process.cwd(),
|
|
363
619
|
});
|
|
364
620
|
const duration = Date.now() - start;
|
|
365
621
|
return {
|
|
366
622
|
ok: result.status === EXIT_CODES.SUCCESS,
|
|
367
623
|
duration,
|
|
368
|
-
fileCount:
|
|
624
|
+
fileCount: existingFiles.length,
|
|
369
625
|
};
|
|
370
626
|
}
|
|
371
627
|
catch (error) {
|
|
372
628
|
console.error('⚠️ Incremental lint failed, falling back to full lint:', error.message);
|
|
373
|
-
const result = run(
|
|
629
|
+
const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog });
|
|
374
630
|
return { ...result, fileCount: -1 };
|
|
375
631
|
}
|
|
376
632
|
}
|
|
@@ -393,29 +649,53 @@ async function runChangedTests({ agentLog, } = {}) {
|
|
|
393
649
|
try {
|
|
394
650
|
const git = getGitForCwd();
|
|
395
651
|
const currentBranch = await git.getCurrentBranch();
|
|
396
|
-
|
|
652
|
+
const isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
|
|
653
|
+
if (isMainBranch) {
|
|
397
654
|
logLine('📋 On main branch - running full test suite');
|
|
398
655
|
const result = run(pnpmCmd('turbo', 'run', 'test'), { agentLog });
|
|
399
656
|
return { ...result, isIncremental: false };
|
|
400
657
|
}
|
|
658
|
+
let changedFiles = [];
|
|
659
|
+
let fileListError = false;
|
|
660
|
+
try {
|
|
661
|
+
changedFiles = await getChangedFilesForIncremental({ git });
|
|
662
|
+
}
|
|
663
|
+
catch (error) {
|
|
664
|
+
fileListError = true;
|
|
665
|
+
logLine(`⚠️ Failed to determine changed files for tests: ${error.message}`);
|
|
666
|
+
}
|
|
667
|
+
const hasConfigChange = !fileListError && changedFiles.some(isTestConfigFile);
|
|
401
668
|
const untrackedOutput = await git.raw(['ls-files', '--others', '--exclude-standard']);
|
|
402
669
|
const untrackedFiles = untrackedOutput
|
|
403
670
|
.split(/\r?\n/)
|
|
404
671
|
.map((f) => f.trim())
|
|
405
672
|
.filter(Boolean);
|
|
406
673
|
const untrackedCodeFiles = untrackedFiles.filter(isCodeFilePath);
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
674
|
+
const hasUntrackedCode = untrackedCodeFiles.length > 0;
|
|
675
|
+
const plan = resolveTestPlan({
|
|
676
|
+
isMainBranch,
|
|
677
|
+
hasUntrackedCode,
|
|
678
|
+
hasConfigChange,
|
|
679
|
+
fileListError,
|
|
680
|
+
});
|
|
681
|
+
if (plan.mode === 'full') {
|
|
682
|
+
if (plan.reason === 'untracked-code') {
|
|
683
|
+
const preview = untrackedCodeFiles.slice(0, 5).join(', ');
|
|
684
|
+
logLine(`⚠️ Untracked code files detected (${untrackedCodeFiles.length}): ${preview}${untrackedCodeFiles.length > 5 ? '...' : ''}`);
|
|
685
|
+
}
|
|
686
|
+
else if (plan.reason === 'test-config') {
|
|
687
|
+
logLine('⚠️ Test config changes detected - running full test suite');
|
|
688
|
+
}
|
|
689
|
+
else if (plan.reason === 'file-list-error') {
|
|
690
|
+
logLine('⚠️ Changed file list unavailable - running full test suite');
|
|
691
|
+
}
|
|
410
692
|
logLine('📋 Running full test suite to avoid missing coverage');
|
|
411
693
|
const result = run(pnpmCmd('turbo', 'run', 'test'), { agentLog });
|
|
412
694
|
return { ...result, duration: Date.now() - start, isIncremental: false };
|
|
413
695
|
}
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const result = run(pnpmCmd('turbo', 'run', 'test'), { agentLog });
|
|
418
|
-
return { ...result, duration: Date.now() - start, isIncremental: false };
|
|
696
|
+
logLine('\n> Running tests (vitest --changed)\n');
|
|
697
|
+
const result = run(pnpmCmd('vitest', 'run', ...buildVitestChangedArgs({ baseBranch: 'origin/main' })), { agentLog });
|
|
698
|
+
return { ...result, duration: Date.now() - start, isIncremental: true };
|
|
419
699
|
}
|
|
420
700
|
catch (error) {
|
|
421
701
|
console.error('⚠️ Changed tests failed, falling back to full suite:', error.message);
|
|
@@ -545,55 +825,85 @@ async function runIntegrationTests({ agentLog, } = {}) {
|
|
|
545
825
|
return { ok: false, duration: Date.now() - start };
|
|
546
826
|
}
|
|
547
827
|
}
|
|
828
|
+
async function getChangedFilesForIncremental({ git, baseBranch = 'origin/main', }) {
|
|
829
|
+
const mergeBase = await git.mergeBase('HEAD', baseBranch);
|
|
830
|
+
const committedOutput = await git.raw(['diff', '--name-only', `${mergeBase}...HEAD`]);
|
|
831
|
+
const committedFiles = committedOutput
|
|
832
|
+
.split('\n')
|
|
833
|
+
.map((f) => f.trim())
|
|
834
|
+
.filter(Boolean);
|
|
835
|
+
const unstagedOutput = await git.raw(['diff', '--name-only']);
|
|
836
|
+
const unstagedFiles = unstagedOutput
|
|
837
|
+
.split('\n')
|
|
838
|
+
.map((f) => f.trim())
|
|
839
|
+
.filter(Boolean);
|
|
840
|
+
const untrackedOutput = await git.raw(['ls-files', '--others', '--exclude-standard']);
|
|
841
|
+
const untrackedFiles = untrackedOutput
|
|
842
|
+
.split('\n')
|
|
843
|
+
.map((f) => f.trim())
|
|
844
|
+
.filter(Boolean);
|
|
845
|
+
return [...new Set([...committedFiles, ...unstagedFiles, ...untrackedFiles])];
|
|
846
|
+
}
|
|
548
847
|
async function getAllChangedFiles(options = {}) {
|
|
549
848
|
const { git = getGitForCwd() } = options;
|
|
550
849
|
try {
|
|
551
|
-
|
|
552
|
-
const mergeBase = await git.mergeBase('HEAD', 'origin/main');
|
|
553
|
-
// Get committed changes
|
|
554
|
-
const committedOutput = await git.raw(['diff', '--name-only', `${mergeBase}...HEAD`]);
|
|
555
|
-
const committedFiles = committedOutput
|
|
556
|
-
.split('\n')
|
|
557
|
-
.map((f) => f.trim())
|
|
558
|
-
.filter(Boolean);
|
|
559
|
-
// Get unstaged changes
|
|
560
|
-
const unstagedOutput = await git.raw(['diff', '--name-only']);
|
|
561
|
-
const unstagedFiles = unstagedOutput
|
|
562
|
-
.split('\n')
|
|
563
|
-
.map((f) => f.trim())
|
|
564
|
-
.filter(Boolean);
|
|
565
|
-
// Get untracked files
|
|
566
|
-
const untrackedOutput = await git.raw(['ls-files', '--others', '--exclude-standard']);
|
|
567
|
-
const untrackedFiles = untrackedOutput
|
|
568
|
-
.split('\n')
|
|
569
|
-
.map((f) => f.trim())
|
|
570
|
-
.filter(Boolean);
|
|
571
|
-
// Combine and deduplicate
|
|
572
|
-
return [...new Set([...committedFiles, ...unstagedFiles, ...untrackedFiles])];
|
|
850
|
+
return await getChangedFilesForIncremental({ git });
|
|
573
851
|
}
|
|
574
852
|
catch (error) {
|
|
575
853
|
console.error('⚠️ Failed to get changed files:', error.message);
|
|
576
854
|
return [];
|
|
577
855
|
}
|
|
578
856
|
}
|
|
579
|
-
|
|
580
|
-
const
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
857
|
+
export async function runGates(options = {}) {
|
|
858
|
+
const originalCwd = process.cwd();
|
|
859
|
+
const targetCwd = options.cwd ?? originalCwd;
|
|
860
|
+
if (targetCwd !== originalCwd) {
|
|
861
|
+
process.chdir(targetCwd);
|
|
862
|
+
}
|
|
863
|
+
try {
|
|
864
|
+
return await executeGates({
|
|
865
|
+
...options,
|
|
866
|
+
coverageMode: options.coverageMode ?? COVERAGE_GATE_MODES.BLOCK,
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
finally {
|
|
873
|
+
if (targetCwd !== originalCwd) {
|
|
874
|
+
process.chdir(originalCwd);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
584
878
|
// Main execution
|
|
585
879
|
// eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing: main() orchestrates multi-step gate workflow
|
|
586
|
-
async function
|
|
587
|
-
const
|
|
880
|
+
async function executeGates(opts) {
|
|
881
|
+
const argv = opts.argv ?? process.argv.slice(2);
|
|
882
|
+
// Get context for telemetry
|
|
883
|
+
const wu_id = getCurrentWU();
|
|
884
|
+
const lane = getCurrentLane();
|
|
885
|
+
const useAgentMode = shouldUseGatesAgentMode({ argv, env: process.env });
|
|
886
|
+
const agentLog = useAgentMode ? createAgentLogContext({ wuId: wu_id, lane }) : null;
|
|
588
887
|
// Parse command line arguments (now via Commander)
|
|
589
888
|
const isDocsOnly = opts.docsOnly || false;
|
|
590
889
|
const isFullLint = opts.fullLint || false;
|
|
591
890
|
const isFullTests = opts.fullTests || false;
|
|
592
891
|
// WU-2244: Full coverage flag forces full test suite and coverage gate (deterministic)
|
|
593
892
|
const isFullCoverage = opts.fullCoverage || false;
|
|
893
|
+
// WU-1262: Resolve coverage config from methodology policy
|
|
894
|
+
// This derives coverage threshold and mode from methodology.testing setting
|
|
895
|
+
// WU-1280: Use resolveTestPolicy to also get tests_required for test failure handling
|
|
896
|
+
const resolvedTestPolicy = resolveTestPolicy(process.cwd());
|
|
594
897
|
// WU-1433: Coverage gate mode (warn or block)
|
|
595
898
|
// WU-2334: Default changed from WARN to BLOCK for TDD enforcement
|
|
596
|
-
|
|
899
|
+
// WU-1262: CLI flag overrides resolved policy, which overrides methodology defaults
|
|
900
|
+
const coverageMode = opts.coverageMode || resolvedTestPolicy.mode || COVERAGE_GATE_MODES.BLOCK;
|
|
901
|
+
const coverageThreshold = resolvedTestPolicy.threshold;
|
|
902
|
+
// WU-1280: Determine if tests are required (affects whether test failures block or warn)
|
|
903
|
+
// When tests_required=false (methodology.testing: none), test failures produce warnings only
|
|
904
|
+
const testsRequired = resolvedTestPolicy.tests_required;
|
|
905
|
+
// WU-1191: Lane health gate mode (warn, error, or off)
|
|
906
|
+
const laneHealthMode = loadLaneHealthConfig(process.cwd());
|
|
597
907
|
if (useAgentMode) {
|
|
598
908
|
console.log(`🧾 gates (agent mode): output -> ${agentLog.logPath} (use --verbose for streaming)\n`);
|
|
599
909
|
}
|
|
@@ -631,27 +941,33 @@ async function main() {
|
|
|
631
941
|
? [
|
|
632
942
|
// WU-2252: Invariants check runs first (non-bypassable)
|
|
633
943
|
{ name: GATE_NAMES.INVARIANTS, cmd: GATE_COMMANDS.INVARIANTS },
|
|
634
|
-
{ name: GATE_NAMES.FORMAT_CHECK,
|
|
944
|
+
{ name: GATE_NAMES.FORMAT_CHECK, run: runFormatCheckGate },
|
|
635
945
|
{ name: GATE_NAMES.SPEC_LINTER, cmd: pnpmRun(SCRIPTS.SPEC_LINTER) },
|
|
636
946
|
{
|
|
637
947
|
name: GATE_NAMES.PROMPTS_LINT,
|
|
638
948
|
cmd: pnpmRun(SCRIPTS.PROMPTS_LINT, CLI_MODES.LOCAL, '--quiet'),
|
|
639
949
|
},
|
|
640
|
-
{ name: GATE_NAMES.BACKLOG_SYNC,
|
|
950
|
+
{ name: GATE_NAMES.BACKLOG_SYNC, run: runBacklogSyncGate },
|
|
641
951
|
// WU-2315: System map validation (warn-only until orphan docs are indexed)
|
|
642
952
|
{
|
|
643
953
|
name: GATE_NAMES.SYSTEM_MAP_VALIDATE,
|
|
644
|
-
|
|
954
|
+
run: runSystemMapGate,
|
|
645
955
|
warnOnly: true,
|
|
646
956
|
},
|
|
957
|
+
// WU-1191: Lane health check (configurable: warn/error/off)
|
|
958
|
+
{
|
|
959
|
+
name: GATE_NAMES.LANE_HEALTH,
|
|
960
|
+
run: (ctx) => runLaneHealthGate({ ...ctx, mode: laneHealthMode }),
|
|
961
|
+
warnOnly: laneHealthMode !== 'error',
|
|
962
|
+
},
|
|
647
963
|
]
|
|
648
964
|
: [
|
|
649
965
|
// WU-2252: Invariants check runs first (non-bypassable)
|
|
650
966
|
{ name: GATE_NAMES.INVARIANTS, cmd: GATE_COMMANDS.INVARIANTS },
|
|
651
|
-
{ name: GATE_NAMES.FORMAT_CHECK,
|
|
967
|
+
{ name: GATE_NAMES.FORMAT_CHECK, run: runFormatCheckGate },
|
|
652
968
|
{
|
|
653
969
|
name: GATE_NAMES.LINT,
|
|
654
|
-
cmd: isFullLint ?
|
|
970
|
+
cmd: isFullLint ? pnpmCmd(SCRIPTS.LINT) : GATE_COMMANDS.INCREMENTAL,
|
|
655
971
|
},
|
|
656
972
|
{ name: GATE_NAMES.TYPECHECK, cmd: pnpmCmd(SCRIPTS.TYPECHECK) },
|
|
657
973
|
{ name: GATE_NAMES.SPEC_LINTER, cmd: pnpmRun(SCRIPTS.SPEC_LINTER) },
|
|
@@ -659,27 +975,47 @@ async function main() {
|
|
|
659
975
|
name: GATE_NAMES.PROMPTS_LINT,
|
|
660
976
|
cmd: pnpmRun(SCRIPTS.PROMPTS_LINT, CLI_MODES.LOCAL, '--quiet'),
|
|
661
977
|
},
|
|
662
|
-
{ name: GATE_NAMES.BACKLOG_SYNC,
|
|
663
|
-
{ name: GATE_NAMES.SUPABASE_DOCS_LINTER,
|
|
978
|
+
{ name: GATE_NAMES.BACKLOG_SYNC, run: runBacklogSyncGate },
|
|
979
|
+
{ name: GATE_NAMES.SUPABASE_DOCS_LINTER, run: runSupabaseDocsGate },
|
|
664
980
|
// WU-2315: System map validation (warn-only until orphan docs are indexed)
|
|
665
981
|
{
|
|
666
982
|
name: GATE_NAMES.SYSTEM_MAP_VALIDATE,
|
|
667
|
-
|
|
983
|
+
run: runSystemMapGate,
|
|
668
984
|
warnOnly: true,
|
|
669
985
|
},
|
|
986
|
+
// WU-1191: Lane health check (configurable: warn/error/off)
|
|
987
|
+
{
|
|
988
|
+
name: GATE_NAMES.LANE_HEALTH,
|
|
989
|
+
run: (ctx) => runLaneHealthGate({ ...ctx, mode: laneHealthMode }),
|
|
990
|
+
warnOnly: laneHealthMode !== 'error',
|
|
991
|
+
},
|
|
670
992
|
// WU-2062: Safety-critical tests ALWAYS run
|
|
671
|
-
|
|
993
|
+
// WU-1280: When tests_required=false (methodology.testing: none), failures only warn
|
|
994
|
+
{
|
|
995
|
+
name: GATE_NAMES.SAFETY_CRITICAL_TEST,
|
|
996
|
+
cmd: GATE_COMMANDS.SAFETY_CRITICAL_TEST,
|
|
997
|
+
warnOnly: !testsRequired,
|
|
998
|
+
},
|
|
672
999
|
// WU-1920: Use changed tests by default, full suite with --full-tests
|
|
673
1000
|
// WU-2244: --full-coverage implies --full-tests for accurate coverage
|
|
1001
|
+
// WU-1280: When tests_required=false (methodology.testing: none), failures only warn
|
|
674
1002
|
{
|
|
675
1003
|
name: GATE_NAMES.TEST,
|
|
676
1004
|
cmd: isFullTests || isFullCoverage
|
|
677
1005
|
? pnpmCmd('turbo', 'run', 'test')
|
|
678
1006
|
: GATE_COMMANDS.INCREMENTAL_TEST,
|
|
1007
|
+
warnOnly: !testsRequired,
|
|
679
1008
|
},
|
|
680
1009
|
// WU-2062: Integration tests only for high-risk changes
|
|
1010
|
+
// WU-1280: When tests_required=false (methodology.testing: none), failures only warn
|
|
681
1011
|
...(riskTier && riskTier.shouldRunIntegration
|
|
682
|
-
? [
|
|
1012
|
+
? [
|
|
1013
|
+
{
|
|
1014
|
+
name: GATE_NAMES.INTEGRATION_TEST,
|
|
1015
|
+
cmd: GATE_COMMANDS.TIERED_TEST,
|
|
1016
|
+
warnOnly: !testsRequired,
|
|
1017
|
+
},
|
|
1018
|
+
]
|
|
683
1019
|
: []),
|
|
684
1020
|
// WU-1433: Coverage gate with configurable mode (warn/block)
|
|
685
1021
|
{ name: GATE_NAMES.COVERAGE, cmd: GATE_COMMANDS.COVERAGE_GATE },
|
|
@@ -695,9 +1031,16 @@ async function main() {
|
|
|
695
1031
|
// Run all gates sequentially
|
|
696
1032
|
// WU-1920: Track last test result to skip coverage gate on changed tests
|
|
697
1033
|
let lastTestResult = null;
|
|
1034
|
+
let lastFormatCheckFiles = null;
|
|
698
1035
|
for (const gate of gates) {
|
|
699
1036
|
let result;
|
|
700
|
-
if (gate.
|
|
1037
|
+
if (gate.run) {
|
|
1038
|
+
result = await gate.run({ agentLog, useAgentMode });
|
|
1039
|
+
if (gate.name === GATE_NAMES.FORMAT_CHECK) {
|
|
1040
|
+
lastFormatCheckFiles = result.filesChecked ?? null;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
else if (gate.cmd === GATE_COMMANDS.INVARIANTS) {
|
|
701
1044
|
// WU-2252: Invariants check runs first (non-bypassable)
|
|
702
1045
|
const logLine = useAgentMode
|
|
703
1046
|
? (line) => writeSync(agentLog.logFd, `${line}\n`)
|
|
@@ -744,14 +1087,17 @@ async function main() {
|
|
|
744
1087
|
continue;
|
|
745
1088
|
}
|
|
746
1089
|
// WU-1433: Special handling for coverage gate
|
|
1090
|
+
// WU-1262: Include threshold from resolved policy in log
|
|
747
1091
|
if (!useAgentMode) {
|
|
748
|
-
console.log(`\n> Coverage gate (mode: ${coverageMode})\n`);
|
|
1092
|
+
console.log(`\n> Coverage gate (mode: ${coverageMode}, threshold: ${coverageThreshold}%)\n`);
|
|
749
1093
|
}
|
|
750
1094
|
else {
|
|
751
|
-
writeSync(agentLog.logFd, `\n> Coverage gate (mode: ${coverageMode})\n\n`);
|
|
1095
|
+
writeSync(agentLog.logFd, `\n> Coverage gate (mode: ${coverageMode}, threshold: ${coverageThreshold}%)\n\n`);
|
|
752
1096
|
}
|
|
753
1097
|
result = await runCoverageGate({
|
|
754
1098
|
mode: coverageMode,
|
|
1099
|
+
// WU-1262: Pass resolved threshold from methodology policy
|
|
1100
|
+
threshold: coverageThreshold,
|
|
755
1101
|
logger: useAgentMode
|
|
756
1102
|
? {
|
|
757
1103
|
log: (msg) => {
|
|
@@ -785,7 +1131,7 @@ async function main() {
|
|
|
785
1131
|
continue;
|
|
786
1132
|
}
|
|
787
1133
|
if (gate.name === GATE_NAMES.FORMAT_CHECK) {
|
|
788
|
-
emitFormatCheckGuidance({ agentLog, useAgentMode });
|
|
1134
|
+
emitFormatCheckGuidance({ agentLog, useAgentMode, files: lastFormatCheckFiles });
|
|
789
1135
|
}
|
|
790
1136
|
if (useAgentMode) {
|
|
791
1137
|
const tail = readLogTail(agentLog.logPath);
|
|
@@ -807,13 +1153,19 @@ async function main() {
|
|
|
807
1153
|
else {
|
|
808
1154
|
console.log(`✅ All gates passed (agent mode). Log: ${agentLog.logPath}\n`);
|
|
809
1155
|
}
|
|
810
|
-
|
|
1156
|
+
return true;
|
|
811
1157
|
}
|
|
812
1158
|
// WU-1071: Use import.meta.main instead of process.argv[1] comparison
|
|
813
1159
|
// The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
|
|
814
1160
|
// path but import.meta.url resolves to the real path - they never match
|
|
815
1161
|
if (import.meta.main) {
|
|
816
|
-
|
|
1162
|
+
// eslint-disable-next-line sonarjs/deprecation -- Pre-existing: parseGatesArgs kept for backwards compatibility
|
|
1163
|
+
const opts = parseGatesArgs();
|
|
1164
|
+
executeGates({ ...opts, argv: process.argv.slice(2) })
|
|
1165
|
+
.then((ok) => {
|
|
1166
|
+
process.exit(ok ? EXIT_CODES.SUCCESS : EXIT_CODES.ERROR);
|
|
1167
|
+
})
|
|
1168
|
+
.catch((error) => {
|
|
817
1169
|
console.error('Gates failed:', error);
|
|
818
1170
|
process.exit(EXIT_CODES.ERROR);
|
|
819
1171
|
});
|