@lumenflow/cli 2.15.2 → 2.17.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 (232) 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 +29 -26
  14. package/dist/commands/integrate.js.map +1 -1
  15. package/dist/commands.js +10 -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 +161 -234
  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 +70 -16
  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/strict-progress.js +253 -0
  162. package/dist/strict-progress.js.map +1 -0
  163. package/dist/sync-templates.js +1 -0
  164. package/dist/sync-templates.js.map +1 -1
  165. package/dist/trace-gen.js +74 -10
  166. package/dist/trace-gen.js.map +1 -1
  167. package/dist/validate-agent-skills.js +6 -5
  168. package/dist/validate-agent-skills.js.map +1 -1
  169. package/dist/validate-agent-sync.js +4 -5
  170. package/dist/validate-agent-sync.js.map +1 -1
  171. package/dist/validate-backlog-sync.js +5 -6
  172. package/dist/validate-backlog-sync.js.map +1 -1
  173. package/dist/validate-skills-spec.js +6 -5
  174. package/dist/validate-skills-spec.js.map +1 -1
  175. package/dist/validate.js +6 -7
  176. package/dist/validate.js.map +1 -1
  177. package/dist/validator-defaults.js +119 -0
  178. package/dist/validator-defaults.js.map +1 -0
  179. package/dist/validator-registry.js +81 -0
  180. package/dist/validator-registry.js.map +1 -0
  181. package/dist/wu-block.js +20 -19
  182. package/dist/wu-block.js.map +1 -1
  183. package/dist/wu-claim-mode.js +1 -1
  184. package/dist/wu-claim-mode.js.map +1 -1
  185. package/dist/wu-claim.js +33 -32
  186. package/dist/wu-claim.js.map +1 -1
  187. package/dist/wu-cleanup.js +11 -11
  188. package/dist/wu-cleanup.js.map +1 -1
  189. package/dist/wu-create.js +27 -27
  190. package/dist/wu-create.js.map +1 -1
  191. package/dist/wu-delete.js +16 -17
  192. package/dist/wu-delete.js.map +1 -1
  193. package/dist/wu-deps.js +7 -7
  194. package/dist/wu-deps.js.map +1 -1
  195. package/dist/wu-done-auto-cleanup.js +19 -20
  196. package/dist/wu-done-auto-cleanup.js.map +1 -1
  197. package/dist/wu-done-check.js +3 -2
  198. package/dist/wu-done-check.js.map +1 -1
  199. package/dist/wu-done-decay.js +3 -7
  200. package/dist/wu-done-decay.js.map +1 -1
  201. package/dist/wu-done.js +124 -63
  202. package/dist/wu-done.js.map +1 -1
  203. package/dist/wu-edit.js +22 -23
  204. package/dist/wu-edit.js.map +1 -1
  205. package/dist/wu-infer-lane.js +6 -6
  206. package/dist/wu-infer-lane.js.map +1 -1
  207. package/dist/wu-preflight.js +7 -7
  208. package/dist/wu-preflight.js.map +1 -1
  209. package/dist/wu-prep.js +10 -8
  210. package/dist/wu-prep.js.map +1 -1
  211. package/dist/wu-proto.js +14 -14
  212. package/dist/wu-proto.js.map +1 -1
  213. package/dist/wu-prune.js +7 -7
  214. package/dist/wu-prune.js.map +1 -1
  215. package/dist/wu-recover.js +13 -13
  216. package/dist/wu-recover.js.map +1 -1
  217. package/dist/wu-release.js +17 -16
  218. package/dist/wu-release.js.map +1 -1
  219. package/dist/wu-repair.js +4 -4
  220. package/dist/wu-repair.js.map +1 -1
  221. package/dist/wu-spawn.js +20 -20
  222. package/dist/wu-spawn.js.map +1 -1
  223. package/dist/wu-status.js +5 -5
  224. package/dist/wu-status.js.map +1 -1
  225. package/dist/wu-unblock.js +23 -22
  226. package/dist/wu-unblock.js.map +1 -1
  227. package/dist/wu-unlock-lane.js +5 -5
  228. package/dist/wu-unlock-lane.js.map +1 -1
  229. package/dist/wu-validate.js +8 -8
  230. package/dist/wu-validate.js.map +1 -1
  231. package/package.json +7 -6
  232. 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
  }
