@lumenflow/cli 2.15.1 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (230) hide show
  1. package/dist/agent-issues-query.js +5 -5
  2. package/dist/agent-issues-query.js.map +1 -1
  3. package/dist/agent-log-issue.js +5 -4
  4. package/dist/agent-log-issue.js.map +1 -1
  5. package/dist/agent-session-end.js +3 -2
  6. package/dist/agent-session-end.js.map +1 -1
  7. package/dist/agent-session.js +3 -2
  8. package/dist/agent-session.js.map +1 -1
  9. package/dist/backlog-prune.js +5 -5
  10. package/dist/backlog-prune.js.map +1 -1
  11. package/dist/cli-entry-point.js +56 -3
  12. package/dist/cli-entry-point.js.map +1 -1
  13. package/dist/commands/integrate.js +4 -5
  14. package/dist/commands/integrate.js.map +1 -1
  15. package/dist/commands.js +1 -1
  16. package/dist/commands.js.map +1 -1
  17. package/dist/deps-add.js +65 -24
  18. package/dist/deps-add.js.map +1 -1
  19. package/dist/deps-remove.js +19 -11
  20. package/dist/deps-remove.js.map +1 -1
  21. package/dist/docs-sync.js +3 -2
  22. package/dist/docs-sync.js.map +1 -1
  23. package/dist/doctor.js +9 -4
  24. package/dist/doctor.js.map +1 -1
  25. package/dist/file-delete.js +1 -1
  26. package/dist/file-delete.js.map +1 -1
  27. package/dist/file-edit.js +1 -1
  28. package/dist/file-edit.js.map +1 -1
  29. package/dist/file-read.js +1 -1
  30. package/dist/file-read.js.map +1 -1
  31. package/dist/file-write.js +2 -2
  32. package/dist/file-write.js.map +1 -1
  33. package/dist/flow-bottlenecks.js +5 -6
  34. package/dist/flow-bottlenecks.js.map +1 -1
  35. package/dist/flow-report.js +4 -5
  36. package/dist/flow-report.js.map +1 -1
  37. package/dist/gate-defaults.js +149 -0
  38. package/dist/gate-defaults.js.map +1 -0
  39. package/dist/gate-registry.js +82 -0
  40. package/dist/gate-registry.js.map +1 -0
  41. package/dist/gates-types.js +10 -0
  42. package/dist/gates-types.js.map +1 -0
  43. package/dist/gates.js +163 -230
  44. package/dist/gates.js.map +1 -1
  45. package/dist/git-branch.js +1 -1
  46. package/dist/git-branch.js.map +1 -1
  47. package/dist/git-diff.js +1 -1
  48. package/dist/git-diff.js.map +1 -1
  49. package/dist/git-log.js +1 -1
  50. package/dist/git-log.js.map +1 -1
  51. package/dist/git-status.js +1 -1
  52. package/dist/git-status.js.map +1 -1
  53. package/dist/guard-locked.js +6 -7
  54. package/dist/guard-locked.js.map +1 -1
  55. package/dist/guard-main-branch.js +3 -2
  56. package/dist/guard-main-branch.js.map +1 -1
  57. package/dist/guard-worktree-commit.js +4 -5
  58. package/dist/guard-worktree-commit.js.map +1 -1
  59. package/dist/hooks/enforcement-generator.js +1 -1
  60. package/dist/hooks/enforcement-generator.js.map +1 -1
  61. package/dist/init.js +14 -6
  62. package/dist/init.js.map +1 -1
  63. package/dist/initiative-add-wu.js +12 -12
  64. package/dist/initiative-add-wu.js.map +1 -1
  65. package/dist/initiative-bulk-assign-wus.js +8 -9
  66. package/dist/initiative-bulk-assign-wus.js.map +1 -1
  67. package/dist/initiative-create.js +11 -11
  68. package/dist/initiative-create.js.map +1 -1
  69. package/dist/initiative-edit.js +9 -9
  70. package/dist/initiative-edit.js.map +1 -1
  71. package/dist/initiative-list.js +4 -4
  72. package/dist/initiative-list.js.map +1 -1
  73. package/dist/initiative-plan.js +12 -12
  74. package/dist/initiative-plan.js.map +1 -1
  75. package/dist/initiative-remove-wu.js +11 -11
  76. package/dist/initiative-remove-wu.js.map +1 -1
  77. package/dist/initiative-status.js +6 -6
  78. package/dist/initiative-status.js.map +1 -1
  79. package/dist/lane-health.js +2 -2
  80. package/dist/lane-health.js.map +1 -1
  81. package/dist/lane-suggest.js +2 -2
  82. package/dist/lane-suggest.js.map +1 -1
  83. package/dist/lifecycle-regression-harness.js +5 -5
  84. package/dist/lifecycle-regression-harness.js.map +1 -1
  85. package/dist/lumenflow-upgrade.js +4 -4
  86. package/dist/lumenflow-upgrade.js.map +1 -1
  87. package/dist/mem-checkpoint.js +8 -7
  88. package/dist/mem-checkpoint.js.map +1 -1
  89. package/dist/mem-cleanup.js +12 -14
  90. package/dist/mem-cleanup.js.map +1 -1
  91. package/dist/mem-context.js +8 -7
  92. package/dist/mem-context.js.map +1 -1
  93. package/dist/mem-create.js +9 -8
  94. package/dist/mem-create.js.map +1 -1
  95. package/dist/mem-delete.js +8 -7
  96. package/dist/mem-delete.js.map +1 -1
  97. package/dist/mem-export.js +9 -8
  98. package/dist/mem-export.js.map +1 -1
  99. package/dist/mem-inbox.js +40 -16
  100. package/dist/mem-inbox.js.map +1 -1
  101. package/dist/mem-index.js +8 -7
  102. package/dist/mem-index.js.map +1 -1
  103. package/dist/mem-init.js +8 -7
  104. package/dist/mem-init.js.map +1 -1
  105. package/dist/mem-profile.js +8 -7
  106. package/dist/mem-profile.js.map +1 -1
  107. package/dist/mem-promote.js +8 -7
  108. package/dist/mem-promote.js.map +1 -1
  109. package/dist/mem-ready.js +9 -8
  110. package/dist/mem-ready.js.map +1 -1
  111. package/dist/mem-recover.js +8 -7
  112. package/dist/mem-recover.js.map +1 -1
  113. package/dist/mem-signal.js +9 -8
  114. package/dist/mem-signal.js.map +1 -1
  115. package/dist/mem-start.js +8 -7
  116. package/dist/mem-start.js.map +1 -1
  117. package/dist/mem-summarize.js +8 -7
  118. package/dist/mem-summarize.js.map +1 -1
  119. package/dist/mem-triage.js +8 -7
  120. package/dist/mem-triage.js.map +1 -1
  121. package/dist/metrics-cli.js +5 -6
  122. package/dist/metrics-cli.js.map +1 -1
  123. package/dist/metrics-snapshot.js +6 -6
  124. package/dist/metrics-snapshot.js.map +1 -1
  125. package/dist/onboarding-smoke-test.js +3 -0
  126. package/dist/onboarding-smoke-test.js.map +1 -1
  127. package/dist/orchestrate-init-status.js +2 -2
  128. package/dist/orchestrate-init-status.js.map +1 -1
  129. package/dist/orchestrate-initiative.js +2 -2
  130. package/dist/orchestrate-initiative.js.map +1 -1
  131. package/dist/orchestrate-monitor.js +8 -1
  132. package/dist/orchestrate-monitor.js.map +1 -1
  133. package/dist/plan-create.js +8 -8
  134. package/dist/plan-create.js.map +1 -1
  135. package/dist/plan-edit.js +7 -7
  136. package/dist/plan-edit.js.map +1 -1
  137. package/dist/plan-link.js +9 -9
  138. package/dist/plan-link.js.map +1 -1
  139. package/dist/plan-promote.js +8 -8
  140. package/dist/plan-promote.js.map +1 -1
  141. package/dist/release.js +10 -9
  142. package/dist/release.js.map +1 -1
  143. package/dist/rotate-progress.js +4 -3
  144. package/dist/rotate-progress.js.map +1 -1
  145. package/dist/session-coordinator.js +2 -2
  146. package/dist/session-coordinator.js.map +1 -1
  147. package/dist/signal-cleanup.js +10 -13
  148. package/dist/signal-cleanup.js.map +1 -1
  149. package/dist/spawn-list.js +9 -10
  150. package/dist/spawn-list.js.map +1 -1
  151. package/dist/state-bootstrap.js +3 -3
  152. package/dist/state-bootstrap.js.map +1 -1
  153. package/dist/state-cleanup.js +13 -16
  154. package/dist/state-cleanup.js.map +1 -1
  155. package/dist/state-doctor-fix.js +14 -20
  156. package/dist/state-doctor-fix.js.map +1 -1
  157. package/dist/state-doctor-stamps.js +21 -0
  158. package/dist/state-doctor-stamps.js.map +1 -0
  159. package/dist/state-doctor.js +17 -26
  160. package/dist/state-doctor.js.map +1 -1
  161. package/dist/sync-templates.js +1 -0
  162. package/dist/sync-templates.js.map +1 -1
  163. package/dist/trace-gen.js +74 -10
  164. package/dist/trace-gen.js.map +1 -1
  165. package/dist/validate-agent-skills.js +6 -5
  166. package/dist/validate-agent-skills.js.map +1 -1
  167. package/dist/validate-agent-sync.js +4 -5
  168. package/dist/validate-agent-sync.js.map +1 -1
  169. package/dist/validate-backlog-sync.js +5 -6
  170. package/dist/validate-backlog-sync.js.map +1 -1
  171. package/dist/validate-skills-spec.js +6 -5
  172. package/dist/validate-skills-spec.js.map +1 -1
  173. package/dist/validate.js +6 -7
  174. package/dist/validate.js.map +1 -1
  175. package/dist/validator-defaults.js +119 -0
  176. package/dist/validator-defaults.js.map +1 -0
  177. package/dist/validator-registry.js +81 -0
  178. package/dist/validator-registry.js.map +1 -0
  179. package/dist/wu-block.js +20 -19
  180. package/dist/wu-block.js.map +1 -1
  181. package/dist/wu-claim-mode.js +1 -1
  182. package/dist/wu-claim-mode.js.map +1 -1
  183. package/dist/wu-claim.js +33 -32
  184. package/dist/wu-claim.js.map +1 -1
  185. package/dist/wu-cleanup.js +11 -11
  186. package/dist/wu-cleanup.js.map +1 -1
  187. package/dist/wu-create.js +27 -27
  188. package/dist/wu-create.js.map +1 -1
  189. package/dist/wu-delete.js +16 -17
  190. package/dist/wu-delete.js.map +1 -1
  191. package/dist/wu-deps.js +7 -7
  192. package/dist/wu-deps.js.map +1 -1
  193. package/dist/wu-done-auto-cleanup.js +46 -21
  194. package/dist/wu-done-auto-cleanup.js.map +1 -1
  195. package/dist/wu-done-check.js +3 -2
  196. package/dist/wu-done-check.js.map +1 -1
  197. package/dist/wu-done-decay.js +3 -7
  198. package/dist/wu-done-decay.js.map +1 -1
  199. package/dist/wu-done.js +132 -66
  200. package/dist/wu-done.js.map +1 -1
  201. package/dist/wu-edit.js +22 -23
  202. package/dist/wu-edit.js.map +1 -1
  203. package/dist/wu-infer-lane.js +6 -6
  204. package/dist/wu-infer-lane.js.map +1 -1
  205. package/dist/wu-preflight.js +7 -7
  206. package/dist/wu-preflight.js.map +1 -1
  207. package/dist/wu-prep.js +10 -8
  208. package/dist/wu-prep.js.map +1 -1
  209. package/dist/wu-proto.js +14 -14
  210. package/dist/wu-proto.js.map +1 -1
  211. package/dist/wu-prune.js +7 -7
  212. package/dist/wu-prune.js.map +1 -1
  213. package/dist/wu-recover.js +13 -13
  214. package/dist/wu-recover.js.map +1 -1
  215. package/dist/wu-release.js +17 -16
  216. package/dist/wu-release.js.map +1 -1
  217. package/dist/wu-repair.js +4 -4
  218. package/dist/wu-repair.js.map +1 -1
  219. package/dist/wu-spawn.js +20 -20
  220. package/dist/wu-spawn.js.map +1 -1
  221. package/dist/wu-status.js +5 -5
  222. package/dist/wu-status.js.map +1 -1
  223. package/dist/wu-unblock.js +23 -22
  224. package/dist/wu-unblock.js.map +1 -1
  225. package/dist/wu-unlock-lane.js +5 -5
  226. package/dist/wu-unlock-lane.js.map +1 -1
  227. package/dist/wu-validate.js +8 -8
  228. package/dist/wu-validate.js.map +1 -1
  229. package/package.json +7 -6
  230. package/templates/core/ai/onboarding/docs-generation.md.template +1 -1
