@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.
Files changed (119) hide show
  1. package/README.md +147 -57
  2. package/dist/__tests__/agent-log-issue.test.js +56 -0
  3. package/dist/__tests__/cli-entry-point.test.js +66 -17
  4. package/dist/__tests__/cli-subprocess.test.js +25 -0
  5. package/dist/__tests__/init.test.js +298 -0
  6. package/dist/__tests__/initiative-plan.test.js +340 -0
  7. package/dist/__tests__/mem-cleanup-execution.test.js +19 -0
  8. package/dist/__tests__/merge-block.test.js +220 -0
  9. package/dist/__tests__/release.test.js +28 -0
  10. package/dist/__tests__/safe-git.test.js +191 -0
  11. package/dist/__tests__/state-doctor.test.js +274 -0
  12. package/dist/__tests__/wu-done.test.js +36 -0
  13. package/dist/__tests__/wu-edit.test.js +119 -0
  14. package/dist/__tests__/wu-prep.test.js +108 -0
  15. package/dist/agent-issues-query.js +4 -3
  16. package/dist/agent-log-issue.js +25 -4
  17. package/dist/backlog-prune.js +5 -4
  18. package/dist/cli-entry-point.js +11 -1
  19. package/dist/doctor.js +368 -0
  20. package/dist/flow-bottlenecks.js +6 -5
  21. package/dist/flow-report.js +4 -3
  22. package/dist/gates.js +468 -116
  23. package/dist/guard-locked.js +4 -3
  24. package/dist/guard-worktree-commit.js +4 -3
  25. package/dist/init.js +508 -86
  26. package/dist/initiative-add-wu.js +4 -3
  27. package/dist/initiative-bulk-assign-wus.js +8 -5
  28. package/dist/initiative-create.js +73 -37
  29. package/dist/initiative-edit.js +37 -21
  30. package/dist/initiative-list.js +4 -3
  31. package/dist/initiative-plan.js +337 -0
  32. package/dist/initiative-status.js +4 -3
  33. package/dist/lane-health.js +377 -0
  34. package/dist/lane-suggest.js +382 -0
  35. package/dist/mem-checkpoint.js +2 -2
  36. package/dist/mem-cleanup.js +2 -2
  37. package/dist/mem-context.js +306 -0
  38. package/dist/mem-create.js +2 -2
  39. package/dist/mem-delete.js +293 -0
  40. package/dist/mem-inbox.js +2 -2
  41. package/dist/mem-index.js +211 -0
  42. package/dist/mem-init.js +1 -1
  43. package/dist/mem-profile.js +207 -0
  44. package/dist/mem-promote.js +254 -0
  45. package/dist/mem-ready.js +2 -2
  46. package/dist/mem-signal.js +2 -2
  47. package/dist/mem-start.js +2 -2
  48. package/dist/mem-summarize.js +2 -2
  49. package/dist/mem-triage.js +2 -2
  50. package/dist/merge-block.js +222 -0
  51. package/dist/metrics-cli.js +7 -4
  52. package/dist/metrics-snapshot.js +4 -3
  53. package/dist/orchestrate-initiative.js +10 -4
  54. package/dist/orchestrate-monitor.js +379 -31
  55. package/dist/signal-cleanup.js +296 -0
  56. package/dist/spawn-list.js +6 -5
  57. package/dist/state-bootstrap.js +5 -4
  58. package/dist/state-cleanup.js +360 -0
  59. package/dist/state-doctor-fix.js +196 -0
  60. package/dist/state-doctor.js +501 -0
  61. package/dist/validate-agent-skills.js +4 -3
  62. package/dist/validate-agent-sync.js +4 -3
  63. package/dist/validate-backlog-sync.js +7 -84
  64. package/dist/validate-skills-spec.js +4 -3
  65. package/dist/validate.js +7 -107
  66. package/dist/wu-block.js +3 -3
  67. package/dist/wu-claim.js +208 -98
  68. package/dist/wu-cleanup.js +5 -4
  69. package/dist/wu-create.js +71 -46
  70. package/dist/wu-delete.js +88 -60
  71. package/dist/wu-deps.js +6 -5
  72. package/dist/wu-done-check.js +34 -0
  73. package/dist/wu-done.js +60 -24
  74. package/dist/wu-edit.js +63 -28
  75. package/dist/wu-infer-lane.js +7 -6
  76. package/dist/wu-preflight.js +23 -81
  77. package/dist/wu-prep.js +125 -0
  78. package/dist/wu-prune.js +4 -3
  79. package/dist/wu-recover.js +88 -22
  80. package/dist/wu-repair.js +7 -6
  81. package/dist/wu-spawn.js +226 -270
  82. package/dist/wu-status.js +4 -3
  83. package/dist/wu-unblock.js +5 -5
  84. package/dist/wu-unlock-lane.js +4 -3
  85. package/dist/wu-validate.js +5 -4
  86. package/package.json +16 -7
  87. package/templates/core/.lumenflow/constraints.md.template +192 -0
  88. package/templates/core/.lumenflow/rules/git-safety.md.template +27 -0
  89. package/templates/core/.lumenflow/rules/wu-workflow.md.template +48 -0
  90. package/templates/core/AGENTS.md.template +60 -0
  91. package/templates/core/LUMENFLOW.md.template +255 -0
  92. package/templates/core/UPGRADING.md.template +121 -0
  93. package/templates/core/ai/onboarding/agent-safety-card.md.template +106 -0
  94. package/templates/core/ai/onboarding/first-wu-mistakes.md.template +198 -0
  95. package/templates/core/ai/onboarding/quick-ref-commands.md.template +186 -0
  96. package/templates/core/ai/onboarding/release-process.md.template +362 -0
  97. package/templates/core/ai/onboarding/troubleshooting-wu-done.md.template +159 -0
  98. package/templates/core/ai/onboarding/wu-create-checklist.md.template +117 -0
  99. package/templates/vendors/aider/.aider.conf.yml.template +27 -0
  100. package/templates/vendors/claude/.claude/CLAUDE.md.template +52 -0
  101. package/templates/vendors/claude/.claude/settings.json.template +49 -0
  102. package/templates/vendors/claude/.claude/skills/bug-classification/SKILL.md.template +192 -0
  103. package/templates/vendors/claude/.claude/skills/code-quality/SKILL.md.template +152 -0
  104. package/templates/vendors/claude/.claude/skills/context-management/SKILL.md.template +155 -0
  105. package/templates/vendors/claude/.claude/skills/execution-memory/SKILL.md.template +304 -0
  106. package/templates/vendors/claude/.claude/skills/frontend-design/SKILL.md.template +131 -0
  107. package/templates/vendors/claude/.claude/skills/initiative-management/SKILL.md.template +164 -0
  108. package/templates/vendors/claude/.claude/skills/library-first/SKILL.md.template +98 -0
  109. package/templates/vendors/claude/.claude/skills/lumenflow-gates/SKILL.md.template +87 -0
  110. package/templates/vendors/claude/.claude/skills/multi-agent-coordination/SKILL.md.template +84 -0
  111. package/templates/vendors/claude/.claude/skills/ops-maintenance/SKILL.md.template +254 -0
  112. package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +189 -0
  113. package/templates/vendors/claude/.claude/skills/tdd-workflow/SKILL.md.template +139 -0
  114. package/templates/vendors/claude/.claude/skills/worktree-discipline/SKILL.md.template +138 -0
  115. package/templates/vendors/claude/.claude/skills/wu-lifecycle/SKILL.md.template +106 -0
  116. package/templates/vendors/cline/.clinerules.template +53 -0
  117. package/templates/vendors/cursor/.cursor/rules/lumenflow.md.template +34 -0
  118. package/templates/vendors/cursor/.cursor/rules.md.template +28 -0
  119. 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.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
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, convertToPackageRelativePaths, } from '@lumenflow/core/dist/incremental-lint.js';
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
- // 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';
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 { 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, PRETTIER_ARGS, PRETTIER_FLAGS, } from '@lumenflow/core/dist/wu-constants.js';
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 cmd = pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_ARGS.LIST_DIFFERENT, '.');
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 files = collectPrettierListDifferent(process.cwd());
201
- if (!files.length)
301
+ function emitFormatCheckGuidance({ agentLog, useAgentMode, files, }) {
302
+ const formattedFiles = collectPrettierListDifferent(process.cwd(), files ?? []);
303
+ if (!formattedFiles.length)
202
304
  return;
203
- const lines = formatFormatCheckGuidance(files);
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
- if (currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER) {
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(pnpmFilter(PACKAGES.WEB, SCRIPTS.LINT), { agentLog });
574
+ const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog });
307
575
  return { ...result, fileCount: -1 };