@@ -1072,41 +1080,30 @@ async function getAllChangedFiles(options = {}) {
1072
1080
  }
1073
1081
  }
1074
1082
  /**
1075
- * WU-1541: runGates still uses process.chdir as a contained, self-restoring pattern.
1076
- * The executeGates function and ~30 sub-functions use process.cwd() extensively.
1077
- * A full refactor to pass explicit cwd through all gate functions would be a separate WU.
1078
- * The try/finally pattern here is safe: chdir is always restored even on error.
1083
+ * Run gates for a specific working directory without mutating global process cwd.
1079
1084
  */
1080
1085
  export async function runGates(options = {}) {
1081
- const originalCwd = process.cwd();
1082
- const targetCwd = options.cwd ?? originalCwd;
1083
- if (targetCwd !== originalCwd) {
1084
- process.chdir(targetCwd);
1085
- }
1086
1086
  try {
1087
1087
  return await executeGates({
1088
1088
  ...options,
1089
+ cwd: options.cwd ?? process.cwd(),
1089
1090
  coverageMode: options.coverageMode ?? COVERAGE_GATE_MODES.BLOCK,
1090
1091
  });
1091
1092
  }
1092
1093
  catch {
1093
1094
  return false;
1094
1095
  }
1095
- finally {
1096
- if (targetCwd !== originalCwd) {
1097
- process.chdir(originalCwd);
1098
- }
1099
- }
1100
1096
  }
1101
1097
  // Main execution
1102
1098
  // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing: main() orchestrates multi-step gate workflow