package/dist/gates.js CHANGED
@@ -42,37 +42,41 @@ import { execSync, spawnSync } from 'node:child_process';
42
42
  import { closeSync, mkdirSync, openSync, readSync, statSync, writeSync } from 'node:fs';
43
43
  import { access } from 'node:fs/promises';
44
44
  import path from 'node:path';
45
- import { emitGateEvent, getCurrentWU, getCurrentLane } from '@lumenflow/core/dist/telemetry.js';
46
- import { die } from '@lumenflow/core/dist/error-handler.js';
45
+ import { emitGateEvent, getCurrentWU, getCurrentLane } from '@lumenflow/core/telemetry';
46
+ import { die } from '@lumenflow/core/error-handler';
47
47
  // WU-1299: Import WU YAML reader to get code_paths for docs-only filtering
48
- import { readWURaw } from '@lumenflow/core/dist/wu-yaml.js';
49
- import { createWuPaths } from '@lumenflow/core/dist/wu-paths.js';
50
- import { getChangedLintableFiles, isLintableFile } from '@lumenflow/core/dist/incremental-lint.js';
51
- import { buildVitestChangedArgs, isCodeFilePath } from '@lumenflow/core/dist/incremental-test.js';
52
- import { getGitForCwd } from '@lumenflow/core/dist/git-adapter.js';
53
- import { runCoverageGate, COVERAGE_GATE_MODES } from '@lumenflow/core/dist/coverage-gate.js';
54
- import { buildGatesLogPath, shouldUseGatesAgentMode, updateGatesLatestSymlink, } from '@lumenflow/core/dist/gates-agent-mode.js';
48
+ import { readWURaw } from '@lumenflow/core/wu-yaml';
49
+ import { createWuPaths } from '@lumenflow/core/wu-paths';
50
+ import { getChangedLintableFiles, isLintableFile } from '@lumenflow/core/incremental-lint';
51
+ import { buildVitestChangedArgs, isCodeFilePath } from '@lumenflow/core/incremental-test';
52
+ import { createGitForPath } from '@lumenflow/core/git-adapter';
53
+ import { runCoverageGate, COVERAGE_GATE_MODES } from '@lumenflow/core/coverage-gate';
54
+ import { buildGatesLogPath, shouldUseGatesAgentMode, updateGatesLatestSymlink, } from '@lumenflow/core/gates-agent-mode';
55
55
  // WU-2062: Import risk detector for tiered test execution
56
- import { detectRiskTier, RISK_TIERS } from '@lumenflow/core/dist/risk-detector.js';
56
+ import { detectRiskTier, RISK_TIERS } from '@lumenflow/core/risk-detector';
57
57
  // WU-2252: Import invariants runner for first-check validation