308
576
  }
309
- // Get changed files in apps/web
310
- const changedFiles = await getChangedLintableFiles({
311
- git,
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
- // Filter to files that still exist (in case of deletions)
320
- const existingFiles = (await Promise.all(changedFiles.map(async (f) => {
321
- try {
322
- await access(f);
323
- return f;
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
- // WU-2571: Convert repo-relative paths to package-relative paths
335
- // ESLint runs from apps/web/ where repo-relative paths don't exist
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
- ...packageRelativeFiles,
608
+ ...existingFiles,
353
609
  ], agentLog
354
610
  ? {
355
611
  stdio: ['ignore', agentLog.logFd, agentLog.logFd],
356
612
  encoding: FILE_SYSTEM.ENCODING,
357
- cwd: webDir,
613
+ cwd: process.cwd(),
358
614
  }
359
615
  : {
360
616
  stdio: 'inherit',
361
617
  encoding: FILE_SYSTEM.ENCODING,
362
- cwd: webDir,
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: packageRelativeFiles.length,
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(pnpmFilter(PACKAGES.WEB, SCRIPTS.LINT), { agentLog });
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
- if (currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER) {
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
- if (untrackedCodeFiles.length > 0) {
408
- const preview = untrackedCodeFiles.slice(0, 5).join(', ');
409
- logLine(`⚠️ Untracked code files detected (${untrackedCodeFiles.length}): ${preview}${untrackedCodeFiles.length > 5 ? '...' : ''}`);
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
- // WU-1006: Use turbo for tests (repo-agnostic)
415
- // Previously used --project tools and test:changed which don't exist in all repos
416
- logLine('\n> Running tests (turbo run test)\n');
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
- // Get merge base
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
- // Get context for telemetry
580
- const wu_id = getCurrentWU();
581
- const lane = getCurrentLane();
582
- const useAgentMode = shouldUseGatesAgentMode({ argv: process.argv.slice(2), env: process.env });
583
- const agentLog = useAgentMode ? createAgentLogContext({ wuId: wu_id, lane }) : null;
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 main() {
587
- const opts = parseGatesArgs();
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
- const coverageMode = opts.coverageMode || COVERAGE_GATE_MODES.BLOCK;
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, cmd: pnpmCmd(SCRIPTS.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, cmd: TOOL_PATHS.VALIDATE_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
- cmd: TOOL_PATHS.SYSTEM_MAP_VALIDATE,
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, cmd: pnpmCmd(SCRIPTS.FORMAT_CHECK) },
967
+ { name: GATE_NAMES.FORMAT_CHECK, run: runFormatCheckGate },
652
968
  {
653
969
  name: GATE_NAMES.LINT,
654
- cmd: isFullLint ? pnpmFilter(PACKAGES.WEB, SCRIPTS.LINT) : GATE_COMMANDS.INCREMENTAL,
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, cmd: TOOL_PATHS.VALIDATE_BACKLOG_SYNC },
663
- { name: GATE_NAMES.SUPABASE_DOCS_LINTER, cmd: TOOL_PATHS.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
- cmd: TOOL_PATHS.SYSTEM_MAP_VALIDATE,
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
- { name: GATE_NAMES.SAFETY_CRITICAL_TEST, cmd: GATE_COMMANDS.SAFETY_CRITICAL_TEST },
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
- ? [{ name: GATE_NAMES.INTEGRATION_TEST, cmd: GATE_COMMANDS.TIERED_TEST }]
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.cmd === GATE_COMMANDS.INVARIANTS) {
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
- process.exit(EXIT_CODES.SUCCESS);
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
- main().catch((error) => {
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
  });