@nathapp/nax 0.50.3 → 0.51.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (353) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +177 -104
  3. package/dist/nax.js +417 -213
  4. package/package.json +1 -3
  5. package/bin/nax.ts +0 -1195
  6. package/src/acceptance/fix-generator.ts +0 -322
  7. package/src/acceptance/generator.ts +0 -415
  8. package/src/acceptance/index.ts +0 -42
  9. package/src/acceptance/refinement.ts +0 -224
  10. package/src/acceptance/templates/cli.ts +0 -47
  11. package/src/acceptance/templates/component.ts +0 -78
  12. package/src/acceptance/templates/e2e.ts +0 -43
  13. package/src/acceptance/templates/index.ts +0 -21
  14. package/src/acceptance/templates/snapshot.ts +0 -50
  15. package/src/acceptance/templates/unit.ts +0 -48
  16. package/src/acceptance/types.ts +0 -138
  17. package/src/agents/acp/adapter.ts +0 -888
  18. package/src/agents/acp/cost.ts +0 -9
  19. package/src/agents/acp/index.ts +0 -7
  20. package/src/agents/acp/interaction-bridge.ts +0 -126
  21. package/src/agents/acp/parser.ts +0 -119
  22. package/src/agents/acp/spawn-client.ts +0 -373
  23. package/src/agents/acp/types.ts +0 -22
  24. package/src/agents/aider/adapter.ts +0 -135
  25. package/src/agents/claude/adapter.ts +0 -258
  26. package/src/agents/claude/complete.ts +0 -80
  27. package/src/agents/claude/cost.ts +0 -16
  28. package/src/agents/claude/execution.ts +0 -215
  29. package/src/agents/claude/index.ts +0 -3
  30. package/src/agents/claude/interactive.ts +0 -77
  31. package/src/agents/claude/plan.ts +0 -179
  32. package/src/agents/codex/adapter.ts +0 -153
  33. package/src/agents/cost/calculate.ts +0 -154
  34. package/src/agents/cost/index.ts +0 -10
  35. package/src/agents/cost/parse.ts +0 -97
  36. package/src/agents/cost/pricing.ts +0 -59
  37. package/src/agents/cost/types.ts +0 -45
  38. package/src/agents/gemini/adapter.ts +0 -177
  39. package/src/agents/index.ts +0 -18
  40. package/src/agents/opencode/adapter.ts +0 -106
  41. package/src/agents/registry.ts +0 -136
  42. package/src/agents/shared/decompose.ts +0 -154
  43. package/src/agents/shared/model-resolution.ts +0 -43
  44. package/src/agents/shared/types-extended.ts +0 -164
  45. package/src/agents/shared/validation.ts +0 -69
  46. package/src/agents/shared/version-detection.ts +0 -109
  47. package/src/agents/types.ts +0 -205
  48. package/src/analyze/classifier.ts +0 -282
  49. package/src/analyze/index.ts +0 -16
  50. package/src/analyze/scanner.ts +0 -171
  51. package/src/analyze/types.ts +0 -51
  52. package/src/cli/accept.ts +0 -108
  53. package/src/cli/agents.ts +0 -87
  54. package/src/cli/analyze-parser.ts +0 -291
  55. package/src/cli/analyze.ts +0 -352
  56. package/src/cli/config-descriptions.ts +0 -219
  57. package/src/cli/config-diff.ts +0 -103
  58. package/src/cli/config-display.ts +0 -285
  59. package/src/cli/config-get.ts +0 -55
  60. package/src/cli/config.ts +0 -14
  61. package/src/cli/constitution.ts +0 -17
  62. package/src/cli/diagnose-analysis.ts +0 -159
  63. package/src/cli/diagnose-formatter.ts +0 -87
  64. package/src/cli/diagnose.ts +0 -203
  65. package/src/cli/generate.ts +0 -250
  66. package/src/cli/index.ts +0 -42
  67. package/src/cli/init-context.ts +0 -405
  68. package/src/cli/init-detect.ts +0 -303
  69. package/src/cli/init.ts +0 -296
  70. package/src/cli/interact.ts +0 -295
  71. package/src/cli/plan.ts +0 -509
  72. package/src/cli/plugins.ts +0 -122
  73. package/src/cli/prompts-export.ts +0 -58
  74. package/src/cli/prompts-init.ts +0 -200
  75. package/src/cli/prompts-main.ts +0 -183
  76. package/src/cli/prompts-shared.ts +0 -70
  77. package/src/cli/prompts-tdd.ts +0 -88
  78. package/src/cli/prompts.ts +0 -17
  79. package/src/cli/runs.ts +0 -174
  80. package/src/cli/status-cost.ts +0 -151
  81. package/src/cli/status-features.ts +0 -405
  82. package/src/cli/status.ts +0 -13
  83. package/src/commands/common.ts +0 -171
  84. package/src/commands/diagnose.ts +0 -17
  85. package/src/commands/index.ts +0 -9
  86. package/src/commands/logs-formatter.ts +0 -201
  87. package/src/commands/logs-reader.ts +0 -171
  88. package/src/commands/logs.ts +0 -103
  89. package/src/commands/precheck.ts +0 -86
  90. package/src/commands/runs.ts +0 -220
  91. package/src/commands/unlock.ts +0 -96
  92. package/src/config/defaults.ts +0 -218
  93. package/src/config/index.ts +0 -22
  94. package/src/config/loader.ts +0 -143
  95. package/src/config/merge.ts +0 -106
  96. package/src/config/merger.ts +0 -147
  97. package/src/config/path-security.ts +0 -121
  98. package/src/config/paths.ts +0 -27
  99. package/src/config/permissions.ts +0 -63
  100. package/src/config/runtime-types.ts +0 -522
  101. package/src/config/schema-types.ts +0 -53
  102. package/src/config/schema.ts +0 -60
  103. package/src/config/schemas.ts +0 -426
  104. package/src/config/test-strategy.ts +0 -71
  105. package/src/config/types.ts +0 -57
  106. package/src/config/validate.ts +0 -103
  107. package/src/constitution/generator.ts +0 -158
  108. package/src/constitution/generators/aider.ts +0 -41
  109. package/src/constitution/generators/claude.ts +0 -35
  110. package/src/constitution/generators/cursor.ts +0 -36
  111. package/src/constitution/generators/opencode.ts +0 -38
  112. package/src/constitution/generators/types.ts +0 -33
  113. package/src/constitution/generators/windsurf.ts +0 -36
  114. package/src/constitution/index.ts +0 -11
  115. package/src/constitution/loader.ts +0 -121
  116. package/src/constitution/types.ts +0 -31
  117. package/src/context/auto-detect.ts +0 -228
  118. package/src/context/builder.ts +0 -299
  119. package/src/context/elements.ts +0 -122
  120. package/src/context/formatter.ts +0 -107
  121. package/src/context/generator.ts +0 -343
  122. package/src/context/generators/aider.ts +0 -34
  123. package/src/context/generators/claude.ts +0 -28
  124. package/src/context/generators/codex.ts +0 -28
  125. package/src/context/generators/cursor.ts +0 -28
  126. package/src/context/generators/gemini.ts +0 -28
  127. package/src/context/generators/opencode.ts +0 -30
  128. package/src/context/generators/windsurf.ts +0 -28
  129. package/src/context/greenfield.ts +0 -114
  130. package/src/context/index.ts +0 -34
  131. package/src/context/injector.ts +0 -279
  132. package/src/context/parent-context.ts +0 -39
  133. package/src/context/test-scanner.ts +0 -370
  134. package/src/context/types.ts +0 -98
  135. package/src/decompose/apply.ts +0 -50
  136. package/src/decompose/builder.ts +0 -181
  137. package/src/decompose/index.ts +0 -8
  138. package/src/decompose/sections/codebase.ts +0 -26
  139. package/src/decompose/sections/constraints.ts +0 -32
  140. package/src/decompose/sections/index.ts +0 -4
  141. package/src/decompose/sections/sibling-stories.ts +0 -25
  142. package/src/decompose/sections/target-story.ts +0 -31
  143. package/src/decompose/types.ts +0 -55
  144. package/src/decompose/validators/complexity.ts +0 -45
  145. package/src/decompose/validators/coverage.ts +0 -134
  146. package/src/decompose/validators/dependency.ts +0 -91
  147. package/src/decompose/validators/index.ts +0 -35
  148. package/src/decompose/validators/overlap.ts +0 -128
  149. package/src/errors.ts +0 -67
  150. package/src/execution/batching.ts +0 -157
  151. package/src/execution/crash-heartbeat.ts +0 -77
  152. package/src/execution/crash-recovery.ts +0 -79
  153. package/src/execution/crash-signals.ts +0 -165
  154. package/src/execution/crash-writer.ts +0 -154
  155. package/src/execution/deferred-review.ts +0 -105
  156. package/src/execution/dry-run.ts +0 -81
  157. package/src/execution/escalation/escalation.ts +0 -46
  158. package/src/execution/escalation/index.ts +0 -13
  159. package/src/execution/escalation/tier-escalation.ts +0 -346
  160. package/src/execution/escalation/tier-outcome.ts +0 -143
  161. package/src/execution/executor-types.ts +0 -73
  162. package/src/execution/helpers.ts +0 -38
  163. package/src/execution/index.ts +0 -27
  164. package/src/execution/iteration-runner.ts +0 -160
  165. package/src/execution/lifecycle/acceptance-loop.ts +0 -309
  166. package/src/execution/lifecycle/headless-formatter.ts +0 -83
  167. package/src/execution/lifecycle/index.ts +0 -11
  168. package/src/execution/lifecycle/parallel-lifecycle.ts +0 -101
  169. package/src/execution/lifecycle/precheck-runner.ts +0 -140
  170. package/src/execution/lifecycle/run-cleanup.ts +0 -81
  171. package/src/execution/lifecycle/run-completion.ts +0 -247
  172. package/src/execution/lifecycle/run-initialization.ts +0 -187
  173. package/src/execution/lifecycle/run-regression.ts +0 -305
  174. package/src/execution/lifecycle/run-setup.ts +0 -240
  175. package/src/execution/lifecycle/story-size-prompts.ts +0 -123
  176. package/src/execution/lock.ts +0 -129
  177. package/src/execution/parallel-coordinator.ts +0 -281
  178. package/src/execution/parallel-executor-rectification-pass.ts +0 -117
  179. package/src/execution/parallel-executor-rectify.ts +0 -136
  180. package/src/execution/parallel-executor.ts +0 -330
  181. package/src/execution/parallel-worker.ts +0 -149
  182. package/src/execution/parallel.ts +0 -13
  183. package/src/execution/pid-registry.ts +0 -275
  184. package/src/execution/pipeline-result-handler.ts +0 -221
  185. package/src/execution/progress.ts +0 -27
  186. package/src/execution/queue-handler.ts +0 -109
  187. package/src/execution/runner-completion.ts +0 -171
  188. package/src/execution/runner-execution.ts +0 -243
  189. package/src/execution/runner-setup.ts +0 -86
  190. package/src/execution/runner.ts +0 -265
  191. package/src/execution/sequential-executor.ts +0 -219
  192. package/src/execution/status-file.ts +0 -264
  193. package/src/execution/status-writer.ts +0 -181
  194. package/src/execution/story-context.ts +0 -266
  195. package/src/execution/story-selector.ts +0 -76
  196. package/src/execution/test-output-parser.ts +0 -14
  197. package/src/execution/timeout-handler.ts +0 -100
  198. package/src/hooks/index.ts +0 -2
  199. package/src/hooks/runner.ts +0 -280
  200. package/src/hooks/types.ts +0 -79
  201. package/src/interaction/chain.ts +0 -170
  202. package/src/interaction/index.ts +0 -61
  203. package/src/interaction/init.ts +0 -84
  204. package/src/interaction/plugins/auto.ts +0 -243
  205. package/src/interaction/plugins/cli.ts +0 -300
  206. package/src/interaction/plugins/telegram.ts +0 -384
  207. package/src/interaction/plugins/webhook.ts +0 -286
  208. package/src/interaction/state.ts +0 -171
  209. package/src/interaction/triggers.ts +0 -250
  210. package/src/interaction/types.ts +0 -170
  211. package/src/logger/formatters.ts +0 -84
  212. package/src/logger/index.ts +0 -16
  213. package/src/logger/logger.ts +0 -296
  214. package/src/logger/types.ts +0 -48
  215. package/src/logging/formatter.ts +0 -355
  216. package/src/logging/index.ts +0 -22
  217. package/src/logging/types.ts +0 -93
  218. package/src/metrics/aggregator.ts +0 -191
  219. package/src/metrics/index.ts +0 -14
  220. package/src/metrics/tracker.ts +0 -200
  221. package/src/metrics/types.ts +0 -115
  222. package/src/optimizer/index.ts +0 -63
  223. package/src/optimizer/noop.optimizer.ts +0 -24
  224. package/src/optimizer/rule-based.optimizer.ts +0 -248
  225. package/src/optimizer/types.ts +0 -53
  226. package/src/pipeline/event-bus.ts +0 -297
  227. package/src/pipeline/events.ts +0 -130
  228. package/src/pipeline/index.ts +0 -19
  229. package/src/pipeline/runner.ts +0 -149
  230. package/src/pipeline/stages/acceptance-setup.ts +0 -144
  231. package/src/pipeline/stages/acceptance.ts +0 -215
  232. package/src/pipeline/stages/autofix.ts +0 -262
  233. package/src/pipeline/stages/completion.ts +0 -110
  234. package/src/pipeline/stages/constitution.ts +0 -63
  235. package/src/pipeline/stages/context.ts +0 -122
  236. package/src/pipeline/stages/execution.ts +0 -359
  237. package/src/pipeline/stages/index.ts +0 -86
  238. package/src/pipeline/stages/optimizer.ts +0 -74
  239. package/src/pipeline/stages/prompt.ts +0 -79
  240. package/src/pipeline/stages/queue-check.ts +0 -103
  241. package/src/pipeline/stages/rectify.ts +0 -101
  242. package/src/pipeline/stages/regression.ts +0 -99
  243. package/src/pipeline/stages/review.ts +0 -94
  244. package/src/pipeline/stages/routing.ts +0 -276
  245. package/src/pipeline/stages/verify.ts +0 -286
  246. package/src/pipeline/subscribers/events-writer.ts +0 -135
  247. package/src/pipeline/subscribers/hooks.ts +0 -179
  248. package/src/pipeline/subscribers/interaction.ts +0 -103
  249. package/src/pipeline/subscribers/registry.ts +0 -73
  250. package/src/pipeline/subscribers/reporters.ts +0 -174
  251. package/src/pipeline/types.ts +0 -220
  252. package/src/plugins/extensions.ts +0 -225
  253. package/src/plugins/index.ts +0 -33
  254. package/src/plugins/loader.ts +0 -352
  255. package/src/plugins/plugin-logger.ts +0 -41
  256. package/src/plugins/registry.ts +0 -168
  257. package/src/plugins/types.ts +0 -206
  258. package/src/plugins/validator.ts +0 -352
  259. package/src/prd/index.ts +0 -220
  260. package/src/prd/schema.ts +0 -268
  261. package/src/prd/types.ts +0 -273
  262. package/src/prd/validate.ts +0 -41
  263. package/src/precheck/checks-agents.ts +0 -63
  264. package/src/precheck/checks-blockers.ts +0 -23
  265. package/src/precheck/checks-cli.ts +0 -68
  266. package/src/precheck/checks-config.ts +0 -102
  267. package/src/precheck/checks-git.ts +0 -117
  268. package/src/precheck/checks-system.ts +0 -101
  269. package/src/precheck/checks-warnings.ts +0 -221
  270. package/src/precheck/checks.ts +0 -36
  271. package/src/precheck/index.ts +0 -374
  272. package/src/precheck/story-size-gate.ts +0 -144
  273. package/src/precheck/types.ts +0 -31
  274. package/src/prompts/builder.ts +0 -166
  275. package/src/prompts/index.ts +0 -2
  276. package/src/prompts/loader.ts +0 -43
  277. package/src/prompts/sections/conventions.ts +0 -19
  278. package/src/prompts/sections/hermetic.ts +0 -41
  279. package/src/prompts/sections/index.ts +0 -12
  280. package/src/prompts/sections/isolation.ts +0 -70
  281. package/src/prompts/sections/role-task.ts +0 -182
  282. package/src/prompts/sections/story.ts +0 -55
  283. package/src/prompts/sections/verdict.ts +0 -70
  284. package/src/prompts/types.ts +0 -21
  285. package/src/queue/index.ts +0 -2
  286. package/src/queue/manager.ts +0 -254
  287. package/src/queue/types.ts +0 -54
  288. package/src/review/index.ts +0 -8
  289. package/src/review/orchestrator.ts +0 -154
  290. package/src/review/runner.ts +0 -303
  291. package/src/review/types.ts +0 -70
  292. package/src/routing/batch-route.ts +0 -35
  293. package/src/routing/builder.ts +0 -81
  294. package/src/routing/chain.ts +0 -75
  295. package/src/routing/content-hash.ts +0 -25
  296. package/src/routing/index.ts +0 -20
  297. package/src/routing/loader.ts +0 -62
  298. package/src/routing/router.ts +0 -305
  299. package/src/routing/strategies/adaptive.ts +0 -215
  300. package/src/routing/strategies/index.ts +0 -8
  301. package/src/routing/strategies/keyword.ts +0 -180
  302. package/src/routing/strategies/llm-prompts.ts +0 -224
  303. package/src/routing/strategies/llm.ts +0 -320
  304. package/src/routing/strategies/manual.ts +0 -50
  305. package/src/routing/strategy.ts +0 -102
  306. package/src/tdd/cleanup.ts +0 -120
  307. package/src/tdd/index.ts +0 -22
  308. package/src/tdd/isolation.ts +0 -117
  309. package/src/tdd/orchestrator.ts +0 -406
  310. package/src/tdd/prompts.ts +0 -40
  311. package/src/tdd/rectification-gate.ts +0 -274
  312. package/src/tdd/session-runner.ts +0 -263
  313. package/src/tdd/types.ts +0 -84
  314. package/src/tdd/verdict-reader.ts +0 -266
  315. package/src/tdd/verdict.ts +0 -152
  316. package/src/tui/App.tsx +0 -265
  317. package/src/tui/components/AgentPanel.tsx +0 -75
  318. package/src/tui/components/CostOverlay.tsx +0 -118
  319. package/src/tui/components/HelpOverlay.tsx +0 -107
  320. package/src/tui/components/StatusBar.tsx +0 -63
  321. package/src/tui/components/StoriesPanel.tsx +0 -177
  322. package/src/tui/hooks/useKeyboard.ts +0 -142
  323. package/src/tui/hooks/useLayout.ts +0 -137
  324. package/src/tui/hooks/usePipelineEvents.ts +0 -183
  325. package/src/tui/hooks/usePty.ts +0 -189
  326. package/src/tui/index.tsx +0 -38
  327. package/src/tui/types.ts +0 -76
  328. package/src/utils/errors.ts +0 -12
  329. package/src/utils/git.ts +0 -245
  330. package/src/utils/json-file.ts +0 -72
  331. package/src/utils/log-test-output.ts +0 -25
  332. package/src/utils/path-security.ts +0 -73
  333. package/src/utils/queue-writer.ts +0 -54
  334. package/src/verification/crash-detector.ts +0 -34
  335. package/src/verification/executor.ts +0 -250
  336. package/src/verification/index.ts +0 -12
  337. package/src/verification/orchestrator-types.ts +0 -154
  338. package/src/verification/orchestrator.ts +0 -76
  339. package/src/verification/parser.ts +0 -220
  340. package/src/verification/rectification-loop.ts +0 -172
  341. package/src/verification/rectification.ts +0 -108
  342. package/src/verification/runners.ts +0 -129
  343. package/src/verification/smart-runner.ts +0 -307
  344. package/src/verification/strategies/acceptance.ts +0 -136
  345. package/src/verification/strategies/regression.ts +0 -90
  346. package/src/verification/strategies/scoped.ts +0 -154
  347. package/src/verification/types.ts +0 -117
  348. package/src/version.ts +0 -40
  349. package/src/worktree/dispatcher.ts +0 -6
  350. package/src/worktree/index.ts +0 -2
  351. package/src/worktree/manager.ts +0 -193
  352. package/src/worktree/merge.ts +0 -302
  353. package/src/worktree/types.ts +0 -4