58
- import { runInvariants } from '@lumenflow/core/dist/invariants-runner.js';
59
- import { createWUParser } from '@lumenflow/core/dist/arg-parser.js';
60
- import { validateBacklogSync } from '@lumenflow/core/dist/validators/backlog-sync.js';
61
- import { runSupabaseDocsLinter } from '@lumenflow/core/dist/validators/supabase-docs-linter.js';
62
- import { runSystemMapValidation } from '@lumenflow/core/dist/system-map-validator.js';
58
+ import { runInvariants } from '@lumenflow/core/invariants-runner';
59
+ import { createWUParser } from '@lumenflow/core/arg-parser';
60
+ import { validateBacklogSync } from '@lumenflow/core/validators/backlog-sync';
61
+ import { runSupabaseDocsLinter } from '@lumenflow/core/validators/supabase-docs-linter';
62
+ import { runSystemMapValidation } from '@lumenflow/core/system-map-validator';
63
63
  // WU-1067: Config-driven gates support (partial implementation - unused imports removed)
64
64
  // WU-1191: Lane health gate configuration
65
65
  // WU-1262: Coverage config from methodology policy
66
66
  // WU-1280: Test policy for tests_required (warn vs block on test failures)
67
67
  // WU-1356: Configurable package manager and test commands
68
- import { loadLaneHealthConfig, resolveTestPolicy, resolveGatesCommands, resolveTestRunner, } from '@lumenflow/core/dist/gates-config.js';
68
+ import { loadLaneHealthConfig, resolveTestPolicy, resolveGatesCommands, resolveTestRunner, } from '@lumenflow/core/gates-config';
69
69
  // WU-1191: Lane health check
70
70
  import { runLaneHealthCheck } from './lane-health.js';
71
71
  // WU-1315: Onboarding smoke test
72
72
  import { runOnboardingSmokeTestGate } from './onboarding-smoke-test.js';
73
- 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';
73
+ import { BRANCHES, PACKAGES, PKG_MANAGER, ESLINT_FLAGS, ESLINT_COMMANDS, ESLINT_DEFAULTS, SCRIPTS, CACHE_STRATEGIES, DIRECTORIES, GATE_NAMES, GATE_COMMANDS, EXIT_CODES, FILE_SYSTEM, PRETTIER_ARGS, PRETTIER_FLAGS, } from '@lumenflow/core/wu-constants';
74
74
  // WU-1520: Gates graceful degradation for missing optional scripts
75
- import { checkScriptExists, buildMissingScriptWarning, loadPackageJsonScripts, resolveGateAction, formatGateSummary, SKIPPABLE_GATE_SCRIPTS, } from './gates-graceful-degradation.js';
75
+ import { buildMissingScriptWarning, loadPackageJsonScripts, resolveGateAction, formatGateSummary, } from './gates-graceful-degradation.js';
76
+ import { runCLI } from './cli-entry-point.js';
77
+ // WU-1550: Gate registry for declarative gate registration
78
+ import { GateRegistry } from './gate-registry.js';
79
+ import { registerDocsOnlyGates, registerCodeGates } from './gate-defaults.js';
76
80
  /**
77
81
  * WU-1087: Gates-specific option definitions for createWUParser.
78
82
  * Exported for testing and consistency with other CLI commands.
@@ -209,6 +213,7 @@ const TEST_CONFIG_PATTERNS = [
209
213
  /^vitest\.config\.(ts|mts|js|mjs|cjs)$/i,
210
214
  /^jest\.config\.(ts|js|mjs|cjs|json)$/i,
211
215
  /^\.mocharc\.(js|json|yaml|yml)$/i,
216
+ // eslint-disable-next-line security/detect-unsafe-regex -- static tsconfig pattern; no backtracking risk
212
217
  /^tsconfig(\..+)?\.json$/i,
213
218
  ];
214
219
  function normalizePath(filePath) {
@@ -410,19 +415,19 @@ export function loadCurrentWUCodePaths(options = {}) {
410
415
  * @param options - Options including packages to test and agent log context
411
416
  * @returns Result object with ok status and duration
412
417
  */
