@lumenflow/cli 2.2.2 → 2.3.2
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 +61 -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 +356 -101
- package/dist/guard-locked.js +4 -3
- package/dist/guard-worktree-commit.js +4 -3
- package/dist/init.js +517 -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/release.js +69 -29
- 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 +4 -3
- package/dist/validate-skills-spec.js +4 -3
- package/dist/validate.js +4 -3
- 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 +39 -12
- 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,21 +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
58
|
import { validateBacklogSync } from '@lumenflow/core/dist/validators/backlog-sync.js';
|
|
59
59
|
import { runSupabaseDocsLinter } from '@lumenflow/core/dist/validators/supabase-docs-linter.js';
|
|
60
60
|
import { runSystemMapValidation } from '@lumenflow/core/dist/system-map-validator.js';
|
|
61
|
-
|
|
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';
|
|
62
69
|
/**
|
|
63
70
|
* WU-1087: Gates-specific option definitions for createWUParser.
|
|
64
71
|
* Exported for testing and consistency with other CLI commands.
|
|
@@ -141,6 +148,7 @@ export function parseGatesOptions() {
|
|
|
141
148
|
* @deprecated Use parseGatesOptions() instead (WU-1087)
|
|
142
149
|
* Kept for backward compatibility during migration.
|
|
143
150
|
*/
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Pre-existing: argv kept for backwards compatibility
|
|
144
152
|
function parseGatesArgs(argv = process.argv) {
|
|
145
153
|
return parseGatesOptions();
|
|
146
154
|
}
|
|
@@ -157,6 +165,91 @@ function pnpmRun(script, ...args) {
|
|
|
157
165
|
const argsStr = args.length > 0 ? ` ${args.join(' ')}` : '';
|
|
158
166
|
return `${PKG_MANAGER} ${SCRIPTS.RUN} ${script}${argsStr}`;
|
|
159
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
|
+
}
|
|
160
253
|
export function parsePrettierListOutput(output) {
|
|
161
254
|
if (!output)
|
|
162
255
|
return [];
|
|
@@ -174,6 +267,10 @@ export function buildPrettierWriteCommand(files) {
|
|
|
174
267
|
const base = pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_FLAGS.WRITE);
|
|
175
268
|
return quotedFiles ? `${base} ${quotedFiles}` : base;
|
|
176
269
|
}
|
|
270
|
+
function buildPrettierCheckCommand(files) {
|
|
271
|
+
const filesArg = files.length > 0 ? quoteShellArgs(files) : '.';
|
|
272
|
+
return pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_ARGS.CHECK, filesArg);
|
|
273
|
+
}
|
|
177
274
|
export function formatFormatCheckGuidance(files) {
|
|
178
275
|
if (!files.length)
|
|
179
276
|
return [];
|
|
@@ -189,8 +286,10 @@ export function formatFormatCheckGuidance(files) {
|
|
|
189
286
|
'',
|
|
190
287
|
];
|
|
191
288
|
}
|
|
192
|
-
function collectPrettierListDifferent(cwd) {
|
|
193
|
-
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
|
|
194
293
|
const result = spawnSync(cmd, [], {
|
|
195
294
|
shell: true,
|
|
196
295
|
cwd,
|
|
@@ -199,11 +298,11 @@ function collectPrettierListDifferent(cwd) {
|
|
|
199
298
|
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
200
299
|
return parsePrettierListOutput(output);
|
|
201
300
|
}
|
|
202
|
-
function emitFormatCheckGuidance({ agentLog, useAgentMode, }) {
|
|
203
|
-
const
|
|
204
|
-
if (!
|
|
301
|
+
function emitFormatCheckGuidance({ agentLog, useAgentMode, files, }) {
|
|
302
|
+
const formattedFiles = collectPrettierListDifferent(process.cwd(), files ?? []);
|
|
303
|
+
if (!formattedFiles.length)
|
|
205
304
|
return;
|
|
206
|
-
const lines = formatFormatCheckGuidance(
|
|
305
|
+
const lines = formatFormatCheckGuidance(formattedFiles);
|
|
207
306
|
const logLine = useAgentMode && agentLog
|
|
208
307
|
? (line) => writeSync(agentLog.logFd, `${line}\n`)
|
|
209
308
|
: (line) => console.log(line);
|
|
@@ -211,12 +310,6 @@ function emitFormatCheckGuidance({ agentLog, useAgentMode, }) {
|
|
|
211
310
|
logLine(line);
|
|
212
311
|
}
|
|
213
312
|
}
|
|
214
|
-
/**
|
|
215
|
-
* Build a pnpm --filter command string
|
|
216
|
-
*/
|
|
217
|
-
function pnpmFilter(pkg, script) {
|
|
218
|
-
return `${PKG_MANAGER} ${PKG_FLAGS.FILTER} ${pkg} ${script}`;
|
|
219
|
-
}
|
|
220
313
|
function readLogTail(logPath, { maxLines = 40, maxBytes = 64 * 1024 } = {}) {
|
|
221
314
|
try {
|
|
222
315
|
const stats = statSync(logPath);
|
|
@@ -261,6 +354,7 @@ function run(cmd, { agentLog } = {}) {
|
|
|
261
354
|
if (!agentLog) {
|
|
262
355
|
console.log(`\n> ${cmd}\n`);
|
|
263
356
|
try {
|
|
357
|
+
// eslint-disable-next-line sonarjs/os-command -- Pre-existing: cmd is built from trusted constants
|
|
264
358
|
execSync(cmd, { stdio: 'inherit', encoding: FILE_SYSTEM.ENCODING });
|
|
265
359
|
return { ok: true, duration: Date.now() - start };
|
|
266
360
|
}
|
|
@@ -269,6 +363,7 @@ function run(cmd, { agentLog } = {}) {
|
|
|
269
363
|
}
|
|
270
364
|
}
|
|
271
365
|
writeSync(agentLog.logFd, `\n> ${cmd}\n\n`);
|
|
366
|
+
// eslint-disable-next-line sonarjs/os-command -- Pre-existing: cmd is built from trusted constants
|
|
272
367
|
const result = spawnSync(cmd, [], {
|
|
273
368
|
shell: true,
|
|
274
369
|
stdio: ['ignore', agentLog.logFd, agentLog.logFd],
|
|
@@ -342,6 +437,119 @@ async function runSystemMapGate({ agentLog, useAgentMode }) {
|
|
|
342
437
|
}
|
|
343
438
|
return { ok: result.valid, duration: Date.now() - start };
|
|
344
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
|
+
}
|
|
345
553
|
/**
|
|
346
554
|
* Run incremental ESLint on changed files only
|
|
347
555
|
* Falls back to full lint if on main branch or if incremental fails
|
|
@@ -356,56 +564,36 @@ async function runIncrementalLint({ agentLog, } = {}) {
|
|
|
356
564
|
}
|
|
357
565
|
writeSync(agentLog.logFd, `${line}\n`);
|
|
358
566
|
};
|
|
359
|
-
// WU-1006: Skip incremental lint if apps/web doesn't exist (repo-agnostic)
|
|
360
|
-
const webDir = path.join(process.cwd(), DIRECTORIES.APPS_WEB);
|
|
361
|
-
try {
|
|
362
|
-
await access(webDir);
|
|
363
|
-
}
|
|
364
|
-
catch {
|
|
365
|
-
logLine('\n> ESLint (incremental) skipped (apps/web not present)\n');
|
|
366
|
-
return { ok: true, duration: Date.now() - start, fileCount: 0 };
|
|
367
|
-
}
|
|
368
567
|
try {
|
|
369
568
|
// Check if we're on main branch
|
|
370
569
|
const git = getGitForCwd();
|
|
371
570
|
const currentBranch = await git.getCurrentBranch();
|
|
372
|
-
|
|
571
|
+
const isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
|
|
572
|
+
if (isMainBranch) {
|
|
373
573
|
logLine('📋 On main branch - running full lint');
|
|
374
|
-
const result = run(
|
|
574
|
+
const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog });
|
|
375
575
|
return { ...result, fileCount: -1 };
|
|
376
576
|
}
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
|
|
380
|
-
filterPath: DIRECTORIES.APPS_WEB,
|
|
381
|
-
});
|
|
382
|
-
if (changedFiles.length === 0) {
|
|
577
|
+
const changedFiles = await getChangedLintableFiles({ git });
|
|
578
|
+
const plan = resolveLintPlan({ isMainBranch, changedFiles });
|
|
579
|
+
if (plan.mode === 'skip') {
|
|
383
580
|
logLine('\n> ESLint (incremental)\n');
|
|
384
581
|
logLine('✅ No lintable files changed - skipping lint');
|
|
385
582
|
return { ok: true, duration: Date.now() - start, fileCount: 0 };
|
|
386
583
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
catch {
|
|
394
|
-
return null;
|
|
395
|
-
}
|
|
396
|
-
}))).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);
|
|
397
590
|
if (existingFiles.length === 0) {
|
|
398
591
|
logLine('\n> ESLint (incremental)\n');
|
|
399
592
|
logLine('✅ All changed files were deleted - skipping lint');
|
|
400
593
|
return { ok: true, duration: Date.now() - start, fileCount: 0 };
|
|
401
594
|
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const packageRelativeFiles = convertToPackageRelativePaths(existingFiles, DIRECTORIES.APPS_WEB);
|
|
405
|
-
logLine(`\n> ESLint (incremental: ${packageRelativeFiles.length} files)\n`);
|
|
406
|
-
logLine(`Files to lint:\n ${packageRelativeFiles.join('\n ')}\n`);
|
|
407
|
-
// WU-2571: Run ESLint from apps/web directory with package-relative paths
|
|
408
|
-
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`);
|
|
409
597
|
const result = spawnSync(PKG_MANAGER, [
|
|
410
598
|
ESLINT_COMMANDS.ESLINT,
|
|
411
599
|
ESLINT_FLAGS.MAX_WARNINGS,
|
|
@@ -417,28 +605,28 @@ async function runIncrementalLint({ agentLog, } = {}) {
|
|
|
417
605
|
ESLINT_FLAGS.CACHE_LOCATION,
|
|
418
606
|
'.eslintcache',
|
|
419
607
|
ESLINT_FLAGS.PASS_ON_UNPRUNED,
|
|
420
|
-
...
|
|
608
|
+
...existingFiles,
|
|
421
609
|
], agentLog
|
|
422
610
|
? {
|
|
423
611
|
stdio: ['ignore', agentLog.logFd, agentLog.logFd],
|
|
424
612
|
encoding: FILE_SYSTEM.ENCODING,
|
|
425
|
-
cwd:
|
|
613
|
+
cwd: process.cwd(),
|
|
426
614
|
}
|
|
427
615
|
: {
|
|
428
616
|
stdio: 'inherit',
|
|
429
617
|
encoding: FILE_SYSTEM.ENCODING,
|
|
430
|
-
cwd:
|
|
618
|
+
cwd: process.cwd(),
|
|
431
619
|
});
|
|
432
620
|
const duration = Date.now() - start;
|
|
433
621
|
return {
|
|
434
622
|
ok: result.status === EXIT_CODES.SUCCESS,
|
|
435
623
|
duration,
|
|
436
|
-
fileCount:
|
|
624
|
+
fileCount: existingFiles.length,
|
|
437
625
|
};
|
|
438
626
|
}
|
|
439
627
|
catch (error) {
|
|
440
628
|
console.error('⚠️ Incremental lint failed, falling back to full lint:', error.message);
|
|
441
|
-
const result = run(
|
|
629
|
+
const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog });
|
|
442
630
|
return { ...result, fileCount: -1 };
|
|
443
631
|
}
|
|
444
632
|
}
|
|
@@ -461,29 +649,53 @@ async function runChangedTests({ agentLog, } = {}) {
|
|
|
461
649
|
try {
|
|
462
650
|
const git = getGitForCwd();
|
|
463
651
|
const currentBranch = await git.getCurrentBranch();
|
|
464
|
-
|
|
652
|
+
const isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
|
|
653
|
+
if (isMainBranch) {
|
|
465
654
|
logLine('📋 On main branch - running full test suite');
|
|
466
655
|
const result = run(pnpmCmd('turbo', 'run', 'test'), { agentLog });
|
|
467
656
|
return { ...result, isIncremental: false };
|
|
468
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);
|
|
469
668
|
const untrackedOutput = await git.raw(['ls-files', '--others', '--exclude-standard']);
|
|
470
669
|
const untrackedFiles = untrackedOutput
|
|
471
670
|
.split(/\r?\n/)
|
|
472
671
|
.map((f) => f.trim())
|
|
473
672
|
.filter(Boolean);
|
|
474
673
|
const untrackedCodeFiles = untrackedFiles.filter(isCodeFilePath);
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
+
}
|
|
478
692
|
logLine('📋 Running full test suite to avoid missing coverage');
|
|
479
693
|
const result = run(pnpmCmd('turbo', 'run', 'test'), { agentLog });
|
|
480
694
|
return { ...result, duration: Date.now() - start, isIncremental: false };
|
|
481
695
|
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
const result = run(pnpmCmd('turbo', 'run', 'test'), { agentLog });
|
|
486
|
-
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 };
|
|
487
699
|
}
|
|
488
700
|
catch (error) {
|
|
489
701
|
console.error('⚠️ Changed tests failed, falling back to full suite:', error.message);
|
|
@@ -613,31 +825,29 @@ async function runIntegrationTests({ agentLog, } = {}) {
|
|
|
613
825
|
return { ok: false, duration: Date.now() - start };
|
|
614
826
|
}
|
|
615
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
|
+
}
|
|
616
847
|
async function getAllChangedFiles(options = {}) {
|
|
617
848
|
const { git = getGitForCwd() } = options;
|
|
618
849
|
try {
|
|
619
|
-
|
|
620
|
-
const mergeBase = await git.mergeBase('HEAD', 'origin/main');
|
|
621
|
-
// Get committed changes
|
|
622
|
-
const committedOutput = await git.raw(['diff', '--name-only', `${mergeBase}...HEAD`]);
|
|
623
|
-
const committedFiles = committedOutput
|
|
624
|
-
.split('\n')
|
|
625
|
-
.map((f) => f.trim())
|
|
626
|
-
.filter(Boolean);
|
|
627
|
-
// Get unstaged changes
|
|
628
|
-
const unstagedOutput = await git.raw(['diff', '--name-only']);
|
|
629
|
-
const unstagedFiles = unstagedOutput
|
|
630
|
-
.split('\n')
|
|
631
|
-
.map((f) => f.trim())
|
|
632
|
-
.filter(Boolean);
|
|
633
|
-
// Get untracked files
|
|
634
|
-
const untrackedOutput = await git.raw(['ls-files', '--others', '--exclude-standard']);
|
|
635
|
-
const untrackedFiles = untrackedOutput
|
|
636
|
-
.split('\n')
|
|
637
|
-
.map((f) => f.trim())
|
|
638
|
-
.filter(Boolean);
|
|
639
|
-
// Combine and deduplicate
|
|
640
|
-
return [...new Set([...committedFiles, ...unstagedFiles, ...untrackedFiles])];
|
|
850
|
+
return await getChangedFilesForIncremental({ git });
|
|
641
851
|
}
|
|
642
852
|
catch (error) {
|
|
643
853
|
console.error('⚠️ Failed to get changed files:', error.message);
|
|
@@ -680,9 +890,20 @@ async function executeGates(opts) {
|
|
|
680
890
|
const isFullTests = opts.fullTests || false;
|
|
681
891
|
// WU-2244: Full coverage flag forces full test suite and coverage gate (deterministic)
|
|
682
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());
|
|
683
897
|
// WU-1433: Coverage gate mode (warn or block)
|
|
684
898
|
// WU-2334: Default changed from WARN to BLOCK for TDD enforcement
|
|
685
|
-
|
|
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());
|
|
686
907
|
if (useAgentMode) {
|
|
687
908
|
console.log(`🧾 gates (agent mode): output -> ${agentLog.logPath} (use --verbose for streaming)\n`);
|
|
688
909
|
}
|
|
@@ -720,7 +941,7 @@ async function executeGates(opts) {
|
|
|
720
941
|
? [
|
|
721
942
|
// WU-2252: Invariants check runs first (non-bypassable)
|
|
722
943
|
{ name: GATE_NAMES.INVARIANTS, cmd: GATE_COMMANDS.INVARIANTS },
|
|
723
|
-
{ name: GATE_NAMES.FORMAT_CHECK,
|
|
944
|
+
{ name: GATE_NAMES.FORMAT_CHECK, run: runFormatCheckGate },
|
|
724
945
|
{ name: GATE_NAMES.SPEC_LINTER, cmd: pnpmRun(SCRIPTS.SPEC_LINTER) },
|
|
725
946
|
{
|
|
726
947
|
name: GATE_NAMES.PROMPTS_LINT,
|
|
@@ -733,14 +954,20 @@ async function executeGates(opts) {
|
|
|
733
954
|
run: runSystemMapGate,
|
|
734
955
|
warnOnly: true,
|
|
735
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
|
+
},
|
|
736
963
|
]
|
|
737
964
|
: [
|
|
738
965
|
// WU-2252: Invariants check runs first (non-bypassable)
|
|
739
966
|
{ name: GATE_NAMES.INVARIANTS, cmd: GATE_COMMANDS.INVARIANTS },
|
|
740
|
-
{ name: GATE_NAMES.FORMAT_CHECK,
|
|
967
|
+
{ name: GATE_NAMES.FORMAT_CHECK, run: runFormatCheckGate },
|
|
741
968
|
{
|
|
742
969
|
name: GATE_NAMES.LINT,
|
|
743
|
-
cmd: isFullLint ?
|
|
970
|
+
cmd: isFullLint ? pnpmCmd(SCRIPTS.LINT) : GATE_COMMANDS.INCREMENTAL,
|
|
744
971
|
},
|
|
745
972
|
{ name: GATE_NAMES.TYPECHECK, cmd: pnpmCmd(SCRIPTS.TYPECHECK) },
|
|
746
973
|
{ name: GATE_NAMES.SPEC_LINTER, cmd: pnpmRun(SCRIPTS.SPEC_LINTER) },
|
|
@@ -756,19 +983,39 @@ async function executeGates(opts) {
|
|
|
756
983
|
run: runSystemMapGate,
|
|
757
984
|
warnOnly: true,
|
|
758
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
|
+
},
|
|
759
992
|
// WU-2062: Safety-critical tests ALWAYS run
|
|
760
|
-
|
|
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
|
+
},
|
|
761
999
|
// WU-1920: Use changed tests by default, full suite with --full-tests
|
|
762
1000
|
// WU-2244: --full-coverage implies --full-tests for accurate coverage
|
|
1001
|
+
// WU-1280: When tests_required=false (methodology.testing: none), failures only warn
|
|
763
1002
|
{
|
|
764
1003
|
name: GATE_NAMES.TEST,
|
|
765
1004
|
cmd: isFullTests || isFullCoverage
|
|
766
1005
|
? pnpmCmd('turbo', 'run', 'test')
|
|
767
1006
|
: GATE_COMMANDS.INCREMENTAL_TEST,
|
|
1007
|
+
warnOnly: !testsRequired,
|
|
768
1008
|
},
|
|
769
1009
|
// WU-2062: Integration tests only for high-risk changes
|
|
1010
|
+
// WU-1280: When tests_required=false (methodology.testing: none), failures only warn
|
|
770
1011
|
...(riskTier && riskTier.shouldRunIntegration
|
|
771
|
-
? [
|
|
1012
|
+
? [
|
|
1013
|
+
{
|
|
1014
|
+
name: GATE_NAMES.INTEGRATION_TEST,
|
|
1015
|
+
cmd: GATE_COMMANDS.TIERED_TEST,
|
|
1016
|
+
warnOnly: !testsRequired,
|
|
1017
|
+
},
|
|
1018
|
+
]
|
|
772
1019
|
: []),
|
|
773
1020
|
// WU-1433: Coverage gate with configurable mode (warn/block)
|
|
774
1021
|
{ name: GATE_NAMES.COVERAGE, cmd: GATE_COMMANDS.COVERAGE_GATE },
|
|
@@ -784,10 +1031,14 @@ async function executeGates(opts) {
|
|
|
784
1031
|
// Run all gates sequentially
|
|
785
1032
|
// WU-1920: Track last test result to skip coverage gate on changed tests
|
|
786
1033
|
let lastTestResult = null;
|
|
1034
|
+
let lastFormatCheckFiles = null;
|
|
787
1035
|
for (const gate of gates) {
|
|
788
1036
|
let result;
|
|
789
1037
|
if (gate.run) {
|
|
790
1038
|
result = await gate.run({ agentLog, useAgentMode });
|
|
1039
|
+
if (gate.name === GATE_NAMES.FORMAT_CHECK) {
|
|
1040
|
+
lastFormatCheckFiles = result.filesChecked ?? null;
|
|
1041
|
+
}
|
|
791
1042
|
}
|
|
792
1043
|
else if (gate.cmd === GATE_COMMANDS.INVARIANTS) {
|
|
793
1044
|
// WU-2252: Invariants check runs first (non-bypassable)
|
|
@@ -836,14 +1087,17 @@ async function executeGates(opts) {
|
|
|
836
1087
|
continue;
|
|
837
1088
|
}
|
|
838
1089
|
// WU-1433: Special handling for coverage gate
|
|
1090
|
+
// WU-1262: Include threshold from resolved policy in log
|
|
839
1091
|
if (!useAgentMode) {
|
|
840
|
-
console.log(`\n> Coverage gate (mode: ${coverageMode})\n`);
|
|
1092
|
+
console.log(`\n> Coverage gate (mode: ${coverageMode}, threshold: ${coverageThreshold}%)\n`);
|
|
841
1093
|
}
|
|
842
1094
|
else {
|
|
843
|
-
writeSync(agentLog.logFd, `\n> Coverage gate (mode: ${coverageMode})\n\n`);
|
|
1095
|
+
writeSync(agentLog.logFd, `\n> Coverage gate (mode: ${coverageMode}, threshold: ${coverageThreshold}%)\n\n`);
|
|
844
1096
|
}
|
|
845
1097
|
result = await runCoverageGate({
|
|
846
1098
|
mode: coverageMode,
|
|
1099
|
+
// WU-1262: Pass resolved threshold from methodology policy
|
|
1100
|
+
threshold: coverageThreshold,
|
|
847
1101
|
logger: useAgentMode
|
|
848
1102
|
? {
|
|
849
1103
|
log: (msg) => {
|
|
@@ -877,7 +1131,7 @@ async function executeGates(opts) {
|
|
|
877
1131
|
continue;
|
|
878
1132
|
}
|
|
879
1133
|
if (gate.name === GATE_NAMES.FORMAT_CHECK) {
|
|
880
|
-
emitFormatCheckGuidance({ agentLog, useAgentMode });
|
|
1134
|
+
emitFormatCheckGuidance({ agentLog, useAgentMode, files: lastFormatCheckFiles });
|
|
881
1135
|
}
|
|
882
1136
|
if (useAgentMode) {
|
|
883
1137
|
const tail = readLogTail(agentLog.logPath);
|
|
@@ -905,6 +1159,7 @@ async function executeGates(opts) {
|
|
|
905
1159
|
// The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
|
|
906
1160
|
// path but import.meta.url resolves to the real path - they never match
|
|
907
1161
|
if (import.meta.main) {
|
|
1162
|
+
// eslint-disable-next-line sonarjs/deprecation -- Pre-existing: parseGatesArgs kept for backwards compatibility
|
|
908
1163
|
const opts = parseGatesArgs();
|
|
909
1164
|
executeGates({ ...opts, argv: process.argv.slice(2) })
|
|
910
1165
|
.then((ok) => {
|