@@ -1,352 +0,0 @@
1
- /**
2
- * Analyze Command -- Parse spec.md into prd.json via agent decompose
3
- *
4
- * Uses agent adapter's decompose() method to break spec into classified stories
5
- * in a single LLM call. Falls back to keyword classification if decompose fails.
6
- */
7
-
8
- import { existsSync } from "node:fs";
9
- import { join } from "node:path";
10
- import { generateAcceptanceTests } from "../acceptance";
11
- import { getAgent } from "../agents/registry";
12
- import { scanCodebase } from "../analyze/scanner";
13
- import type { NaxConfig } from "../config";
14
- import { resolveModel } from "../config/schema";
15
- import { applyDecomposition } from "../decompose/apply";
16
- import { DecomposeBuilder } from "../decompose/builder";
17
- import type { DecomposeConfig as BuilderDecomposeConfig, DecomposeResult, SubStory } from "../decompose/types";
18
- import { getLogger } from "../logger";
19
- import { loadPRD, savePRD } from "../prd";
20
- import type { PRD, UserStory } from "../prd";
21
- import { routeTask } from "../routing";
22
- import { NAX_VERSION } from "../version";
23
- import {
24
- applyKeywordClassification,
25
- buildCodebaseContext,
26
- parseUserStoriesFromSpec,
27
- reclassifyExistingPRD,
28
- } from "./analyze-parser";
29
-
30
- export interface AnalyzeOptions {
31
- featureDir: string;
32
- featureName: string;
33
- branchName: string;
34
- config?: NaxConfig;
35
- specPath?: string;
36
- reclassify?: boolean;
37
- }
38
-
39
- /** Parse spec.md into PRD via agent decompose */
40
- export async function analyzeFeature(options: AnalyzeOptions): Promise<PRD> {
41
- const { featureDir, featureName, branchName, config, specPath: explicitSpecPath, reclassify = false } = options;
42
- const workdir = join(featureDir, "../..");
43
-
44
- if (reclassify) {
45
- return await reclassifyExistingPRD(featureDir, featureName, branchName, workdir, config);
46
- }
47
-
48
- const specPath = explicitSpecPath || join(featureDir, "spec.md");
49
- if (!existsSync(specPath)) throw new Error(`spec.md not found at ${specPath}`);
50
-
51
- const specContent = await Bun.file(specPath).text();
52
- let userStories: UserStory[];
53
-
54
- const logger = getLogger();
55
- if (config?.analyze.llmEnhanced) {
56
- userStories = await decomposeLLM(specContent, workdir, config, logger);
57
- } else {
58
- logger.info("cli", "LLM-enhanced analysis disabled, using manual parsing + keyword classification");
59
- userStories = parseUserStoriesFromSpec(specContent);
60
- if (userStories.length === 0) throw new Error("No user stories found in spec.md. Expected '## US-xxx' headings.");
61
- userStories = applyKeywordClassification(userStories, config);
62
- }
63
-
64
- if (config && userStories.length > config.execution.maxStoriesPerFeature) {
65
- logger.warn(
66
- "cli",
67
- `Feature has ${userStories.length} stories, exceeding recommended limit of ${config.execution.maxStoriesPerFeature}. Consider splitting.`,
68
- );
69
- }
70
-
71
- const naxVersion = NAX_VERSION;
72
-
73
- const now = new Date().toISOString();
74
- const prd: PRD = {
75
- project: "nax",
76
- feature: featureName,
77
- branchName,
78
- createdAt: now,
79
- updatedAt: now,
80
- userStories,
81
- analyzeConfig: config
82
- ? {
83
- naxVersion,
84
- model: config.analyze.model,
85
- llmEnhanced: config.analyze.llmEnhanced,
86
- maxStoriesPerFeature: config.execution.maxStoriesPerFeature,
87
- routingStrategy: config.analyze.llmEnhanced ? "llm" : "keyword",
88
- }
89
- : undefined,
90
- };
91
-
92
- // Generate acceptance tests if enabled
93
- if (config?.acceptance.enabled && config.acceptance.generateTests) {
94
- await generateAcceptanceTestsForFeature(specContent, featureName, featureDir, workdir, config, logger);
95
- }
96
-
97
- return prd;
98
- }
99
-
100
- /** Run LLM-enhanced decompose and classify stories. */
101
- async function decomposeLLM(
102
- specContent: string,
103
- workdir: string,
104
- config: NaxConfig,
105
- logger: ReturnType<typeof getLogger>,
106
- ): Promise<UserStory[]> {
107
- logger.info("cli", "Running agent decompose (decompose + classify in single LLM call)");
108
-
109
- try {
110
- const scan = await scanCodebase(workdir);
111
- const codebaseContext = buildCodebaseContext(scan);
112
- const agentName = config.autoMode.defaultAgent;
113
- const adapter = getAgent(agentName);
114
- if (!adapter) throw new Error(`Agent "${agentName}" not found`);
115
-
116
- const modelTier = config.analyze.model;
117
- const modelDef = resolveModel(config.models[modelTier]);
118
- const result = await adapter.decompose({ specContent, workdir, codebaseContext, modelTier, modelDef, config });
119
-
120
- logger.info("cli", "[OK] Agent decompose complete", { storiesCount: result.stories.length });
121
-
122
- return result.stories.map((ds) => {
123
- let testStrategy: import("../config").TestStrategy;
124
- let routingStrategy: "llm" | "keyword";
125
-
126
- if (ds.testStrategy) {
127
- testStrategy = ds.testStrategy;
128
- routingStrategy = "llm";
129
- } else {
130
- const routing = routeTask(ds.title, ds.description, ds.acceptanceCriteria, ds.tags, config);
131
- testStrategy = routing.testStrategy;
132
- routingStrategy = "keyword";
133
- }
134
-
135
- return {
136
- id: ds.id,
137
- title: ds.title,
138
- description: ds.description,
139
- acceptanceCriteria: ds.acceptanceCriteria,
140
- tags: ds.tags,
141
- dependencies: ds.dependencies,
142
- status: "pending" as const,
143
- passes: false,
144
- escalations: [],
145
- attempts: 0,
146
- routing: {
147
- complexity: ds.complexity,
148
- testStrategy,
149
- reasoning: ds.reasoning,
150
- estimatedLOC: ds.estimatedLOC,
151
- risks: ds.risks,
152
- strategy: routingStrategy,
153
- llmModel: routingStrategy === "llm" ? modelDef.model : undefined,
154
- },
155
- contextFiles: ds.contextFiles,
156
- };
157
- });
158
- } catch (error) {
159
- logger.warn("cli", "Agent decompose failed, falling back to manual story extraction", {
160
- error: (error as Error).message,
161
- });
162
- const stories = parseUserStoriesFromSpec(specContent);
163
- if (stories.length === 0) {
164
- throw new Error("No user stories found in spec.md. Expected '## US-xxx' headings or agent decompose to succeed.");
165
- }
166
- return applyKeywordClassification(stories, config);
167
- }
168
- }
169
-
170
- /** Generate acceptance tests for a feature. */
171
- async function generateAcceptanceTestsForFeature(
172
- specContent: string,
173
- featureName: string,
174
- featureDir: string,
175
- workdir: string,
176
- config: NaxConfig,
177
- logger: ReturnType<typeof getLogger>,
178
- ): Promise<void> {
179
- logger.info("cli", "Generating acceptance tests from spec.md");
180
- try {
181
- const scan = await scanCodebase(workdir);
182
- const codebaseContext = buildCodebaseContext(scan);
183
- const adapter = getAgent(config.autoMode.defaultAgent);
184
- if (!adapter) throw new Error(`Agent "${config.autoMode.defaultAgent}" not found`);
185
-
186
- const modelTier = config.analyze.model;
187
- const modelDef = resolveModel(config.models[modelTier]);
188
- const result = await generateAcceptanceTests(adapter, {
189
- specContent,
190
- featureName,
191
- workdir,
192
- codebaseContext,
193
- modelTier,
194
- modelDef,
195
- config,
196
- });
197
-
198
- const acceptanceTestPath = join(featureDir, config.acceptance.testPath);
199
- await Bun.write(acceptanceTestPath, result.testCode);
200
- logger.info("cli", "[OK] Acceptance tests generated", {
201
- criteriaCount: result.criteria.length,
202
- testPath: acceptanceTestPath,
203
- });
204
- } catch (error) {
205
- logger.warn("cli", "Failed to generate acceptance tests", { error: (error as Error).message });
206
- }
207
- }
208
-
209
- // ============================================================================
210
- // SD-004: Story decompose CLI entry points
211
- // ============================================================================
212
-
213
- /** Default runDecompose implementation — replaced in tests via _decomposeCLIDeps. */
214
- async function runDecomposeDefault(
215
- story: UserStory,
216
- prd: PRD,
217
- config: NaxConfig,
218
- _featureDir: string,
219
- ): Promise<DecomposeResult> {
220
- const naxDecompose = config.decompose;
221
- const builderConfig: BuilderDecomposeConfig = {
222
- maxSubStories: naxDecompose?.maxSubstories ?? 5,
223
- maxComplexity: naxDecompose?.maxSubstoryComplexity ?? "medium",
224
- maxRetries: naxDecompose?.maxRetries ?? 2,
225
- };
226
- const agent = getAgent(config.autoMode.defaultAgent);
227
- if (!agent) {
228
- throw new Error(`[decompose] Agent "${config.autoMode.defaultAgent}" not found — cannot decompose`);
229
- }
230
- const adapter = {
231
- async decompose(prompt: string): Promise<string> {
232
- return agent.complete(prompt, { jsonMode: true, config });
233
- },
234
- };
235
- return DecomposeBuilder.for(story).prd(prd).config(builderConfig).decompose(adapter);
236
- }
237
-
238
- /** Load PRD from featureDir and return both PRD and resolved path. */
239
- async function loadPRDFromDir(featureDir: string): Promise<{ prd: PRD; prdPath: string }> {
240
- const prdPath = join(featureDir, "prd.json");
241
- const prd = await loadPRD(prdPath);
242
- return { prd, prdPath };
243
- }
244
-
245
- /** Build a human-readable summary of decomposed substories. */
246
- function buildSummaryLines(subStories: SubStory[]): string[] {
247
- const lines: string[] = ["Decomposed substories:"];
248
- for (const sub of subStories) {
249
- lines.push(` ${sub.id} ${sub.title} [${sub.complexity}] parent: ${sub.parentStoryId}`);
250
- }
251
- return lines;
252
- }
253
-
254
- /** Default print implementation — writes lines to stdout. */
255
- function printSummaryDefault(lines: string[]): void {
256
- for (const line of lines) {
257
- process.stdout.write(`${line}\n`);
258
- }
259
- }
260
-
261
- /**
262
- * Swappable dependencies for CLI decompose functions.
263
- * Tests override individual entries without using mock.module().
264
- */
265
- export const _decomposeCLIDeps = {
266
- loadPRD: loadPRDFromDir,
267
- runDecompose: runDecomposeDefault,
268
- applyDecomposition,
269
- savePRD,
270
- printSummary: printSummaryDefault,
271
- };
272
-
273
- /**
274
- * Decompose a single story by ID via --decompose <storyId>.
275
- *
276
- * Loads the PRD, runs decomposition, applies result, and saves the updated PRD.
277
- * Prints a summary table of the generated substories.
278
- */
279
- export async function decomposeStory(
280
- storyId: string,
281
- options: { featureDir: string; config: NaxConfig },
282
- ): Promise<void> {
283
- const { featureDir, config } = options;
284
- const logger = getLogger();
285
-
286
- const { prd, prdPath } = await _decomposeCLIDeps.loadPRD(featureDir);
287
-
288
- const story = prd.userStories.find((s) => s.id === storyId);
289
- if (!story) {
290
- throw new Error(`Story ${storyId} not found in PRD`);
291
- }
292
-
293
- const result = await _decomposeCLIDeps.runDecompose(story, prd, config, featureDir);
294
-
295
- if (!result.validation.valid) {
296
- logger.warn("cli", `Decompose failed for ${storyId}: ${result.validation.errors.join(", ")}`);
297
- return;
298
- }
299
-
300
- _decomposeCLIDeps.applyDecomposition(prd, result);
301
- await _decomposeCLIDeps.savePRD(prd, prdPath);
302
-
303
- const lines = buildSummaryLines(result.subStories);
304
- _decomposeCLIDeps.printSummary(lines);
305
- }
306
-
307
- /**
308
- * Decompose all oversized stories via --decompose-oversized.
309
- *
310
- * Iterates all stories, decomposes any that exceed config.decompose.maxAcceptanceCriteria
311
- * AND have complex/expert complexity. Saves the PRD once after all decompositions.
312
- */
313
- export async function decomposeOversized(options: { featureDir: string; config: NaxConfig }): Promise<void> {
314
- const { featureDir, config } = options;
315
- const logger = getLogger();
316
-
317
- const { prd, prdPath } = await _decomposeCLIDeps.loadPRD(featureDir);
318
-
319
- const threshold = config.decompose?.maxAcceptanceCriteria ?? 6;
320
- const oversized = prd.userStories.filter((s) => {
321
- const complexity = s.routing?.complexity;
322
- return s.acceptanceCriteria.length > threshold && (complexity === "complex" || complexity === "expert");
323
- });
324
-
325
- if (oversized.length === 0) {
326
- logger.info("cli", "No oversized stories found");
327
- return;
328
- }
329
-
330
- const allSubStories: SubStory[] = [];
331
- let anyDecomposed = false;
332
-
333
- for (const story of oversized) {
334
- const result = await _decomposeCLIDeps.runDecompose(story, prd, config, featureDir);
335
- if (result.validation.valid) {
336
- _decomposeCLIDeps.applyDecomposition(prd, result);
337
- allSubStories.push(...result.subStories);
338
- anyDecomposed = true;
339
- } else {
340
- logger.warn("cli", `Decompose failed for ${story.id}: ${result.validation.errors.join(", ")}`);
341
- }
342
- }
343
-
344
- if (anyDecomposed) {
345
- await _decomposeCLIDeps.savePRD(prd, prdPath);
346
- }
347
-
348
- if (allSubStories.length > 0) {
349
- const lines = buildSummaryLines(allSubStories);
350
- _decomposeCLIDeps.printSummary(lines);
351
- }
352
- }
@@ -1,219 +0,0 @@
1
- /**
2
- * Configuration Field Descriptions
3
- *
4
- * Human-readable descriptions for all configuration fields.
5
- * Extracted from config-display.ts for better maintainability.
6
- */
7
-
8
- export const FIELD_DESCRIPTIONS: Record<string, string> = {
9
- // Top-level
10
- version: "Configuration schema version",
11
-
12
- // Models
13
- models: "Model tier definitions (fast/balanced/powerful)",
14
- "models.fast": "Fast model for lightweight tasks (e.g., haiku)",
15
- "models.balanced": "Balanced model for general coding (e.g., sonnet)",
16
- "models.powerful": "Powerful model for complex tasks (e.g., opus)",
17
-
18
- // Auto mode
19
- autoMode:
20
- "Auto mode configuration for agent orchestration. Enables multi-agent routing with model tier selection per task complexity and escalation on failures.",
21
- "autoMode.enabled": "Enable automatic agent selection and escalation",
22
- "autoMode.defaultAgent":
23
- "Default agent to use when no specific agent is requested. Examples: 'claude' (Claude Code), 'codex' (GitHub Copilot), 'opencode' (OpenCode). The agent handles the main coding tasks.",
24
- "autoMode.fallbackOrder":
25
- 'Fallback order for agent selection when the primary agent is rate-limited, unavailable, or fails. Tries each agent in sequence until one succeeds. Example: ["claude", "codex", "opencode"] means try Claude first, then Copilot, then OpenCode.',
26
- "autoMode.complexityRouting":
27
- "Model tier routing rules mapped to story complexity levels. Determines which model (fast/balanced/powerful) to use based on task complexity: simple → fast, medium → balanced, complex → powerful, expert → powerful.",
28
- "autoMode.complexityRouting.simple": "Model tier for simple tasks (low complexity, straightforward changes)",
29
- "autoMode.complexityRouting.medium": "Model tier for medium tasks (moderate complexity, multi-file changes)",
30
- "autoMode.complexityRouting.complex": "Model tier for complex tasks (high complexity, architectural decisions)",
31
- "autoMode.complexityRouting.expert":
32
- "Model tier for expert tasks (highest complexity, novel problems, design patterns)",
33
- "autoMode.escalation":
34
- "Escalation settings for failed stories. When a story fails after max attempts at current tier, escalate to the next tier in tierOrder. Enables progressive use of more powerful models.",
35
- "autoMode.escalation.enabled": "Enable tier escalation on failure",
36
- "autoMode.escalation.tierOrder":
37
- 'Ordered tier escalation chain with per-tier attempt budgets. Format: [{"tier": "fast", "attempts": 2}, {"tier": "balanced", "attempts": 2}, {"tier": "powerful", "attempts": 1}]. Allows each tier to attempt fixes before escalating to the next.',
38
- "autoMode.escalation.escalateEntireBatch":
39
- "When enabled, escalate all stories in a batch if one fails. When disabled, only the failing story escalates (allows parallel attempts at different tiers).",
40
-
41
- // Routing
42
- routing: "Model routing strategy configuration",
43
- "routing.strategy": "Routing strategy: keyword | llm | manual | adaptive | custom",
44
- "routing.customStrategyPath": "Path to custom routing strategy (if strategy=custom)",
45
- "routing.adaptive": "Adaptive routing settings",
46
- "routing.adaptive.minSamples": "Minimum samples before adaptive routing activates",
47
- "routing.adaptive.costThreshold": "Cost threshold for strategy switching (0-1)",
48
- "routing.adaptive.fallbackStrategy": "Fallback strategy if adaptive fails",
49
- "routing.llm": "LLM-based routing settings",
50
- "routing.llm.model": "Model tier for routing decisions",
51
- "routing.llm.fallbackToKeywords": "Fall back to keyword routing on LLM failure",
52
- "routing.llm.cacheDecisions": "Cache routing decisions per story ID",
53
- "routing.llm.mode": "Routing mode: one-shot | per-story | hybrid",
54
- "routing.llm.timeoutMs": "Timeout for LLM routing call in milliseconds",
55
-
56
- // Execution
57
- execution: "Execution limits and timeouts",
58
- "execution.maxIterations": "Max iterations per feature run (auto-calculated if not set)",
59
- "execution.iterationDelayMs": "Delay between iterations in milliseconds",
60
- "execution.costLimit": "Max cost in USD before pausing execution",
61
- "execution.sessionTimeoutSeconds": "Timeout per agent coding session in seconds",
62
- "execution.verificationTimeoutSeconds": "Verification subprocess timeout in seconds",
63
- "execution.maxStoriesPerFeature": "Max stories per feature (prevents memory exhaustion)",
64
- "execution.contextProviderTokenBudget": "Token budget for plugin context providers",
65
- "execution.lintCommand": "Lint command override (null=disabled, undefined=auto-detect)",
66
- "execution.typecheckCommand": "Typecheck command override (null=disabled, undefined=auto-detect)",
67
- "execution.dangerouslySkipPermissions": "Skip permissions for agent (use with caution)",
68
- "execution.rectification": "Rectification loop settings (retry failed tests)",
69
- "execution.rectification.enabled": "Enable rectification loop",
70
- "execution.rectification.maxRetries": "Max retry attempts per story",
71
- "execution.rectification.fullSuiteTimeoutSeconds": "Timeout for full test suite run in seconds",
72
- "execution.rectification.maxFailureSummaryChars": "Max characters in failure summary",
73
- "execution.rectification.abortOnIncreasingFailures": "Abort if failure count increases",
74
- "execution.regressionGate": "Regression gate settings (full suite after scoped tests)",
75
- "execution.regressionGate.enabled": "Enable full-suite regression gate",
76
- "execution.regressionGate.timeoutSeconds": "Timeout for regression run in seconds",
77
-
78
- // Quality
79
- quality: "Quality gate configuration",
80
- "quality.requireTypecheck": "Require typecheck to pass",
81
- "quality.requireLint": "Require lint to pass",
82
- "quality.requireTests": "Require tests to pass",
83
- "quality.commands": "Custom quality commands",
84
- "quality.commands.typecheck": "Custom typecheck command",
85
- "quality.commands.lint": "Custom lint command",
86
- "quality.commands.test": "Custom test command",
87
- "quality.forceExit": "Append --forceExit to test command (prevents hangs)",
88
- "quality.detectOpenHandles": "Append --detectOpenHandles on timeout",
89
- "quality.detectOpenHandlesRetries": "Max retries with --detectOpenHandles",
90
- "quality.gracePeriodMs": "Grace period in ms after SIGTERM before SIGKILL",
91
- "quality.drainTimeoutMs": "Deadline in ms to drain stdout/stderr after kill",
92
- "quality.shell": "Shell to use for verification commands",
93
- "quality.stripEnvVars": "Environment variables to strip during verification",
94
- "quality.environmentalEscalationDivisor": "Divisor for environmental failure early escalation",
95
-
96
- // TDD
97
- tdd: "Test-driven development configuration",
98
- "tdd.maxRetries": "Max retries per TDD session before escalating",
99
- "tdd.autoVerifyIsolation": "Auto-verify test isolation between sessions",
100
- "tdd.strategy": "TDD strategy: auto | strict | lite | off",
101
- "tdd.autoApproveVerifier": "Auto-approve legitimate fixes in verifier session",
102
- "tdd.sessionTiers": "Per-session model tier overrides",
103
- "tdd.sessionTiers.testWriter": "Model tier for test-writer session",
104
- "tdd.sessionTiers.implementer": "Model tier for implementer session",
105
- "tdd.sessionTiers.verifier": "Model tier for verifier session",
106
- "tdd.testWriterAllowedPaths": "Glob patterns for files test-writer can modify",
107
- "tdd.rollbackOnFailure": "Rollback git changes when TDD fails",
108
- "tdd.greenfieldDetection": "Force test-after on projects with no test files",
109
-
110
- // Constitution
111
- constitution: "Constitution settings (core rules and constraints)",
112
- "constitution.enabled": "Enable constitution loading and injection",
113
- "constitution.path": "Path to constitution file (relative to nax/ directory)",
114
- "constitution.maxTokens": "Maximum tokens allowed for constitution content",
115
- "constitution.skipGlobal": "Skip loading global constitution",
116
-
117
- // Analyze
118
- analyze: "Feature analysis settings",
119
- "analyze.llmEnhanced": "Enable LLM-enhanced analysis",
120
- "analyze.model": "Model tier for decompose and classify",
121
- "analyze.fallbackToKeywords": "Fall back to keyword matching on LLM failure",
122
- "analyze.maxCodebaseSummaryTokens": "Max tokens for codebase summary",
123
-
124
- // Review
125
- review: "Review phase configuration",
126
- "review.enabled": "Enable review phase",
127
- "review.checks": "List of checks to run (typecheck, lint, test)",
128
- "review.commands": "Custom commands per check",
129
- "review.commands.typecheck": "Custom typecheck command for review",
130
- "review.commands.lint": "Custom lint command for review",
131
- "review.commands.test": "Custom test command for review",
132
-
133
- // Plan
134
- plan: "Planning phase configuration",
135
- "plan.model": "Model tier for planning",
136
- "plan.outputPath": "Output path for generated spec (relative to nax/)",
137
-
138
- // Acceptance
139
- acceptance: "Acceptance test configuration",
140
- "acceptance.enabled": "Enable acceptance test generation and validation",
141
- "acceptance.maxRetries": "Max retry loops for fix stories",
142
- "acceptance.generateTests": "Generate acceptance tests during analyze",
143
- "acceptance.testPath": "Path to acceptance test file (relative to feature dir)",
144
- "acceptance.timeoutMs": "Timeout for acceptance test generation in milliseconds (default: 1800000 = 30 min)",
145
-
146
- // Context
147
- context: "Context injection configuration",
148
- "context.fileInjection":
149
- "Mode: 'disabled' (default, MCP-aware agents pull context on-demand) | 'keyword' (legacy git-grep injection for non-MCP agents). Set context.fileInjection in config.",
150
- "context.testCoverage": "Test coverage context settings",
151
- "context.testCoverage.enabled": "Enable test coverage context injection",
152
- "context.testCoverage.detail": "Detail level: names-only | names-and-counts | describe-blocks",
153
- "context.testCoverage.maxTokens": "Max tokens for test summary",
154
- "context.testCoverage.testDir": "Test directory relative to workdir",
155
- "context.testCoverage.testPattern": "Glob pattern for test files",
156
- "context.testCoverage.scopeToStory": "Scope test coverage to story-relevant files only",
157
- "context.autoDetect": "Auto-detect relevant files settings",
158
- "context.autoDetect.enabled": "Enable auto-detection of relevant files",
159
- "context.autoDetect.maxFiles": "Max files to auto-detect",
160
- "context.autoDetect.traceImports": "Trace imports to find related files",
161
-
162
- // Optimizer
163
- optimizer: "Prompt optimizer configuration",
164
- "optimizer.enabled": "Enable prompt optimizer",
165
- "optimizer.strategy": "Optimization strategy: rule-based | llm | noop",
166
-
167
- // Plugins
168
- plugins: "Plugin configurations",
169
-
170
- // Hooks
171
- hooks: "Hooks configuration",
172
- "hooks.skipGlobal": "Skip loading global hooks",
173
-
174
- // Interaction
175
- interaction: "Interaction plugin configuration",
176
- "interaction.plugin": "Plugin to use for interactions (default: cli)",
177
- "interaction.config": "Plugin-specific configuration",
178
- "interaction.defaults": "Default interaction settings",
179
- "interaction.defaults.timeout": "Default timeout in milliseconds",
180
- "interaction.defaults.fallback": "Default fallback behavior: continue | skip | escalate | abort",
181
- "interaction.triggers": "Enable/disable built-in triggers",
182
-
183
- // Precheck
184
- precheck: "Precheck configuration (run before analysis)",
185
- "precheck.storySizeGate": "Story size gate settings",
186
- "precheck.storySizeGate.enabled": "Enable story size gate",
187
- "precheck.storySizeGate.maxAcCount": "Max acceptance criteria count before flagging",
188
- "precheck.storySizeGate.maxDescriptionLength": "Max description character length before flagging",
189
- "precheck.storySizeGate.maxBulletPoints": "Max bullet point count before flagging",
190
-
191
- // Prompts
192
- prompts: "Prompt template overrides (PB-003: PromptBuilder)",
193
- "prompts.overrides": "Custom prompt template files for specific roles",
194
- "prompts.overrides.test-writer": 'Path to custom test-writer prompt (e.g., ".nax/prompts/test-writer.md")',
195
- "prompts.overrides.implementer": 'Path to custom implementer prompt (e.g., ".nax/prompts/implementer.md")',
196
- "prompts.overrides.verifier": 'Path to custom verifier prompt (e.g., ".nax/prompts/verifier.md")',
197
- "prompts.overrides.single-session": 'Path to custom single-session prompt (e.g., ".nax/prompts/single-session.md")',
198
-
199
- // Decompose
200
- decompose: "Story decomposition configuration (SD-003)",
201
- "decompose.trigger": "Decomposition trigger mode: auto | confirm | disabled",
202
- "decompose.maxAcceptanceCriteria": "Max acceptance criteria before flagging as oversized (default: 6)",
203
- "decompose.maxSubstories": "Max number of substories to generate (default: 5)",
204
- "decompose.maxSubstoryComplexity": "Max complexity for any generated substory (default: 'medium')",
205
- "decompose.maxRetries": "Max retries on decomposition validation failure (default: 2)",
206
- "decompose.model": "Model tier for decomposition LLM calls (default: 'balanced')",
207
-
208
- // Agent protocol
209
- agent: "Agent protocol configuration (ACP-003)",
210
- "agent.protocol": "Protocol for agent communication: 'acp' | 'cli' (default: 'acp')",
211
- "agent.maxInteractionTurns":
212
- "Max turns in multi-turn interaction loop when interactionBridge is active (default: 10)",
213
- // quality.testing (ENH-010) — per-package overridable
214
- "quality.testing": "Hermetic test enforcement — per-package overridable (ENH-010)",
215
- "quality.testing.hermetic":
216
- "Inject hermetic test requirement into prompts — never call real external services in tests (default: true)",
217
- "quality.testing.externalBoundaries": "Project-specific CLI tools/clients to mock (e.g. ['claude', 'acpx', 'redis'])",
218
- "quality.testing.mockGuidance": "Project-specific mocking guidance injected verbatim into the prompt",
219
- };
@@ -1,103 +0,0 @@
1
- /**
2
- * Config Diffing
3
- *
4
- * Compare global and project configurations.
5
- */
6
-
7
- /**
8
- * Represents a single config field difference.
9
- */
10
- export interface ConfigDiff {
11
- /** Dot-separated field path (e.g., "execution.maxIterations") */
12
- path: string;
13
- /** Value from global config */
14
- globalValue: unknown;
15
- /** Value from project config */
16
- projectValue: unknown;
17
- }
18
-
19
- /**
20
- * Deep diff two config objects, returning only fields that differ.
21
- *
22
- * @param global - Global config (defaults + global overrides)
23
- * @param project - Project config (raw overrides only)
24
- * @param currentPath - Current path in object tree (for recursion)
25
- * @returns Array of differences
26
- */
27
- export function deepDiffConfigs(
28
- global: Record<string, unknown>,
29
- project: Record<string, unknown>,
30
- currentPath: string[] = [],
31
- ): ConfigDiff[] {
32
- const diffs: ConfigDiff[] = [];
33
-
34
- // Iterate over project config keys (we only care about what project overrides)
35
- for (const key of Object.keys(project)) {
36
- const projectValue = project[key];
37
- const globalValue = global[key];
38
- const path = [...currentPath, key];
39
- const pathStr = path.join(".");
40
-
41
- // Handle nested objects
42
- if (
43
- projectValue !== null &&
44
- typeof projectValue === "object" &&
45
- !Array.isArray(projectValue) &&
46
- globalValue !== null &&
47
- typeof globalValue === "object" &&
48
- !Array.isArray(globalValue)
49
- ) {
50
- // Recurse into nested object
51
- const nestedDiffs = deepDiffConfigs(
52
- globalValue as Record<string, unknown>,
53
- projectValue as Record<string, unknown>,
54
- path,
55
- );
56
- diffs.push(...nestedDiffs);
57
- } else {
58
- // Compare primitive values or arrays
59
- if (!deepEqual(projectValue, globalValue)) {
60
- diffs.push({
61
- path: pathStr,
62
- globalValue,
63
- projectValue,
64
- });
65
- }
66
- }
67
- }
68
-
69
- return diffs;
70
- }
71
-
72
- /**
73
- * Deep equality check for two values.
74
- *
75
- * @param a - First value
76
- * @param b - Second value
77
- * @returns True if values are deeply equal
78
- */
79
- export function deepEqual(a: unknown, b: unknown): boolean {
80
- if (a === b) return true;
81
- if (a === null || b === null) return false;
82
- if (a === undefined || b === undefined) return false;
83
-
84
- // Handle arrays
85
- if (Array.isArray(a) && Array.isArray(b)) {
86
- if (a.length !== b.length) return false;
87
- return a.every((val, idx) => deepEqual(val, b[idx]));
88
- }
89
-
90
- // Handle objects
91
- if (typeof a === "object" && typeof b === "object") {
92
- const aObj = a as Record<string, unknown>;
93
- const bObj = b as Record<string, unknown>;
94
- const aKeys = Object.keys(aObj);
95
- const bKeys = Object.keys(bObj);
96
-
97
- if (aKeys.length !== bKeys.length) return false;
98
-
99
- return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
100
- }
101
-
102
- return false;
103
- }