413
- async function runDocsOnlyFilteredTests({ packages, agentLog, }) {
418
+ async function runDocsOnlyFilteredTests({ packages, agentLog, cwd = process.cwd(), }) {
414
419
  const start = Date.now();
415
- const logLine = makeGateLogger({ agentLog, useAgentMode: !!agentLog });
420
+ const logLine = makeGateLogger({ agentLog, useAgentMode: !!agentLog, cwd });
416
421
  if (packages.length === 0) {
417
422
  logLine('📝 docs-only mode: no packages to test, skipping');
418
423
  return { ok: true, duration: Date.now() - start };
419
424
  }
420
425
  logLine(`\n> Tests (docs-only filtered: ${packages.join(', ')})\n`);
421
426
  // WU-1356: Use configured test command with filter
422
- const gatesCommands = resolveGatesCommands(process.cwd());
427
+ const gatesCommands = resolveGatesCommands(cwd);
423
428
  // If there's a configured test_docs_only command, use it
424
429
  if (gatesCommands.test_docs_only) {
425
- const result = run(gatesCommands.test_docs_only, { agentLog });
430
+ const result = run(gatesCommands.test_docs_only, { agentLog, cwd });
426
431
  return { ok: result.ok, duration: Date.now() - start };
427
432
  }
428
433
  // Otherwise, use the full test command with filter args
@@ -481,8 +486,8 @@ function collectPrettierListDifferent(cwd, files = []) {
481
486
  const output = `${result.stdout || ''}\n${result.stderr || ''}`;
482
487
  return parsePrettierListOutput(output);
483
488
  }
484
- function emitFormatCheckGuidance({ agentLog, useAgentMode, files, }) {
485
- const formattedFiles = collectPrettierListDifferent(process.cwd(), files ?? []);
489
+ function emitFormatCheckGuidance({ agentLog, useAgentMode, files, cwd, }) {
490
+ const formattedFiles = collectPrettierListDifferent(cwd, files ?? []);
486
491
  if (!formattedFiles.length)
487
492
  return;
488
493
  const lines = formatFormatCheckGuidance(formattedFiles);
@@ -514,8 +519,7 @@ function readLogTail(logPath, { maxLines = 40, maxBytes = 64 * 1024 } = {}) {
514
519
  return '';
515
520
  }
516
521
  }
517
- function createAgentLogContext({ wuId, lane }) {
518
- const cwd = process.cwd();
522
+ function createAgentLogContext({ wuId, lane, cwd, }) {
519
523
  const logPath = buildGatesLogPath({ cwd, env: process.env, wuId, lane });
520
524
  mkdirSync(path.dirname(logPath), { recursive: true });
521
525
  const logFd = openSync(logPath, 'a');
@@ -532,12 +536,16 @@ function createAgentLogContext({ wuId, lane }) {
532
536
  });
533
537
  return { logPath, logFd };
534
538
  }
535
- function run(cmd, { agentLog } = {}) {
539
+ function run(cmd, { agentLog, cwd = process.cwd(), } = {}) {
536
540
  const start = Date.now();
537
541
  if (!agentLog) {
538
542
  console.log(`\n> ${cmd}\n`);
539
543
  try {
540
- execSync(cmd, { stdio: 'inherit', encoding: FILE_SYSTEM.ENCODING });
544
+ execSync(cmd, {
545
+ stdio: 'inherit',
546
+ encoding: FILE_SYSTEM.ENCODING,
547
+ cwd,
548
+ });
541
549
  return { ok: true, duration: Date.now() - start };
542
550
  }
543
551
  catch {
@@ -548,17 +556,17 @@ function run(cmd, { agentLog } = {}) {
548
556
  const result = spawnSync(cmd, [], {
549
557
  shell: true,
550
558
  stdio: ['ignore', agentLog.logFd, agentLog.logFd],
551
- cwd: process.cwd(),
559
+ cwd,
552
560
  encoding: FILE_SYSTEM.ENCODING,
553
561
  });
554
562
  return { ok: result.status === EXIT_CODES.SUCCESS, duration: Date.now() - start };
555
563
  }
556
- async function runSpecLinterGate({ agentLog, useAgentMode }) {
564
+ async function runSpecLinterGate({ agentLog, useAgentMode, cwd }) {
557
565
  const start = Date.now();
558
566
  const wuId = getCurrentWU();
559
567
  if (wuId) {
560
568
  const scopedCmd = pnpmCmd('wu:validate', '--id', wuId);
561
- const scopedResult = run(scopedCmd, { agentLog });
569
+ const scopedResult = run(scopedCmd, { agentLog, cwd });
562
570
  if (!scopedResult.ok) {
563
571
  return { ok: false, duration: Date.now() - start };
564
572
  }
@@ -569,7 +577,7 @@ async function runSpecLinterGate({ agentLog, useAgentMode }) {
569
577
  else if (agentLog) {
570
578
  writeSync(agentLog.logFd, '⚠️ Unable to detect current WU; skipping scoped validation.\n');
571
579
  }
572
- const globalResult = run(pnpmRun(SCRIPTS.SPEC_LINTER), { agentLog });
580
+ const globalResult = run(pnpmRun(SCRIPTS.SPEC_LINTER), { agentLog, cwd });
573
581
  return { ok: globalResult.ok, duration: Date.now() - start };
574
582
  }
575
583
  function makeGateLogger({ agentLog, useAgentMode }) {
@@ -583,11 +591,11 @@ function makeGateLogger({ agentLog, useAgentMode }) {
583
591
  }
584
592
  };
585
593
  }
586
- async function runBacklogSyncGate({ agentLog, useAgentMode }) {
594
+ async function runBacklogSyncGate({ agentLog, useAgentMode, cwd }) {
587
595
  const start = Date.now();
588
596
  const logLine = makeGateLogger({ agentLog, useAgentMode });
589
597
  logLine('\n> Backlog sync\n');
590
- const result = await validateBacklogSync({ cwd: process.cwd() });
598
+ const result = await validateBacklogSync({ cwd });
591
599
  if (result.errors.length > 0) {
592
600
  logLine('❌ Backlog sync errors:');
593
601
  result.errors.forEach((error) => logLine(` - ${error}`));
@@ -599,11 +607,11 @@ async function runBacklogSyncGate({ agentLog, useAgentMode }) {
599
607
  logLine(`Backlog sync summary: WU files=${result.wuCount}, Backlog refs=${result.backlogCount}`);
600
608
  return { ok: result.valid, duration: Date.now() - start };
601
609
  }
602
- async function runSupabaseDocsGate({ agentLog, useAgentMode }) {
610
+ async function runSupabaseDocsGate({ agentLog, useAgentMode, cwd }) {
603
611
  const start = Date.now();
604
612
  const logLine = makeGateLogger({ agentLog, useAgentMode });
605
613
  logLine('\n> Supabase docs linter\n');
606
- const result = await runSupabaseDocsLinter({ cwd: process.cwd(), logger: { log: logLine } });
614
+ const result = await runSupabaseDocsLinter({ cwd, logger: { log: logLine } });
607
615
  if (result.skipped) {
608
616
  logLine(`⚠️ ${result.message ?? 'Supabase docs linter skipped.'}`);
609
617
  }
@@ -616,12 +624,12 @@ async function runSupabaseDocsGate({ agentLog, useAgentMode }) {
616
624
  }
617
625
  return { ok: result.ok, duration: Date.now() - start };
618
626
  }
619
- async function runSystemMapGate({ agentLog, useAgentMode }) {
627
+ async function runSystemMapGate({ agentLog, useAgentMode, cwd }) {
620
628
  const start = Date.now();
621
629
  const logLine = makeGateLogger({ agentLog, useAgentMode });
622
630
  logLine('\n> System map validation\n');
623
631
  const result = await runSystemMapValidation({
624
- cwd: process.cwd(),
632
+ cwd,
625
633
  logger: { log: logLine, warn: logLine, error: logLine },
626
634
  });
627
635
  if (!result.valid) {
@@ -646,7 +654,7 @@ async function runSystemMapGate({ agentLog, useAgentMode }) {
646
654
  * - 'error': Fail the gate if issues detected
647
655
  * - 'off': Skip the check entirely
648
656
  */
649
- async function runLaneHealthGate({ agentLog, useAgentMode, mode, }) {
657
+ async function runLaneHealthGate({ agentLog, useAgentMode, mode, cwd, }) {
650
658
  const start = Date.now();
651
659
  const logLine = makeGateLogger({ agentLog, useAgentMode });
652
660
  // Skip if mode is 'off'
@@ -655,7 +663,7 @@ async function runLaneHealthGate({ agentLog, useAgentMode, mode, }) {
655
663
  return { ok: true, duration: Date.now() - start };
656
664
  }
657
665
  logLine(`\n> Lane health check (mode: ${mode})\n`);
658
- const report = runLaneHealthCheck({ projectRoot: process.cwd() });
666
+ const report = runLaneHealthCheck({ projectRoot: cwd });
659
667
  if (!report.healthy) {
660
668
  logLine('⚠️ Lane health issues detected:');
661
669
  if (report.overlaps.hasOverlaps) {
@@ -688,24 +696,24 @@ async function filterExistingFiles(files) {
688
696
  }));
689
697
  return existingFiles.filter((file) => Boolean(file));
690
698
  }
691
- async function runFormatCheckGate({ agentLog, useAgentMode }) {
699
+ async function runFormatCheckGate({ agentLog, useAgentMode, cwd }) {
692
700
  const start = Date.now();
693
701
  const logLine = makeGateLogger({ agentLog, useAgentMode });
694
702
  let git;
695
703
  let isMainBranch = false;
696
704
  try {
697
- git = getGitForCwd();
705
+ git = createGitForPath(cwd);
698
706
  const currentBranch = await git.getCurrentBranch();
699
707
  isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
700
708
  }
701
709
  catch (error) {
702
710
  logLine(`⚠️ Failed to determine branch for format check: ${error.message}`);
703
- const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog });
711
+ const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog, cwd });
704
712
  return { ...result, duration: Date.now() - start, fileCount: -1 };
705
713
  }
706
714
  if (isMainBranch) {
707
715
  logLine('📋 On main branch - running full format check');
708
- const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog });
716
+ const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog, cwd });
709
717
  return { ...result, duration: Date.now() - start, fileCount: -1 };
710
718
  }
711
719
  let changedFiles = [];
@@ -730,7 +738,7 @@ async function runFormatCheckGate({ agentLog, useAgentMode }) {
730
738
  ? ' (file list unavailable)'
731
739
  : '';
732
740
  logLine(`📋 Running full format check${reason}`);
733
- const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog });
741
+ const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog, cwd });
734
742
  return { ...result, duration: Date.now() - start, fileCount: -1 };
735
743
  }
736
744
  const existingFiles = await filterExistingFiles(plan.files);
@@ -740,7 +748,7 @@ async function runFormatCheckGate({ agentLog, useAgentMode }) {
740
748
  return { ok: true, duration: Date.now() - start, fileCount: 0, filesChecked: [] };
741
749
  }
742
750
  logLine(`\n> format:check (incremental: ${existingFiles.length} files)\n`);
743
- const result = run(buildPrettierCheckCommand(existingFiles), { agentLog });
751
+ const result = run(buildPrettierCheckCommand(existingFiles), { agentLog, cwd });
744
752
  return {
745
753
  ...result,
746
754
  duration: Date.now() - start,
@@ -753,7 +761,7 @@ async function runFormatCheckGate({ agentLog, useAgentMode }) {
753
761
  * Falls back to full lint if on main branch or if incremental fails
754
762
  * @returns {{ ok: boolean, duration: number, fileCount: number }}
755
763
  */
756
- async function runIncrementalLint({ agentLog, } = {}) {
764
+ async function runIncrementalLint({ agentLog, cwd, }) {
757
765
  const start = Date.now();
758
766
  const logLine = (line) => {
759
767
  if (!agentLog) {
@@ -764,12 +772,12 @@ async function runIncrementalLint({ agentLog, } = {}) {
764
772
  };
765
773
  try {
766
774
  // Check if we're on main branch
767
- const git = getGitForCwd();
775
+ const git = createGitForPath(cwd);
768
776
  const currentBranch = await git.getCurrentBranch();
769
777
  const isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
770
778
  if (isMainBranch) {
771
779
  logLine('📋 On main branch - running full lint');
772
- const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog });
780
+ const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog, cwd });
773
781
  return { ...result, fileCount: -1 };
774
782
  }
775
783
  const changedFiles = await getChangedLintableFiles({ git });
@@ -781,7 +789,7 @@ async function runIncrementalLint({ agentLog, } = {}) {
781
789
  }
782
790
  if (plan.mode === 'full') {
783
791
  logLine('📋 Running full lint (incremental plan forced full)');
784
- const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog });
792
+ const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog, cwd });
785
793
  return { ...result, fileCount: -1 };
786
794
  }
787
795
  const existingFiles = await filterExistingFiles(plan.files);
@@ -808,12 +816,12 @@ async function runIncrementalLint({ agentLog, } = {}) {
808
816
  ? {
809
817
  stdio: ['ignore', agentLog.logFd, agentLog.logFd],
810
818
  encoding: FILE_SYSTEM.ENCODING,
811
- cwd: process.cwd(),
819
+ cwd,
812
820
  }
813
821
  : {
814
822
  stdio: 'inherit',
815
823
  encoding: FILE_SYSTEM.ENCODING,
816
- cwd: process.cwd(),
824
+ cwd,
817
825
  });
818
826
  const duration = Date.now() - start;
819
827
  return {
@@ -824,7 +832,7 @@ async function runIncrementalLint({ agentLog, } = {}) {
824
832
  }
825
833
  catch (error) {
826
834
  console.error('⚠️ Incremental lint failed, falling back to full lint:', error.message);
827
- const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog });
835
+ const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog, cwd });
828
836
  return { ...result, fileCount: -1 };
829
837
  }
830
838
  }
@@ -835,7 +843,7 @@ async function runIncrementalLint({ agentLog, } = {}) {
835
843
  *
836
844
  * @returns {{ ok: boolean, duration: number, isIncremental: boolean }}
837
845
  */
838
- async function runChangedTests({ agentLog, } = {}) {
846
+ async function runChangedTests({ agentLog, cwd, }) {
839
847
  const start = Date.now();
840
848
  // eslint-disable-next-line sonarjs/no-identical-functions -- Pre-existing: logLine helper duplicated across gate runners
841
849
  const logLine = (line) => {
@@ -846,15 +854,15 @@ async function runChangedTests({ agentLog, } = {}) {
846
854
  writeSync(agentLog.logFd, `${line}\n`);
847
855
  };
848
856
  // WU-1356: Get configured commands
849
- const gatesCommands = resolveGatesCommands(process.cwd());
850
- const testRunner = resolveTestRunner(process.cwd());
857
+ const gatesCommands = resolveGatesCommands(cwd);
858
+ const testRunner = resolveTestRunner(cwd);
851
859
  try {
852
- const git = getGitForCwd();
860
+ const git = createGitForPath(cwd);
853
861
  const currentBranch = await git.getCurrentBranch();
854
862
  const isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
855
863
  if (isMainBranch) {
856
864
  logLine('📋 On main branch - running full test suite');
857
- const result = run(gatesCommands.test_full, { agentLog });
865
+ const result = run(gatesCommands.test_full, { agentLog, cwd });
858
866
  return { ...result, isIncremental: false };
859
867
  }
860
868
  let changedFiles = [];
@@ -892,29 +900,29 @@ async function runChangedTests({ agentLog, } = {}) {
892
900
  logLine('⚠️ Changed file list unavailable - running full test suite');
893
901
  }
894
902
  logLine('📋 Running full test suite to avoid missing coverage');
895
- const result = run(gatesCommands.test_full, { agentLog });
903
+ const result = run(gatesCommands.test_full, { agentLog, cwd });
896
904
  return { ...result, duration: Date.now() - start, isIncremental: false };
897
905
  }
898
906
  // WU-1356: Use configured incremental test command
899
907
  logLine(`\n> Running tests (${testRunner} --changed)\n`);
900
908
  // If test_incremental is configured, use it directly
901
909
  if (gatesCommands.test_incremental) {
902
- const result = run(gatesCommands.test_incremental, { agentLog });
910
+ const result = run(gatesCommands.test_incremental, { agentLog, cwd });
903
911
  return { ...result, duration: Date.now() - start, isIncremental: true };
904
912
  }
905
913
  // Fallback: For vitest, use the built-in changed args helper
906
914
  if (testRunner === 'vitest') {
907
- const result = run(pnpmCmd('vitest', 'run', ...buildVitestChangedArgs({ baseBranch: 'origin/main' })), { agentLog });
915
+ const result = run(pnpmCmd('vitest', 'run', ...buildVitestChangedArgs({ baseBranch: 'origin/main' })), { agentLog, cwd });
908
916
  return { ...result, duration: Date.now() - start, isIncremental: true };
909
917
  }
910
918
  // For other runners without configured incremental, fall back to full
911
919
  logLine('⚠️ No incremental test command configured, running full suite');
912
- const result = run(gatesCommands.test_full, { agentLog });
920
+ const result = run(gatesCommands.test_full, { agentLog, cwd });
913
921
  return { ...result, duration: Date.now() - start, isIncremental: false };
914
922
  }
915
923
  catch (error) {
916
924
  console.error('⚠️ Changed tests failed, falling back to full suite:', error.message);
917
- const result = run(gatesCommands.test_full, { agentLog });
925
+ const result = run(gatesCommands.test_full, { agentLog, cwd });
918
926
  return { ...result, isIncremental: false };
919
927
  }
920
928
  }
@@ -953,7 +961,7 @@ const SAFETY_CRITICAL_TEST_FILES = [
953
961
  * @param {object} [options.agentLog] - Agent log context
954
962
  * @returns {Promise<{ ok: boolean, duration: number, testCount: number }>}
955
963
  */
956
- async function runSafetyCriticalTests({ agentLog, } = {}) {
964
+ async function runSafetyCriticalTests({ agentLog, cwd, }) {
957
965
  const start = Date.now();
958
966
  // eslint-disable-next-line sonarjs/no-identical-functions -- Pre-existing: logLine helper duplicated across gate runners
959
967
  const logLine = (line) => {
@@ -964,7 +972,7 @@ async function runSafetyCriticalTests({ agentLog, } = {}) {
964
972
  writeSync(agentLog.logFd, `${line}\n`);
965
973
  };
966
974
  // WU-1006: Skip safety-critical tests if apps/web doesn't exist (repo-agnostic)
967
- const webDir = path.join(process.cwd(), DIRECTORIES.APPS_WEB);
975
+ const webDir = path.join(cwd, DIRECTORIES.APPS_WEB);
968
976
  try {
969
977
  await access(webDir);
970
978
  }
@@ -989,12 +997,12 @@ async function runSafetyCriticalTests({ agentLog, } = {}) {
989
997
  ? {
990
998
  stdio: ['ignore', agentLog.logFd, agentLog.logFd],
991
999
  encoding: FILE_SYSTEM.ENCODING,
992
- cwd: process.cwd(),
1000
+ cwd,
993
1001
  }
994
1002
  : {
995
1003
  stdio: 'inherit',
996
1004
  encoding: FILE_SYSTEM.ENCODING,
997
- cwd: process.cwd(),
1005
+ cwd,
998
1006
  });
999
1007
  const duration = Date.now() - start;
1000
1008
  return {
@@ -1016,7 +1024,7 @@ async function runSafetyCriticalTests({ agentLog, } = {}) {
1016
1024
  * @param {object} [options.agentLog] - Agent log context
1017
1025
  * @returns {Promise<{ ok: boolean, duration: number }>}
1018
1026
  */
1019
- async function runIntegrationTests({ agentLog, } = {}) {
1027
+ async function runIntegrationTests({ agentLog, cwd, }) {
1020
1028
  const start = Date.now();
1021
1029
  // eslint-disable-next-line sonarjs/no-identical-functions -- Pre-existing: logLine helper duplicated across gate runners
1022
1030
  const logLine = (line) => {
@@ -1030,7 +1038,7 @@ async function runIntegrationTests({ agentLog, } = {}) {
1030
1038
  logLine('\n> Integration tests (high-risk changes detected)\n');
1031
1039
  // WU-1415: vitest doesn't support --include flag
1032
1040
  // Pass glob patterns as positional arguments instead
1033
- const result = run(`RUN_INTEGRATION_TESTS=1 ${pnpmCmd('vitest', 'run', "'**/*.integration.*'", "'**/golden-*.test.*'")}`, { agentLog });
1041
+ const result = run(`RUN_INTEGRATION_TESTS=1 ${pnpmCmd('vitest', 'run', "'**/*.integration.*'", "'**/golden-*.test.*'")}`, { agentLog, cwd });
1034
1042
  const duration = Date.now() - start;
1035
1043
  return {
1036
1044
  ok: result.ok,
@@ -1062,7 +1070,7 @@ async function getChangedFilesForIncremental({ git, baseBranch = 'origin/main',
1062
1070
  return [...new Set([...committedFiles, ...unstagedFiles, ...untrackedFiles])];
1063
1071
  }
1064
1072
  async function getAllChangedFiles(options = {}) {
1065
- const { git = getGitForCwd() } = options;
1073
+ const { git = createGitForPath(options.cwd ?? process.cwd()) } = options;
1066
1074
  try {
1067
1075
  return await getChangedFilesForIncremental({ git });
1068
1076
  }
@@ -1071,36 +1079,31 @@ async function getAllChangedFiles(options = {}) {
1071
1079
  return [];
1072
1080
  }
1073
1081
  }
1082
+ /**
1083
+ * Run gates for a specific working directory without mutating global process cwd.
1084
+ */
1074
1085
  export async function runGates(options = {}) {
1075
- const originalCwd = process.cwd();
1076
- const targetCwd = options.cwd ?? originalCwd;
1077
- if (targetCwd !== originalCwd) {
1078
- process.chdir(targetCwd);
1079
- }
1080
1086
  try {
1081
1087
  return await executeGates({
1082
1088
  ...options,
1089
+ cwd: options.cwd ?? process.cwd(),
1083
1090
  coverageMode: options.coverageMode ?? COVERAGE_GATE_MODES.BLOCK,
1084
1091
  });
1085
1092
  }
1086
1093
  catch {
1087
1094
  return false;
1088
1095
  }
1089
- finally {
1090
- if (targetCwd !== originalCwd) {
1091
- process.chdir(originalCwd);
1092
- }
1093
- }
1094
1096
  }
1095
1097
  // Main execution
1096
1098
  // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing: main() orchestrates multi-step gate workflow
1097
1099
  async function executeGates(opts) {
1100
+ const cwd = opts.cwd ?? process.cwd();
1098
1101
  const argv = opts.argv ?? process.argv.slice(2);
1099
1102
  // Get context for telemetry
1100
1103
  const wu_id = getCurrentWU();
1101
1104
  const lane = getCurrentLane();
1102
1105
  const useAgentMode = shouldUseGatesAgentMode({ argv, env: process.env });
1103
- const agentLog = useAgentMode ? createAgentLogContext({ wuId: wu_id, lane }) : null;
1106
+ const agentLog = useAgentMode ? createAgentLogContext({ wuId: wu_id, lane, cwd }) : null;
1104
1107
  // Parse command line arguments (now via Commander)
1105
1108
  const isDocsOnly = opts.docsOnly || false;
1106
1109
  const isFullLint = opts.fullLint || false;
@@ -1110,7 +1113,7 @@ async function executeGates(opts) {
1110
1113
  // WU-1262: Resolve coverage config from methodology policy
1111
1114
  // This derives coverage threshold and mode from methodology.testing setting
1112
1115
  // WU-1280: Use resolveTestPolicy to also get tests_required for test failure handling
1113
- const resolvedTestPolicy = resolveTestPolicy(process.cwd());
1116
+ const resolvedTestPolicy = resolveTestPolicy(cwd);
1114
1117
  // WU-1433: Coverage gate mode (warn or block)
1115
1118
  // WU-2334: Default changed from WARN to BLOCK for TDD enforcement
1116
1119
  // WU-1262: CLI flag overrides resolved policy, which overrides methodology defaults
@@ -1120,12 +1123,12 @@ async function executeGates(opts) {
1120
1123
  // When tests_required=false (methodology.testing: none), test failures produce warnings only
1121
1124
  const testsRequired = resolvedTestPolicy.tests_required;
1122
1125
  // WU-1191: Lane health gate mode (warn, error, or off)
1123
- const laneHealthMode = loadLaneHealthConfig(process.cwd());
1126
+ const laneHealthMode = loadLaneHealthConfig(cwd);
1124
1127
  // WU-1356: Resolve configured gates commands for test execution
1125
- const configuredGatesCommands = resolveGatesCommands(process.cwd());
1128
+ const configuredGatesCommands = resolveGatesCommands(cwd);
1126
1129
  // WU-1520: Strict mode and script existence checking for graceful degradation
1127
1130
  const isStrict = opts.strict || false;
1128
- const packageJsonScripts = loadPackageJsonScripts(process.cwd());
1131
+ const packageJsonScripts = loadPackageJsonScripts(cwd);
1129
1132
  // WU-1520: Track gate results for summary
1130
1133
  const gateResults = [];
1131
1134
  if (useAgentMode) {
@@ -1136,7 +1139,7 @@ async function executeGates(opts) {
1136
1139
  let changedFiles = [];
1137
1140
  if (!isDocsOnly) {
1138
1141
  try {
1139
- changedFiles = await getAllChangedFiles();
1142
+ changedFiles = await getAllChangedFiles({ cwd });
1140
1143
  riskTier = detectRiskTier({ changedFiles });
1141
1144
  const logLine = useAgentMode
1142
1145
  ? (line) => writeSync(agentLog.logFd, `${line}\n`)
@@ -1162,132 +1165,62 @@ async function executeGates(opts) {
1162
1165
  // WU-1299: Load code_paths and compute docs-only test plan
1163
1166
  let docsOnlyTestPlan = null;
1164
1167
  if (effectiveDocsOnly) {
1165
- const codePaths = loadCurrentWUCodePaths({ cwd: process.cwd() });
1168
+ const codePaths = loadCurrentWUCodePaths({ cwd });
1166
1169
  docsOnlyTestPlan = resolveDocsOnlyTestPlan({ codePaths });
1167
1170
  }
1168
- // Determine which gates to run
1171
+ // WU-1550: Build gate list via GateRegistry (declarative, Open-Closed Principle)
1172
+ // New gates can be added by calling registry.register() without modifying this function.
1169
1173
  // WU-2252: Invariants gate runs FIRST and is included in both docs-only and regular modes
1170
1174
  // WU-1520: scriptName field maps gates to their package.json script for existence checking
1171
- const gates = effectiveDocsOnly
1172
- ? [
1173
- // WU-2252: Invariants check runs first (non-bypassable)
1174
- { name: GATE_NAMES.INVARIANTS, cmd: GATE_COMMANDS.INVARIANTS },
1175
- {
1176
- name: GATE_NAMES.FORMAT_CHECK,
1177
- run: runFormatCheckGate,
1178
- scriptName: SCRIPTS.FORMAT_CHECK,
1179
- },
1180
- { name: GATE_NAMES.SPEC_LINTER, run: runSpecLinterGate, scriptName: SCRIPTS.SPEC_LINTER },
1181
- // WU-1467: prompts:lint removed -- was a stub (exit 0), not an authoritative gate
1182
- { name: GATE_NAMES.BACKLOG_SYNC, run: runBacklogSyncGate },
1183
- // WU-2315: System map validation (warn-only until orphan docs are indexed)
1184
- {
1185
- name: GATE_NAMES.SYSTEM_MAP_VALIDATE,
1186
- run: runSystemMapGate,
1187
- warnOnly: true,
1188
- },
1189
- // WU-1191: Lane health check (configurable: warn/error/off)
1190
- {
1191
- name: GATE_NAMES.LANE_HEALTH,
1192
- run: (ctx) => runLaneHealthGate({ ...ctx, mode: laneHealthMode }),
1193
- warnOnly: laneHealthMode !== 'error',
1194
- },
1195
- // WU-1315: Onboarding smoke test (init + wu:create validation)
1196
- {
1197
- name: GATE_NAMES.ONBOARDING_SMOKE_TEST,
1198
- cmd: GATE_COMMANDS.ONBOARDING_SMOKE_TEST,
1199
- },
1200
- // WU-1299: Filtered tests for packages in code_paths (if any)
1201
- // When docs-only mode has packages in code_paths, run tests only for those packages
1202
- // This prevents pre-existing failures in unrelated packages from blocking
1203
- ...(docsOnlyTestPlan && docsOnlyTestPlan.mode === 'filtered'
1204
- ? [
1205
- {
1206
- name: GATE_NAMES.TEST,
1207
- run: (ctx) => {
1208
- // Safe access: docsOnlyTestPlan is guaranteed non-null by the outer conditional
1209
- const pkgs = docsOnlyTestPlan.packages;
1210
- return runDocsOnlyFilteredTests({
1211
- packages: pkgs,
1212
- agentLog: ctx.agentLog,
1213
- });
1214
- },
1215
- warnOnly: !testsRequired,
1216
- },
1217
- ]
1218
- : []),
1219
- ]
1220
- : [
1221
- // WU-2252: Invariants check runs first (non-bypassable)
1222
- { name: GATE_NAMES.INVARIANTS, cmd: GATE_COMMANDS.INVARIANTS },
1223
- {
1224
- name: GATE_NAMES.FORMAT_CHECK,
1225
- run: runFormatCheckGate,
1226
- scriptName: SCRIPTS.FORMAT_CHECK,
1227
- },
1228
- {
1229
- name: GATE_NAMES.LINT,
1230
- cmd: isFullLint ? pnpmCmd(SCRIPTS.LINT) : GATE_COMMANDS.INCREMENTAL,
1231
- scriptName: SCRIPTS.LINT,
1232
- },
1233
- {
1234
- name: GATE_NAMES.TYPECHECK,
1235
- cmd: pnpmCmd(SCRIPTS.TYPECHECK),
1236
- scriptName: SCRIPTS.TYPECHECK,
1237
- },
1238
- { name: GATE_NAMES.SPEC_LINTER, run: runSpecLinterGate, scriptName: SCRIPTS.SPEC_LINTER },
1239
- // WU-1467: prompts:lint removed -- was a stub (exit 0), not an authoritative gate
1240
- { name: GATE_NAMES.BACKLOG_SYNC, run: runBacklogSyncGate },
1241
- { name: GATE_NAMES.SUPABASE_DOCS_LINTER, run: runSupabaseDocsGate },
1242
- // WU-2315: System map validation (warn-only until orphan docs are indexed)
1243
- {
1244
- name: GATE_NAMES.SYSTEM_MAP_VALIDATE,
1245
- run: runSystemMapGate,
1246
- warnOnly: true,
1247
- },
1248
- // WU-1191: Lane health check (configurable: warn/error/off)
1249
- {
1250
- name: GATE_NAMES.LANE_HEALTH,
1251
- run: (ctx) => runLaneHealthGate({ ...ctx, mode: laneHealthMode }),
1252
- warnOnly: laneHealthMode !== 'error',
1253
- },
1254
- // WU-1315: Onboarding smoke test (init + wu:create validation)
1255
- {
1256
- name: GATE_NAMES.ONBOARDING_SMOKE_TEST,
1257
- cmd: GATE_COMMANDS.ONBOARDING_SMOKE_TEST,
1258
- },
1259
- // WU-2062: Safety-critical tests ALWAYS run
1260
- // WU-1280: When tests_required=false (methodology.testing: none), failures only warn
1261
- {
1262
- name: GATE_NAMES.SAFETY_CRITICAL_TEST,
1263
- cmd: GATE_COMMANDS.SAFETY_CRITICAL_TEST,
1264
- warnOnly: !testsRequired,
1265
- },
1266
- // WU-1920: Use changed tests by default, full suite with --full-tests
1267
- // WU-2244: --full-coverage implies --full-tests for accurate coverage
1268
- // WU-1280: When tests_required=false (methodology.testing: none), failures only warn
1269
- // WU-1356: Use configured test command instead of hard-coded turbo
1270
- {
1271
- name: GATE_NAMES.TEST,
1272
- cmd: isFullTests || isFullCoverage
1273
- ? configuredGatesCommands.test_full
1274
- : GATE_COMMANDS.INCREMENTAL_TEST,
1275
- warnOnly: !testsRequired,
1276
- },
1277
- // WU-2062: Integration tests only for high-risk changes
1278
- // WU-1280: When tests_required=false (methodology.testing: none), failures only warn
1279
- ...(riskTier && riskTier.shouldRunIntegration
1280
- ? [
1281
- {
1282
- name: GATE_NAMES.INTEGRATION_TEST,
1283
- cmd: GATE_COMMANDS.TIERED_TEST,
1284
- warnOnly: !testsRequired,
1285
- },
1286
- ]
1287
- : []),
1288
- // WU-1433: Coverage gate with configurable mode (warn/block)
1289
- { name: GATE_NAMES.COVERAGE, cmd: GATE_COMMANDS.COVERAGE_GATE },
1290
- ];
1175
+ const gateRegistry = new GateRegistry();
1176
+ if (effectiveDocsOnly) {
1177
+ registerDocsOnlyGates(gateRegistry, {
1178
+ laneHealthMode,
1179
+ testsRequired,
1180
+ docsOnlyTestPlan,
1181
+ });
1182
+ }
1183
+ else {
1184
+ registerCodeGates(gateRegistry, {
1185
+ isFullLint,
1186
+ isFullTests,
1187
+ isFullCoverage,
1188
+ laneHealthMode,
1189
+ testsRequired,
1190
+ shouldRunIntegration: !!(riskTier && riskTier.shouldRunIntegration),
1191
+ configuredTestFullCmd: configuredGatesCommands.test_full,
1192
+ });
1193
+ }
1194
+ // WU-1550: Inject run functions for gates that need them.
1195
+ // The registry stores declarative metadata; run functions are bound here
1196
+ // because they depend on local gate-runner functions in this module.
1197
+ const gateRunFunctions = {
1198
+ [GATE_NAMES.FORMAT_CHECK]: runFormatCheckGate,
1199
+ [GATE_NAMES.SPEC_LINTER]: runSpecLinterGate,
1200
+ [GATE_NAMES.BACKLOG_SYNC]: runBacklogSyncGate,
1201
+ [GATE_NAMES.SUPABASE_DOCS_LINTER]: runSupabaseDocsGate,
1202
+ [GATE_NAMES.SYSTEM_MAP_VALIDATE]: runSystemMapGate,
1203
+ [GATE_NAMES.LANE_HEALTH]: (ctx) => runLaneHealthGate({ ...ctx, mode: laneHealthMode }),
1204
+ };
1205
+ // WU-1299: Docs-only filtered tests get a custom run function
1206
+ if (docsOnlyTestPlan && docsOnlyTestPlan.mode === 'filtered') {
1207
+ gateRunFunctions[GATE_NAMES.TEST] = (ctx) => {
1208
+ const pkgs = docsOnlyTestPlan.packages;
1209
+ return runDocsOnlyFilteredTests({
1210
+ packages: pkgs,
1211
+ agentLog: ctx.agentLog,
1212
+ cwd: ctx.cwd,
1213
+ });
1214
+ };
1215
+ }
1216
+ // Apply run functions to registered gates
1217
+ const gates = gateRegistry.getAll().map((gate) => {
1218
+ const runFn = gateRunFunctions[gate.name];
1219
+ if (runFn && !gate.run) {
1220
+ return { ...gate, run: runFn };
1221
+ }
1222
+ return gate;
1223
+ });
1291
1224
  if (effectiveDocsOnly) {
1292
1225
  // WU-1299: Show clear messaging about what's being skipped/run in docs-only mode
1293
1226
  const docsOnlyMessage = docsOnlyTestPlan && docsOnlyTestPlan.mode === 'filtered'
@@ -1310,7 +1243,7 @@ async function executeGates(opts) {
1310
1243
  const gateScriptName = gate.scriptName ?? null;
1311
1244
  const gateAction = resolveGateAction(gate.name, gateScriptName, packageJsonScripts, isStrict);
1312
1245
  if (gateAction === 'skip') {
1313
- const logLine = makeGateLogger({ agentLog, useAgentMode });
1246
+ const logLine = makeGateLogger({ agentLog, useAgentMode, cwd });
1314
1247
  const warningMsg = buildMissingScriptWarning(gateScriptName);
1315
1248
  logLine(`\n${warningMsg}\n`);
1316
1249
  gateResults.push({
@@ -1322,7 +1255,7 @@ async function executeGates(opts) {
1322
1255
  continue;
1323
1256
  }
1324
1257
  if (gateAction === 'fail') {
1325
- const logLine = makeGateLogger({ agentLog, useAgentMode });
1258
+ const logLine = makeGateLogger({ agentLog, useAgentMode, cwd });
1326
1259
  logLine(`\n❌ "${gateScriptName}" script not found in package.json (--strict mode)\n`);
1327
1260
  gateResults.push({
1328
1261
  name: gate.name,
@@ -1333,7 +1266,7 @@ async function executeGates(opts) {
1333
1266
  die(`${gate.name} failed: missing script "${gateScriptName}" in package.json (--strict mode requires all gate scripts)`);
1334
1267
  }
1335
1268
  if (gate.run) {
1336
- result = await gate.run({ agentLog, useAgentMode });
1269
+ result = await gate.run({ agentLog, useAgentMode, cwd });
1337
1270
  if (gate.name === GATE_NAMES.FORMAT_CHECK) {
1338
1271
  lastFormatCheckFiles = result.filesChecked ?? null;
1339
1272
  }
@@ -1344,7 +1277,7 @@ async function executeGates(opts) {
1344
1277
  ? (line) => writeSync(agentLog.logFd, `${line}\n`)
1345
1278
  : (line) => console.log(line);
1346
1279
  logLine('\n> Invariants check\n');
1347
- const invariantsResult = runInvariants({ baseDir: process.cwd(), silent: false });
1280
+ const invariantsResult = runInvariants({ baseDir: cwd, silent: false });
1348
1281
  result = {
1349
1282
  ok: invariantsResult.success,
1350
1283
  duration: 0, // runInvariants doesn't track duration
@@ -1356,20 +1289,20 @@ async function executeGates(opts) {
1356
1289
  }
1357
1290
  else if (gate.cmd === GATE_COMMANDS.INCREMENTAL) {
1358
1291
  // Special handling for incremental lint
1359
- result = await runIncrementalLint({ agentLog });
1292
+ result = await runIncrementalLint({ agentLog, cwd });
1360
1293
  }
1361
1294
  else if (gate.cmd === GATE_COMMANDS.SAFETY_CRITICAL_TEST) {
1362
1295
  // WU-2062: Safety-critical tests always run
1363
- result = await runSafetyCriticalTests({ agentLog });
1296
+ result = await runSafetyCriticalTests({ agentLog, cwd });
1364
1297
  }
1365
1298
  else if (gate.cmd === GATE_COMMANDS.INCREMENTAL_TEST) {
1366
1299
  // WU-1920: Special handling for changed tests
1367
- result = await runChangedTests({ agentLog });
1300
+ result = await runChangedTests({ agentLog, cwd });
1368
1301
  lastTestResult = result;
1369
1302
  }
1370
1303
  else if (gate.cmd === GATE_COMMANDS.TIERED_TEST) {
1371
1304
  // WU-2062: Integration tests for high-risk changes
1372
- result = await runIntegrationTests({ agentLog });
1305
+ result = await runIntegrationTests({ agentLog, cwd });
1373
1306
  }
1374
1307
  else if (gate.cmd === GATE_COMMANDS.COVERAGE_GATE) {
1375
1308
  // WU-1920: Skip coverage gate when tests were changed (partial coverage)
@@ -1423,7 +1356,7 @@ async function executeGates(opts) {
1423
1356
  });
1424
1357
  }
1425
1358
  else {
1426
- result = run(gate.cmd, { agentLog });
1359
+ result = run(gate.cmd, { agentLog, cwd });
1427
1360
  }
1428
1361
  // Emit telemetry event
1429
1362
  emitGateEvent({
@@ -1452,7 +1385,7 @@ async function executeGates(opts) {
1452
1385
  continue;
1453
1386
  }
1454
1387
  if (gate.name === GATE_NAMES.FORMAT_CHECK) {
1455
- emitFormatCheckGuidance({ agentLog, useAgentMode, files: lastFormatCheckFiles });
1388
+ emitFormatCheckGuidance({ agentLog, useAgentMode, files: lastFormatCheckFiles, cwd });
1456
1389
  }
1457
1390
  // WU-1520: Track failed gate before dying
1458
1391
  gateResults.push({
@@ -1461,7 +1394,7 @@ async function executeGates(opts) {
1461
1394
  durationMs: result.duration,
1462
1395
  });
1463
1396
  // WU-1520: Print summary before failing
1464
- const logLine = makeGateLogger({ agentLog, useAgentMode });
1397
+ const logLine = makeGateLogger({ agentLog, useAgentMode, cwd });
1465
1398
  logLine(`\n${formatGateSummary(gateResults)}\n`);
1466
1399
  if (useAgentMode) {
1467
1400
  const tail = readLogTail(agentLog.logPath);
@@ -1481,10 +1414,10 @@ async function executeGates(opts) {
1481
1414
  }
1482
1415
  // WU-2064: Create/update gates-latest.log symlink for easy agent access
1483
1416
  if (agentLog) {
1484
- updateGatesLatestSymlink({ logPath: agentLog.logPath, cwd: process.cwd(), env: process.env });
1417
+ updateGatesLatestSymlink({ logPath: agentLog.logPath, cwd, env: process.env });
1485
1418
  }
1486
1419
  // WU-1520: Print gate summary showing passed/skipped/failed/warned
1487
- const summaryLogLine = makeGateLogger({ agentLog, useAgentMode });
1420
+ const summaryLogLine = makeGateLogger({ agentLog, useAgentMode, cwd });
1488
1421
  summaryLogLine(`\n${formatGateSummary(gateResults)}`);
1489
1422
  if (!useAgentMode) {
1490
1423
  console.log('\n✅ All gates passed!\n');
@@ -1494,18 +1427,18 @@ async function executeGates(opts) {
1494
1427
  }
1495
1428
  return true;
1496
1429
  }
1430
+ // WU-1537: Wrap executeGates in a standard main() for runCLI consistency
1431
+ async function main() {
1432
+ const opts = parseGatesArgs();
1433
+ const ok = await executeGates({ ...opts, argv: process.argv.slice(2) });
1434
+ if (!ok) {
1435
+ process.exit(EXIT_CODES.ERROR);
1436
+ }
1437
+ }
1497
1438
  // WU-1071: Use import.meta.main instead of process.argv[1] comparison
1498
1439
  // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
1499
1440
  // path but import.meta.url resolves to the real path - they never match
1500
1441
  if (import.meta.main) {
1501
- const opts = parseGatesArgs();
1502
- executeGates({ ...opts, argv: process.argv.slice(2) })
1503
- .then((ok) => {
1504
- process.exit(ok ? EXIT_CODES.SUCCESS : EXIT_CODES.ERROR);
1505
- })
1506
- .catch((error) => {
1507
- console.error('Gates failed:', error);
1508
- process.exit(EXIT_CODES.ERROR);
1509
- });
1442
+ void runCLI(main);
1510
1443
  }
1511
1444
  //# sourceMappingURL=gates.js.map