1103
1099
  async function executeGates(opts) {
1100
+ const cwd = opts.cwd ?? process.cwd();
1104
1101
  const argv = opts.argv ?? process.argv.slice(2);
1105
1102
  // Get context for telemetry
1106
1103
  const wu_id = getCurrentWU();
1107
1104
  const lane = getCurrentLane();
1108
1105
  const useAgentMode = shouldUseGatesAgentMode({ argv, env: process.env });
1109
- const agentLog = useAgentMode ? createAgentLogContext({ wuId: wu_id, lane }) : null;
1106
+ const agentLog = useAgentMode ? createAgentLogContext({ wuId: wu_id, lane, cwd }) : null;
1110
1107
  // Parse command line arguments (now via Commander)
1111
1108
  const isDocsOnly = opts.docsOnly || false;
1112
1109
  const isFullLint = opts.fullLint || false;
@@ -1116,7 +1113,7 @@ async function executeGates(opts) {
1116
1113
  // WU-1262: Resolve coverage config from methodology policy
1117
1114
  // This derives coverage threshold and mode from methodology.testing setting
1118
1115
  // WU-1280: Use resolveTestPolicy to also get tests_required for test failure handling
1119
- const resolvedTestPolicy = resolveTestPolicy(process.cwd());
1116
+ const resolvedTestPolicy = resolveTestPolicy(cwd);
1120
1117
  // WU-1433: Coverage gate mode (warn or block)
1121
1118
  // WU-2334: Default changed from WARN to BLOCK for TDD enforcement
1122
1119
  // WU-1262: CLI flag overrides resolved policy, which overrides methodology defaults
@@ -1126,12 +1123,12 @@ async function executeGates(opts) {
1126
1123
  // When tests_required=false (methodology.testing: none), test failures produce warnings only
1127
1124
  const testsRequired = resolvedTestPolicy.tests_required;
1128
1125
  // WU-1191: Lane health gate mode (warn, error, or off)
1129
- const laneHealthMode = loadLaneHealthConfig(process.cwd());
1126
+ const laneHealthMode = loadLaneHealthConfig(cwd);
1130
1127
  // WU-1356: Resolve configured gates commands for test execution
1131
- const configuredGatesCommands = resolveGatesCommands(process.cwd());
1128
+ const configuredGatesCommands = resolveGatesCommands(cwd);
1132
1129
  // WU-1520: Strict mode and script existence checking for graceful degradation
1133
1130
  const isStrict = opts.strict || false;
1134
- const packageJsonScripts = loadPackageJsonScripts(process.cwd());
1131
+ const packageJsonScripts = loadPackageJsonScripts(cwd);
1135
1132
  // WU-1520: Track gate results for summary
1136
1133
  const gateResults = [];
1137
1134
  if (useAgentMode) {
@@ -1142,7 +1139,7 @@ async function executeGates(opts) {
1142
1139
  let changedFiles = [];
1143
1140
  if (!isDocsOnly) {
1144
1141
  try {
1145
- changedFiles = await getAllChangedFiles();
1142
+ changedFiles = await getAllChangedFiles({ cwd });
1146
1143
  riskTier = detectRiskTier({ changedFiles });
1147
1144
  const logLine = useAgentMode
1148
1145
  ? (line) => writeSync(agentLog.logFd, `${line}\n`)
@@ -1168,132 +1165,62 @@ async function executeGates(opts) {
1168
1165
  // WU-1299: Load code_paths and compute docs-only test plan
1169
1166
  let docsOnlyTestPlan = null;
1170
1167
  if (effectiveDocsOnly) {
1171
- const codePaths = loadCurrentWUCodePaths({ cwd: process.cwd() });
1168
+ const codePaths = loadCurrentWUCodePaths({ cwd });
1172
1169
  docsOnlyTestPlan = resolveDocsOnlyTestPlan({ codePaths });
1173
1170
  }
1174
- // 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.
1175
1173
  // WU-2252: Invariants gate runs FIRST and is included in both docs-only and regular modes
1176
1174
  // WU-1520: scriptName field maps gates to their package.json script for existence checking
1177
- const gates = effectiveDocsOnly
1178
- ? [
1179
- // WU-2252: Invariants check runs first (non-bypassable)
1180
- { name: GATE_NAMES.INVARIANTS, cmd: GATE_COMMANDS.INVARIANTS },
1181
- {
1182
- name: GATE_NAMES.FORMAT_CHECK,
1183
- run: runFormatCheckGate,
1184
- scriptName: SCRIPTS.FORMAT_CHECK,
1185
- },
1186
- { name: GATE_NAMES.SPEC_LINTER, run: runSpecLinterGate, scriptName: SCRIPTS.SPEC_LINTER },
1187
- // WU-1467: prompts:lint removed -- was a stub (exit 0), not an authoritative gate
1188
- { name: GATE_NAMES.BACKLOG_SYNC, run: runBacklogSyncGate },
1189
- // WU-2315: System map validation (warn-only until orphan docs are indexed)
1190
- {
1191
- name: GATE_NAMES.SYSTEM_MAP_VALIDATE,
1192
- run: runSystemMapGate,
1193
- warnOnly: true,
1194
- },
1195
- // WU-1191: Lane health check (configurable: warn/error/off)
1196
- {
1197
- name: GATE_NAMES.LANE_HEALTH,
1198
- run: (ctx) => runLaneHealthGate({ ...ctx, mode: laneHealthMode }),
1199
- warnOnly: laneHealthMode !== 'error',
1200
- },
1201
- // WU-1315: Onboarding smoke test (init + wu:create validation)
1202
- {
1203
- name: GATE_NAMES.ONBOARDING_SMOKE_TEST,
1204
- cmd: GATE_COMMANDS.ONBOARDING_SMOKE_TEST,
1205
- },
1206
- // WU-1299: Filtered tests for packages in code_paths (if any)
1207
- // When docs-only mode has packages in code_paths, run tests only for those packages
1208
- // This prevents pre-existing failures in unrelated packages from blocking
1209
- ...(docsOnlyTestPlan && docsOnlyTestPlan.mode === 'filtered'
1210
- ? [
1211
- {
1212
- name: GATE_NAMES.TEST,
1213
- run: (ctx) => {
1214
- // Safe access: docsOnlyTestPlan is guaranteed non-null by the outer conditional
1215
- const pkgs = docsOnlyTestPlan.packages;
1216
- return runDocsOnlyFilteredTests({
1217
- packages: pkgs,
1218
- agentLog: ctx.agentLog,
1219
- });
1220
- },
1221
- warnOnly: !testsRequired,
1222
- },
1223
- ]
1224
- : []),
1225
- ]
1226
- : [
1227
- // WU-2252: Invariants check runs first (non-bypassable)
1228
- { name: GATE_NAMES.INVARIANTS, cmd: GATE_COMMANDS.INVARIANTS },
1229
- {
1230
- name: GATE_NAMES.FORMAT_CHECK,
1231
- run: runFormatCheckGate,
1232
- scriptName: SCRIPTS.FORMAT_CHECK,
1233
- },
1234
- {
1235
- name: GATE_NAMES.LINT,
1236
- cmd: isFullLint ? pnpmCmd(SCRIPTS.LINT) : GATE_COMMANDS.INCREMENTAL,
1237
- scriptName: SCRIPTS.LINT,
1238
- },
1239
- {
1240
- name: GATE_NAMES.TYPECHECK,
1241
- cmd: pnpmCmd(SCRIPTS.TYPECHECK),
1242
- scriptName: SCRIPTS.TYPECHECK,
1243
- },
1244
- { name: GATE_NAMES.SPEC_LINTER, run: runSpecLinterGate, scriptName: SCRIPTS.SPEC_LINTER },
1245
- // WU-1467: prompts:lint removed -- was a stub (exit 0), not an authoritative gate
1246
- { name: GATE_NAMES.BACKLOG_SYNC, run: runBacklogSyncGate },
1247
- { name: GATE_NAMES.SUPABASE_DOCS_LINTER, run: runSupabaseDocsGate },
1248
- // WU-2315: System map validation (warn-only until orphan docs are indexed)
1249
- {
1250
- name: GATE_NAMES.SYSTEM_MAP_VALIDATE,
1251
- run: runSystemMapGate,
1252
- warnOnly: true,
1253
- },
1254
- // WU-1191: Lane health check (configurable: warn/error/off)
1255
- {
1256
- name: GATE_NAMES.LANE_HEALTH,
1257
- run: (ctx) => runLaneHealthGate({ ...ctx, mode: laneHealthMode }),
1258
- warnOnly: laneHealthMode !== 'error',
1259
- },
1260
- // WU-1315: Onboarding smoke test (init + wu:create validation)
1261
- {
1262
- name: GATE_NAMES.ONBOARDING_SMOKE_TEST,
1263
- cmd: GATE_COMMANDS.ONBOARDING_SMOKE_TEST,
1264
- },
1265
- // WU-2062: Safety-critical tests ALWAYS run
1266
- // WU-1280: When tests_required=false (methodology.testing: none), failures only warn
1267
- {
1268
- name: GATE_NAMES.SAFETY_CRITICAL_TEST,
1269
- cmd: GATE_COMMANDS.SAFETY_CRITICAL_TEST,
1270
- warnOnly: !testsRequired,
1271
- },
1272
- // WU-1920: Use changed tests by default, full suite with --full-tests
1273
- // WU-2244: --full-coverage implies --full-tests for accurate coverage
1274
- // WU-1280: When tests_required=false (methodology.testing: none), failures only warn
1275
- // WU-1356: Use configured test command instead of hard-coded turbo
1276
- {
1277
- name: GATE_NAMES.TEST,
1278
- cmd: isFullTests || isFullCoverage
1279
- ? configuredGatesCommands.test_full
1280
- : GATE_COMMANDS.INCREMENTAL_TEST,
1281
- warnOnly: !testsRequired,
1282
- },
1283
- // WU-2062: Integration tests only for high-risk changes
1284
- // WU-1280: When tests_required=false (methodology.testing: none), failures only warn
1285
- ...(riskTier && riskTier.shouldRunIntegration
1286
- ? [
1287
- {
1288
- name: GATE_NAMES.INTEGRATION_TEST,
1289
- cmd: GATE_COMMANDS.TIERED_TEST,
1290
- warnOnly: !testsRequired,
1291
- },
1292
- ]
1293
- : []),
1294
- // WU-1433: Coverage gate with configurable mode (warn/block)
1295
- { name: GATE_NAMES.COVERAGE, cmd: GATE_COMMANDS.COVERAGE_GATE },
1296
- ];
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
+ });
1297
1224
  if (effectiveDocsOnly) {
1298
1225
  // WU-1299: Show clear messaging about what's being skipped/run in docs-only mode
1299
1226
  const docsOnlyMessage = docsOnlyTestPlan && docsOnlyTestPlan.mode === 'filtered'
@@ -1316,7 +1243,7 @@ async function executeGates(opts) {
1316
1243
  const gateScriptName = gate.scriptName ?? null;
1317
1244
  const gateAction = resolveGateAction(gate.name, gateScriptName, packageJsonScripts, isStrict);
1318
1245
  if (gateAction === 'skip') {
1319
- const logLine = makeGateLogger({ agentLog, useAgentMode });
1246
+ const logLine = makeGateLogger({ agentLog, useAgentMode, cwd });
1320
1247
  const warningMsg = buildMissingScriptWarning(gateScriptName);
1321
1248
  logLine(`\n${warningMsg}\n`);
1322
1249
  gateResults.push({
@@ -1328,7 +1255,7 @@ async function executeGates(opts) {
1328
1255
  continue;
1329
1256
  }
1330
1257
  if (gateAction === 'fail') {
1331
- const logLine = makeGateLogger({ agentLog, useAgentMode });
1258
+ const logLine = makeGateLogger({ agentLog, useAgentMode, cwd });
1332
1259
  logLine(`\n❌ "${gateScriptName}" script not found in package.json (--strict mode)\n`);
1333
1260
  gateResults.push({
1334
1261
  name: gate.name,
@@ -1339,7 +1266,7 @@ async function executeGates(opts) {
1339
1266
  die(`${gate.name} failed: missing script "${gateScriptName}" in package.json (--strict mode requires all gate scripts)`);
1340
1267
  }
1341
1268
  if (gate.run) {
1342
- result = await gate.run({ agentLog, useAgentMode });
1269
+ result = await gate.run({ agentLog, useAgentMode, cwd });
1343
1270
  if (gate.name === GATE_NAMES.FORMAT_CHECK) {
1344
1271
  lastFormatCheckFiles = result.filesChecked ?? null;
1345
1272
  }
@@ -1350,7 +1277,7 @@ async function executeGates(opts) {
1350
1277
  ? (line) => writeSync(agentLog.logFd, `${line}\n`)
1351
1278
  : (line) => console.log(line);
1352
1279
  logLine('\n> Invariants check\n');
1353
- const invariantsResult = runInvariants({ baseDir: process.cwd(), silent: false });
1280
+ const invariantsResult = runInvariants({ baseDir: cwd, silent: false });
1354
1281
  result = {
1355
1282
  ok: invariantsResult.success,
1356
1283
  duration: 0, // runInvariants doesn't track duration
@@ -1362,20 +1289,20 @@ async function executeGates(opts) {
1362
1289
  }
1363
1290
  else if (gate.cmd === GATE_COMMANDS.INCREMENTAL) {
1364
1291
  // Special handling for incremental lint
1365
- result = await runIncrementalLint({ agentLog });
1292
+ result = await runIncrementalLint({ agentLog, cwd });
1366
1293
  }
1367
1294
  else if (gate.cmd === GATE_COMMANDS.SAFETY_CRITICAL_TEST) {
1368
1295
  // WU-2062: Safety-critical tests always run
1369
- result = await runSafetyCriticalTests({ agentLog });
1296
+ result = await runSafetyCriticalTests({ agentLog, cwd });
1370
1297
  }
1371
1298
  else if (gate.cmd === GATE_COMMANDS.INCREMENTAL_TEST) {
1372
1299
  // WU-1920: Special handling for changed tests
1373
- result = await runChangedTests({ agentLog });
1300
+ result = await runChangedTests({ agentLog, cwd });
1374
1301
  lastTestResult = result;
1375
1302
  }
1376
1303
  else if (gate.cmd === GATE_COMMANDS.TIERED_TEST) {
1377
1304
  // WU-2062: Integration tests for high-risk changes
1378
- result = await runIntegrationTests({ agentLog });
1305
+ result = await runIntegrationTests({ agentLog, cwd });
1379
1306
  }
1380
1307
  else if (gate.cmd === GATE_COMMANDS.COVERAGE_GATE) {
1381
1308
  // WU-1920: Skip coverage gate when tests were changed (partial coverage)
@@ -1429,7 +1356,7 @@ async function executeGates(opts) {
1429
1356
  });
1430
1357
  }
1431
1358
  else {
1432
- result = run(gate.cmd, { agentLog });
1359
+ result = run(gate.cmd, { agentLog, cwd });
1433
1360
  }
1434
1361
  // Emit telemetry event
1435
1362
  emitGateEvent({
@@ -1458,7 +1385,7 @@ async function executeGates(opts) {
1458
1385
  continue;
1459
1386
  }
1460
1387
  if (gate.name === GATE_NAMES.FORMAT_CHECK) {
1461
- emitFormatCheckGuidance({ agentLog, useAgentMode, files: lastFormatCheckFiles });
1388
+ emitFormatCheckGuidance({ agentLog, useAgentMode, files: lastFormatCheckFiles, cwd });
1462
1389
  }
1463
1390
  // WU-1520: Track failed gate before dying
1464
1391
  gateResults.push({
@@ -1467,7 +1394,7 @@ async function executeGates(opts) {
1467
1394
  durationMs: result.duration,
1468
1395
  });
1469
1396
  // WU-1520: Print summary before failing
1470
- const logLine = makeGateLogger({ agentLog, useAgentMode });
1397
+ const logLine = makeGateLogger({ agentLog, useAgentMode, cwd });
1471
1398
  logLine(`\n${formatGateSummary(gateResults)}\n`);
1472
1399
  if (useAgentMode) {
1473
1400
  const tail = readLogTail(agentLog.logPath);
@@ -1487,10 +1414,10 @@ async function executeGates(opts) {
1487
1414
  }
1488
1415
  // WU-2064: Create/update gates-latest.log symlink for easy agent access
1489
1416
  if (agentLog) {
1490
- updateGatesLatestSymlink({ logPath: agentLog.logPath, cwd: process.cwd(), env: process.env });
1417
+ updateGatesLatestSymlink({ logPath: agentLog.logPath, cwd, env: process.env });
1491
1418
  }
1492
1419
  // WU-1520: Print gate summary showing passed/skipped/failed/warned
1493
- const summaryLogLine = makeGateLogger({ agentLog, useAgentMode });
1420
+ const summaryLogLine = makeGateLogger({ agentLog, useAgentMode, cwd });
1494
1421
  summaryLogLine(`\n${formatGateSummary(gateResults)}`);
1495
1422
  if (!useAgentMode) {
1496
1423
  console.log('\n✅ All gates passed!\n');
@@ -1500,18 +1427,18 @@ async function executeGates(opts) {
1500
1427
  }
1501
1428
  return true;
1502
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
+ }
1503
1438
  // WU-1071: Use import.meta.main instead of process.argv[1] comparison
1504
1439
  // The old pattern fails with pnpm symlinks because process.argv[1] is the symlink
1505
1440
  // path but import.meta.url resolves to the real path - they never match
1506
1441
  if (import.meta.main) {
1507
- const opts = parseGatesArgs();
1508
- executeGates({ ...opts, argv: process.argv.slice(2) })
1509
- .then((ok) => {
1510
- process.exit(ok ? EXIT_CODES.SUCCESS : EXIT_CODES.ERROR);
1511
- })
1512
- .catch((error) => {
1513
- console.error('Gates failed:', error);
1514
- process.exit(EXIT_CODES.ERROR);
1515
- });
1442
+ void runCLI(main);
1516
1443
  }
1517
1444
  //# sourceMappingURL=gates.js.map