@nathapp/nax 0.20.0 → 0.22.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 (233) hide show
  1. package/.claude/settings.json +15 -0
  2. package/.mcp.json +8 -0
  3. package/docs/20260304-review-nax.md +492 -0
  4. package/docs/ROADMAP.md +65 -18
  5. package/docs/adr/ADR-005-implementation-plan.md +655 -0
  6. package/docs/adr/ADR-005-pipeline-re-architecture.md +464 -0
  7. package/docs/specs/bug-039-orphan-processes.md +131 -0
  8. package/docs/specs/bug-040-review-rectification.md +82 -0
  9. package/docs/specs/bug-041-cross-story-test-isolation.md +88 -0
  10. package/docs/specs/bug-042-verifier-failure-capture.md +117 -0
  11. package/docs/specs/feat-010-smart-runner-git-history.md +96 -0
  12. package/docs/specs/feat-011-file-context-strategy.md +73 -0
  13. package/docs/specs/feat-012-tdd-writer-tier.md +79 -0
  14. package/docs/specs/feat-013-test-after-review.md +89 -0
  15. package/docs/specs/feat-014-heartbeat-observability.md +127 -0
  16. package/memory/topic/feat-010-baseref.md +28 -0
  17. package/memory/topic/feat-013-test-after-deprecation.md +22 -0
  18. package/nax/config.json +7 -4
  19. package/nax/features/bug-039-medium/prd.json +45 -0
  20. package/package.json +2 -2
  21. package/src/agents/claude.ts +109 -15
  22. package/src/config/types.ts +11 -0
  23. package/src/context/builder.ts +9 -1
  24. package/src/execution/dry-run.ts +81 -0
  25. package/src/execution/escalation/tier-outcome.ts +29 -44
  26. package/src/execution/executor-types.ts +65 -0
  27. package/src/execution/index.ts +0 -17
  28. package/src/execution/iteration-runner.ts +132 -0
  29. package/src/execution/lifecycle/index.ts +0 -1
  30. package/src/execution/lifecycle/run-regression.ts +5 -5
  31. package/src/execution/pipeline-result-handler.ts +51 -254
  32. package/src/execution/sequential-executor.ts +72 -315
  33. package/src/execution/story-selector.ts +75 -0
  34. package/src/pipeline/event-bus.ts +276 -0
  35. package/src/pipeline/runner.ts +51 -77
  36. package/src/pipeline/stages/autofix.ts +133 -0
  37. package/src/pipeline/stages/completion.ts +22 -30
  38. package/src/pipeline/stages/index.ts +30 -13
  39. package/src/pipeline/stages/rectify.ts +93 -0
  40. package/src/pipeline/stages/regression.ts +88 -0
  41. package/src/pipeline/stages/review.ts +19 -153
  42. package/src/pipeline/stages/verify.ts +19 -3
  43. package/src/pipeline/subscribers/hooks.ts +133 -0
  44. package/src/pipeline/subscribers/interaction.ts +68 -0
  45. package/src/pipeline/subscribers/reporters.ts +174 -0
  46. package/src/pipeline/types.ts +12 -1
  47. package/src/review/orchestrator.ts +105 -0
  48. package/src/review/runner.ts +39 -4
  49. package/src/routing/router.ts +3 -3
  50. package/src/routing/strategies/keyword.ts +5 -2
  51. package/src/routing/strategies/llm.ts +27 -1
  52. package/src/tdd/prompts.ts +1 -1
  53. package/src/utils/git.ts +49 -25
  54. package/src/verification/executor.ts +8 -2
  55. package/src/verification/index.ts +1 -1
  56. package/src/verification/orchestrator-types.ts +145 -0
  57. package/src/verification/orchestrator.ts +76 -0
  58. package/src/{execution/post-verify-rectification.ts → verification/rectification-loop.ts} +13 -20
  59. package/src/verification/{gate.ts → runners.ts} +17 -105
  60. package/src/verification/smart-runner.ts +6 -10
  61. package/src/verification/strategies/acceptance.ts +133 -0
  62. package/src/verification/strategies/regression.ts +90 -0
  63. package/src/verification/strategies/scoped.ts +123 -0
  64. package/test/COVERAGE-GAPS.md +333 -0
  65. package/test/{acceptance → e2e}/cm-003-default-view.test.ts +1 -0
  66. package/test/{integration/e2e.test.ts → e2e/plan-analyze-run.test.ts} +1 -0
  67. package/test/integration/{agent-validation.test.ts → cli/agent-validation.test.ts} +3 -3
  68. package/test/integration/{cli-config-default-edge-cases.test.ts → cli/cli-config-default-edge-cases.test.ts} +6 -5
  69. package/test/integration/{cli-config-default-view.test.ts → cli/cli-config-default-view.test.ts} +8 -7
  70. package/test/integration/{cli-config-diff.test.ts → cli/cli-config-diff.test.ts} +3 -2
  71. package/test/integration/{cli-config.test.ts → cli/cli-config.test.ts} +3 -2
  72. package/test/integration/{cli-diagnose.test.ts → cli/cli-diagnose.test.ts} +5 -4
  73. package/test/integration/{cli-logs.test.ts → cli/cli-logs.test.ts} +12 -3
  74. package/test/integration/{cli-plugins.test.ts → cli/cli-plugins.test.ts} +4 -3
  75. package/test/integration/{cli-precheck.test.ts → cli/cli-precheck.test.ts} +4 -3
  76. package/test/integration/{cli-run-headless.test.ts → cli/cli-run-headless.test.ts} +3 -2
  77. package/test/integration/{cli.test.ts → cli/cli.test.ts} +2 -1
  78. package/test/integration/{precheck-integration.test.ts → cli/precheck-integration.test.ts} +10 -9
  79. package/test/integration/{precheck-orchestrator.test.ts → cli/precheck-orchestrator.test.ts} +4 -3
  80. package/test/integration/{precheck.test.ts → cli/precheck.test.ts} +5 -4
  81. package/test/integration/{config-loader.test.ts → config/config-loader.test.ts} +2 -1
  82. package/test/integration/{config.test.ts → config/config.test.ts} +2 -2
  83. package/test/integration/config/merger.test.ts +1 -0
  84. package/test/integration/config/paths.test.ts +1 -0
  85. package/test/integration/{security-loader.test.ts → config/security-loader.test.ts} +2 -2
  86. package/test/integration/{context-integration.test.ts → context/context-integration.test.ts} +7 -6
  87. package/test/integration/{path-security.test.ts → context/context-path-security.test.ts} +2 -2
  88. package/test/integration/{context-provider-injection.test.ts → context/context-provider-injection.test.ts} +7 -6
  89. package/test/integration/{context-verification-integration.test.ts → context/context-verification-integration.test.ts} +5 -4
  90. package/test/integration/{s5-greenfield-fallback.test.ts → context/s5-greenfield-fallback.test.ts} +4 -3
  91. package/test/integration/{isolation.test.ts → execution/execution-isolation.test.ts} +1 -1
  92. package/test/integration/{execution.test.ts → execution/execution.test.ts} +8 -8
  93. package/test/integration/{parallel.test.ts → execution/parallel.test.ts} +2 -1
  94. package/test/integration/{prd-pause.test.ts → execution/prd-pause.test.ts} +2 -2
  95. package/test/integration/{prd-resolvers.test.ts → execution/prd-resolvers.test.ts} +3 -2
  96. package/test/integration/{progress.test.ts → execution/progress.test.ts} +1 -1
  97. package/test/integration/execution/runner-batching.test.ts +682 -0
  98. package/test/integration/{runner-config-plugins.test.ts → execution/runner-config-plugins.test.ts} +3 -2
  99. package/test/integration/execution/runner-escalation.test.ts +561 -0
  100. package/test/integration/{runner-fixes.test.ts → execution/runner-fixes.test.ts} +4 -3
  101. package/test/integration/{runner-plugin-integration.test.ts → execution/runner-plugin-integration.test.ts} +6 -5
  102. package/test/integration/execution/runner-queue-and-attempts.test.ts +476 -0
  103. package/test/integration/{status-file-integration.test.ts → execution/status-file-integration.test.ts} +9 -8
  104. package/test/integration/{status-file.test.ts → execution/status-file.test.ts} +3 -2
  105. package/test/integration/{status-writer.test.ts → execution/status-writer.test.ts} +5 -4
  106. package/test/integration/{story-id-in-events.test.ts → execution/story-id-in-events.test.ts} +9 -8
  107. package/test/integration/{interaction-chain-pipeline.test.ts → interaction/interaction-chain-pipeline.test.ts} +26 -14
  108. package/test/integration/{hooks.test.ts → pipeline/hooks.test.ts} +4 -2
  109. package/test/integration/{pipeline-acceptance.test.ts → pipeline/pipeline-acceptance.test.ts} +7 -6
  110. package/test/integration/{pipeline-events.test.ts → pipeline/pipeline-events.test.ts} +7 -6
  111. package/test/integration/{pipeline.test.ts → pipeline/pipeline.test.ts} +9 -7
  112. package/test/integration/{reporter-lifecycle.test.ts → pipeline/reporter-lifecycle.test.ts} +9 -7
  113. package/test/integration/{verify-stage.test.ts → pipeline/verify-stage.test.ts} +7 -5
  114. package/test/integration/{analyze-integration.test.ts → plan/analyze-integration.test.ts} +3 -2
  115. package/test/integration/{analyze-scanner.test.ts → plan/analyze-scanner.test.ts} +8 -7
  116. package/test/integration/{logger.test.ts → plan/logger.test.ts} +1 -1
  117. package/test/integration/{plan.test.ts → plan/plan.test.ts} +3 -3
  118. package/test/integration/plugins/config-integration.test.ts +1 -0
  119. package/test/integration/plugins/config-resolution.test.ts +1 -0
  120. package/test/integration/plugins/loader.test.ts +1 -0
  121. package/test/integration/plugins/{registry.test.ts → plugins-registry.test.ts} +1 -0
  122. package/test/integration/plugins/validator.test.ts +1 -0
  123. package/test/integration/{review-config-commands.test.ts → review/review-config-commands.test.ts} +4 -3
  124. package/test/integration/{review-config-schema.test.ts → review/review-config-schema.test.ts} +3 -2
  125. package/test/integration/{review-plugin-integration.test.ts → review/review-plugin-integration.test.ts} +5 -4
  126. package/test/integration/{review.test.ts → review/review.test.ts} +3 -2
  127. package/test/integration/routing/plugin-routing-advanced.test.ts +461 -0
  128. package/test/integration/{plugin-routing.test.ts → routing/plugin-routing-core.test.ts} +10 -404
  129. package/test/integration/{routing-stage-bug-021.test.ts → routing/routing-stage-bug-021.test.ts} +8 -7
  130. package/test/integration/{routing-stage-greenfield.test.ts → routing/routing-stage-greenfield.test.ts} +7 -6
  131. package/test/integration/{tdd-cleanup.test.ts → tdd/tdd-cleanup.test.ts} +1 -1
  132. package/test/integration/tdd/tdd-orchestrator-core.test.ts +565 -0
  133. package/test/integration/tdd/tdd-orchestrator-failureCategory.test.ts +355 -0
  134. package/test/integration/tdd/tdd-orchestrator-fallback.test.ts +311 -0
  135. package/test/integration/tdd/tdd-orchestrator-lite.test.ts +289 -0
  136. package/test/integration/tdd/tdd-orchestrator-prompts.test.ts +260 -0
  137. package/test/integration/tdd/tdd-orchestrator-verdict.test.ts +536 -0
  138. package/test/integration/tmp/headless-test/test.jsonl +30 -0
  139. package/test/integration/{test-scanner.test.ts → verification/test-scanner.test.ts} +1 -1
  140. package/test/integration/{verification-asset-check.test.ts → verification/verification-asset-check.test.ts} +3 -2
  141. package/test/unit/acceptance.test.ts +1 -0
  142. package/test/unit/agent-stderr-capture.test.ts +1 -0
  143. package/test/unit/agents/claude.test.ts +107 -0
  144. package/test/unit/analyze-classifier.test.ts +1 -0
  145. package/test/unit/auto-detect.test.ts +1 -0
  146. package/test/unit/cli-status.test.ts +1 -0
  147. package/test/unit/commands/common.test.ts +1 -0
  148. package/test/unit/commands/logs.test.ts +1 -0
  149. package/test/unit/commands/unlock.test.ts +1 -0
  150. package/test/unit/config/defaults.test.ts +1 -0
  151. package/test/unit/config/regression-gate-schema.test.ts +1 -0
  152. package/test/unit/config/smart-runner-flag.test.ts +1 -0
  153. package/test/unit/constitution-generators.test.ts +1 -0
  154. package/test/unit/constitution.test.ts +1 -0
  155. package/test/unit/context/context-autodetect.test.ts +297 -0
  156. package/test/unit/context/context-build.test.ts +575 -0
  157. package/test/unit/context/context-coverage.test.ts +236 -0
  158. package/test/unit/context/context-error.test.ts +93 -0
  159. package/test/unit/context/context-estimate-tokens.test.ts +201 -0
  160. package/test/unit/context/context-format.test.ts +302 -0
  161. package/test/unit/context/context-isolation.test.ts +267 -0
  162. package/test/unit/context/context-sort.test.ts +93 -0
  163. package/test/unit/context/context-story.test.ts +108 -0
  164. package/test/{context → unit/context}/prior-failures.test.ts +5 -4
  165. package/test/unit/context.test.ts +7 -3
  166. package/test/unit/crash-recovery.test.ts +1 -0
  167. package/test/unit/escalation.test.ts +1 -0
  168. package/test/unit/execution/lifecycle/run-completion.test.ts +1 -0
  169. package/test/unit/execution/lifecycle/run-regression.test.ts +2 -0
  170. package/test/{execution → unit/execution}/pid-registry.test.ts +2 -1
  171. package/test/{execution → unit/execution}/structured-failure.test.ts +3 -2
  172. package/test/unit/execution-logging-stderr.test.ts +1 -0
  173. package/test/unit/execution-stage.test.ts +1 -0
  174. package/test/unit/fix-generator.test.ts +1 -0
  175. package/test/unit/greenfield.test.ts +1 -0
  176. package/test/unit/interaction/human-review-trigger.test.ts +1 -0
  177. package/test/unit/interaction-network-failures.test.ts +1 -0
  178. package/test/unit/interaction-plugins.test.ts +1 -0
  179. package/test/unit/logging/formatter.test.ts +1 -0
  180. package/test/unit/merge.test.ts +1 -0
  181. package/test/unit/pipeline/event-bus.test.ts +105 -0
  182. package/test/unit/pipeline/routing-partial-override.test.ts +1 -0
  183. package/test/unit/pipeline/runner-retry.test.ts +89 -0
  184. package/test/unit/pipeline/stages/autofix.test.ts +97 -0
  185. package/test/unit/pipeline/stages/rectify.test.ts +101 -0
  186. package/test/unit/pipeline/stages/regression-stage.test.ts +69 -0
  187. package/test/unit/pipeline/stages/verify.test.ts +1 -0
  188. package/test/unit/pipeline/subscribers/hooks.test.ts +45 -0
  189. package/test/unit/pipeline/subscribers/interaction.test.ts +31 -0
  190. package/test/unit/pipeline/subscribers/reporters.test.ts +90 -0
  191. package/test/unit/pipeline/verify-smart-runner.test.ts +2 -1
  192. package/test/unit/prd-auto-default.test.ts +3 -2
  193. package/test/unit/prd-failure-category.test.ts +1 -0
  194. package/test/unit/prd-get-next-story.test.ts +1 -0
  195. package/test/unit/precheck-checks.test.ts +1 -0
  196. package/test/unit/precheck-story-size-gate.test.ts +1 -0
  197. package/test/unit/precheck-types.test.ts +1 -0
  198. package/test/unit/prompts.test.ts +1 -0
  199. package/test/unit/rectification.test.ts +2 -1
  200. package/test/unit/registry.test.ts +1 -0
  201. package/test/unit/routing/routing-stability.test.ts +2 -1
  202. package/test/unit/routing/strategies/llm.test.ts +251 -0
  203. package/test/unit/routing-advanced.test.ts +313 -0
  204. package/test/unit/routing-core.test.ts +341 -0
  205. package/test/unit/routing-strategies.test.ts +442 -0
  206. package/test/unit/storyid-events.test.ts +1 -0
  207. package/test/{ui → unit/ui}/tui-controls.test.ts +8 -7
  208. package/test/{ui → unit/ui}/tui-cost-and-pty.test.ts +4 -3
  209. package/test/{ui → unit/ui}/tui-layout.test.ts +5 -4
  210. package/test/{ui → unit/ui}/tui-stories.test.ts +5 -4
  211. package/test/unit/{isolation.test.ts → unit-isolation.test.ts} +1 -0
  212. package/test/unit/{helpers.test.ts → utils-helpers.test.ts} +1 -0
  213. package/test/unit/verdict.test.ts +1 -0
  214. package/test/unit/verification/orchestrator-types.test.ts +54 -0
  215. package/test/unit/verification/orchestrator.test.ts +66 -0
  216. package/test/unit/verification/smart-runner-config.test.ts +1 -0
  217. package/test/unit/verification/smart-runner-discovery.test.ts +8 -7
  218. package/test/unit/verification/strategies/acceptance.test.ts +33 -0
  219. package/test/unit/verification/strategies/regression.test.ts +87 -0
  220. package/test/unit/verification/strategies/scoped.test.ts +100 -0
  221. package/test/unit/worktree-manager.test.ts +1 -0
  222. package/src/execution/lifecycle/story-hooks.ts +0 -38
  223. package/src/execution/post-verify.ts +0 -193
  224. package/src/execution/rectification.ts +0 -13
  225. package/src/execution/verification.ts +0 -72
  226. package/test/integration/rectification-flow.test.ts +0 -512
  227. package/test/integration/runner.test.ts +0 -1679
  228. package/test/integration/tdd-orchestrator.test.ts +0 -1762
  229. package/test/unit/execution/post-verify-regression.test.ts +0 -362
  230. package/test/unit/execution/post-verify.test.ts +0 -236
  231. package/test/unit/routing.test.ts +0 -1039
  232. /package/test/{integration → helpers}/helpers.test.ts +0 -0
  233. /package/test/integration/worktree/{merge.test.ts → worktree-merge.test.ts} +0 -0
@@ -0,0 +1,461 @@
1
+ // RE-ARCH: keep
2
+ /**
3
+ * Plugin Routing Integration Tests
4
+ *
5
+ * Tests for US-005: Plugin routing strategies integrate into router chain
6
+ *
7
+ * Acceptance Criteria:
8
+ * 1. Plugin routers are tried before the built-in routing strategy
9
+ * 2. First plugin router that returns a non-null result wins
10
+ * 3. If all plugin routers return null, built-in strategy is used as fallback
11
+ * 4. Plugin routers receive the same story context as built-in routers
12
+ * 5. Router errors are caught and logged; fallback to next router in chain
13
+ */
14
+
15
+ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
16
+ import { DEFAULT_CONFIG } from "../../../src/config";
17
+ import * as loggerModule from "../../../src/logger";
18
+ import { PluginRegistry } from "../../../src/plugins/registry";
19
+ import type { NaxPlugin } from "../../../src/plugins/types";
20
+ import type { UserStory } from "../../../src/prd/types";
21
+ import { buildStrategyChain } from "../../../src/routing/builder";
22
+ import { routeStory } from "../../../src/routing/router";
23
+ import type { RoutingContext, RoutingDecision, RoutingStrategy } from "../../../src/routing/strategy";
24
+
25
+ // ============================================================================
26
+ // Test Helpers
27
+ // ============================================================================
28
+
29
+ function createTestStory(overrides?: Partial<UserStory>): UserStory {
30
+ return {
31
+ id: "US-TEST",
32
+ title: "Test story",
33
+ description: "Test description",
34
+ acceptanceCriteria: ["AC1", "AC2"],
35
+ tags: [],
36
+ dependencies: [],
37
+ status: "pending",
38
+ passes: false,
39
+ escalations: [],
40
+ attempts: 0,
41
+ ...overrides,
42
+ };
43
+ }
44
+
45
+ function createTestContext(overrides?: Partial<RoutingContext>): RoutingContext {
46
+ return {
47
+ config: DEFAULT_CONFIG,
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ function createPluginRouter(name: string, routeFn: RoutingStrategy["route"]): RoutingStrategy {
53
+ return {
54
+ name,
55
+ route: routeFn,
56
+ };
57
+ }
58
+
59
+ function createMockPlugin(pluginName: string, router?: RoutingStrategy): NaxPlugin {
60
+ const plugin: NaxPlugin = {
61
+ name: pluginName,
62
+ version: "1.0.0",
63
+ provides: router ? ["router"] : [],
64
+ extensions: {},
65
+ };
66
+
67
+ if (router) {
68
+ plugin.extensions.router = router;
69
+ }
70
+
71
+ return plugin;
72
+ }
73
+
74
+
75
+ describe("Plugin router error handling", () => {
76
+ test("error in plugin router is caught and next router is tried", async () => {
77
+ const errorRouter = createPluginRouter("error-router", () => {
78
+ throw new Error("Plugin router error");
79
+ });
80
+
81
+ const successRouter = createPluginRouter("success-router", () => ({
82
+ complexity: "simple",
83
+ modelTier: "fast",
84
+ testStrategy: "test-after",
85
+ reasoning: "Success router decision",
86
+ }));
87
+
88
+ const registry = new PluginRegistry([
89
+ createMockPlugin("error-plugin", errorRouter),
90
+ createMockPlugin("success-plugin", successRouter),
91
+ ]);
92
+
93
+ const story = createTestStory();
94
+ const context = createTestContext();
95
+
96
+ const decision = await routeStory(story, context, "/tmp", registry);
97
+
98
+ // Second router should succeed
99
+ expect(decision.reasoning).toBe("Success router decision");
100
+ });
101
+
102
+ test("error in plugin router is logged", async () => {
103
+ const loggedErrors: Array<{ category: string; message: string; data?: unknown }> = [];
104
+
105
+ // Mock logger to capture error logs
106
+ const mockLogger = {
107
+ error: (category: string, message: string, data?: unknown) => {
108
+ loggedErrors.push({ category, message, data });
109
+ },
110
+ info: () => {},
111
+ warn: () => {},
112
+ debug: () => {},
113
+ };
114
+
115
+ spyOn(loggerModule, "getSafeLogger").mockReturnValue(mockLogger as any);
116
+
117
+ const errorRouter = createPluginRouter("error-router", () => {
118
+ throw new Error("Plugin router failed");
119
+ });
120
+
121
+ const fallbackRouter = createPluginRouter("fallback-router", () => ({
122
+ complexity: "simple",
123
+ modelTier: "fast",
124
+ testStrategy: "test-after",
125
+ reasoning: "Fallback decision",
126
+ }));
127
+
128
+ const registry = new PluginRegistry([
129
+ createMockPlugin("error-plugin", errorRouter),
130
+ createMockPlugin("fallback-plugin", fallbackRouter),
131
+ ]);
132
+
133
+ const story = createTestStory();
134
+ const context = createTestContext();
135
+
136
+ await routeStory(story, context, "/tmp", registry);
137
+
138
+ // Verify error was logged
139
+ expect(loggedErrors.length).toBeGreaterThan(0);
140
+ const errorLog = loggedErrors.find(
141
+ (log) => log.message.includes("error-router") || log.message.includes("Plugin router failed"),
142
+ );
143
+ expect(errorLog).toBeDefined();
144
+ });
145
+
146
+ test("multiple router errors are caught and keyword fallback succeeds", async () => {
147
+ const errorRouter1 = createPluginRouter("error-router-1", () => {
148
+ throw new Error("Router 1 error");
149
+ });
150
+
151
+ const errorRouter2 = createPluginRouter("error-router-2", () => {
152
+ throw new Error("Router 2 error");
153
+ });
154
+
155
+ const registry = new PluginRegistry([
156
+ createMockPlugin("error-plugin-1", errorRouter1),
157
+ createMockPlugin("error-plugin-2", errorRouter2),
158
+ ]);
159
+
160
+ const story = createTestStory({
161
+ title: "Simple task",
162
+ description: "Simple description",
163
+ acceptanceCriteria: ["AC1"],
164
+ tags: [],
165
+ });
166
+
167
+ const context = createTestContext();
168
+
169
+ // Should not throw; keyword strategy should succeed
170
+ const decision = await routeStory(story, context, "/tmp", registry);
171
+
172
+ expect(decision.complexity).toBe("simple");
173
+ expect(decision.modelTier).toBe("fast");
174
+ });
175
+
176
+ test("async error in plugin router is caught", async () => {
177
+ const asyncErrorRouter = createPluginRouter("async-error-router", async () => {
178
+ await Bun.sleep(10);
179
+ throw new Error("Async plugin error");
180
+ });
181
+
182
+ const successRouter = createPluginRouter("success-router", () => ({
183
+ complexity: "medium",
184
+ modelTier: "balanced",
185
+ testStrategy: "test-after",
186
+ reasoning: "Success after async error",
187
+ }));
188
+
189
+ const registry = new PluginRegistry([
190
+ createMockPlugin("async-error-plugin", asyncErrorRouter),
191
+ createMockPlugin("success-plugin", successRouter),
192
+ ]);
193
+
194
+ const story = createTestStory();
195
+ const context = createTestContext();
196
+
197
+ const decision = await routeStory(story, context, "/tmp", registry);
198
+
199
+ // Should succeed with second router
200
+ expect(decision.reasoning).toBe("Success after async error");
201
+ });
202
+
203
+ test("error in last plugin router falls back to keyword strategy", async () => {
204
+ const errorRouter = createPluginRouter("error-router", () => {
205
+ throw new Error("Last plugin router error");
206
+ });
207
+
208
+ const registry = new PluginRegistry([createMockPlugin("error-plugin", errorRouter)]);
209
+
210
+ const story = createTestStory({
211
+ title: "Fix typo",
212
+ description: "Fix typo in README",
213
+ acceptanceCriteria: ["Typo fixed"],
214
+ tags: [],
215
+ });
216
+
217
+ const context = createTestContext();
218
+
219
+ const decision = await routeStory(story, context, "/tmp", registry);
220
+
221
+ // Keyword strategy should succeed
222
+ expect(decision.complexity).toBe("simple");
223
+ expect(decision.modelTier).toBe("fast");
224
+ });
225
+
226
+ test("error message includes plugin name for debugging", async () => {
227
+ const loggedErrors: Array<{ category: string; message: string; data?: unknown }> = [];
228
+
229
+ const mockLogger = {
230
+ error: (category: string, message: string, data?: unknown) => {
231
+ loggedErrors.push({ category, message, data });
232
+ },
233
+ info: () => {},
234
+ warn: () => {},
235
+ debug: () => {},
236
+ };
237
+
238
+ spyOn(loggerModule, "getSafeLogger").mockReturnValue(mockLogger as any);
239
+
240
+ const errorRouter = createPluginRouter("my-custom-router", () => {
241
+ throw new Error("Custom error");
242
+ });
243
+
244
+ const successRouter = createPluginRouter("success-router", () => ({
245
+ complexity: "simple",
246
+ modelTier: "fast",
247
+ testStrategy: "test-after",
248
+ reasoning: "Success",
249
+ }));
250
+
251
+ const registry = new PluginRegistry([
252
+ createMockPlugin("custom-plugin", errorRouter),
253
+ createMockPlugin("success-plugin", successRouter),
254
+ ]);
255
+
256
+ const story = createTestStory();
257
+ const context = createTestContext();
258
+
259
+ await routeStory(story, context, "/tmp", registry);
260
+
261
+ // Verify error log includes plugin router name
262
+ const errorLog = loggedErrors.find(
263
+ (log) => log.message.includes("my-custom-router") || log.data?.toString().includes("my-custom-router"),
264
+ );
265
+ expect(errorLog).toBeDefined();
266
+ });
267
+ });
268
+
269
+ // ============================================================================
270
+ // Integration Tests: Real-world scenarios
271
+ // ============================================================================
272
+
273
+ describe("Plugin routing integration scenarios", () => {
274
+ test("premium plugin forces security stories to expert tier", async () => {
275
+ const premiumRouter = createPluginRouter("premium-security-router", (story, context) => {
276
+ if (story.tags.includes("security") || story.tags.includes("auth")) {
277
+ return {
278
+ complexity: "expert",
279
+ modelTier: "powerful",
280
+ testStrategy: "three-session-tdd",
281
+ reasoning: "Premium plugin: security/auth always use expert tier",
282
+ };
283
+ }
284
+ return null;
285
+ });
286
+
287
+ const plugin = createMockPlugin("premium-plugin", premiumRouter);
288
+ const registry = new PluginRegistry([plugin]);
289
+
290
+ // Simple story with security tag
291
+ const story = createTestStory({
292
+ title: "Update login button text",
293
+ description: "Change 'Login' to 'Sign In'",
294
+ acceptanceCriteria: ["Button text updated"],
295
+ tags: ["security", "ui"],
296
+ });
297
+
298
+ const context = createTestContext();
299
+ const decision = await routeStory(story, context, "/tmp", registry);
300
+
301
+ // Plugin should force expert tier despite simple nature
302
+ expect(decision.complexity).toBe("expert");
303
+ expect(decision.modelTier).toBe("powerful");
304
+ expect(decision.reasoning).toContain("Premium plugin");
305
+ });
306
+
307
+ test("cost-optimization plugin downgrades simple docs to fast tier", async () => {
308
+ const costOptimizationRouter = createPluginRouter("cost-optimization-router", (story, context) => {
309
+ if (story.tags.includes("docs") && story.acceptanceCriteria.length <= 2) {
310
+ return {
311
+ complexity: "simple",
312
+ modelTier: "fast",
313
+ testStrategy: "test-after",
314
+ reasoning: "Cost optimization: simple docs use fast tier",
315
+ };
316
+ }
317
+ return null;
318
+ });
319
+
320
+ const plugin = createMockPlugin("cost-optimization-plugin", costOptimizationRouter);
321
+ const registry = new PluginRegistry([plugin]);
322
+
323
+ const story = createTestStory({
324
+ title: "Update API documentation",
325
+ description: "Add examples to API docs",
326
+ acceptanceCriteria: ["Examples added"],
327
+ tags: ["docs"],
328
+ });
329
+
330
+ const context = createTestContext();
331
+ const decision = await routeStory(story, context, "/tmp", registry);
332
+
333
+ expect(decision.modelTier).toBe("fast");
334
+ expect(decision.reasoning).toContain("Cost optimization");
335
+ });
336
+
337
+ test("domain-specific plugin routes database migrations to expert tier", async () => {
338
+ const domainRouter = createPluginRouter("domain-router", (story, context) => {
339
+ const text = [story.title, story.description, ...story.tags].join(" ").toLowerCase();
340
+ if (text.includes("migration") || text.includes("database") || text.includes("schema")) {
341
+ return {
342
+ complexity: "expert",
343
+ modelTier: "powerful",
344
+ testStrategy: "three-session-tdd",
345
+ reasoning: "Domain-specific: database changes require expert review",
346
+ };
347
+ }
348
+ return null;
349
+ });
350
+
351
+ const plugin = createMockPlugin("domain-plugin", domainRouter);
352
+ const registry = new PluginRegistry([plugin]);
353
+
354
+ const story = createTestStory({
355
+ title: "Add user_email column",
356
+ description: "Add email column to users table migration",
357
+ acceptanceCriteria: ["Column added", "Migration tested"],
358
+ tags: ["database"],
359
+ });
360
+
361
+ const context = createTestContext();
362
+ const decision = await routeStory(story, context, "/tmp", registry);
363
+
364
+ expect(decision.complexity).toBe("expert");
365
+ expect(decision.reasoning).toContain("Domain-specific");
366
+ });
367
+
368
+ test("multiple plugins: first matching plugin wins", async () => {
369
+ const securityRouter = createPluginRouter("security-router", (story) => {
370
+ if (story.tags.includes("security")) {
371
+ return {
372
+ complexity: "expert",
373
+ modelTier: "powerful",
374
+ testStrategy: "three-session-tdd",
375
+ reasoning: "Security plugin decision",
376
+ };
377
+ }
378
+ return null;
379
+ });
380
+
381
+ const uiRouter = createPluginRouter("ui-router", (story) => {
382
+ if (story.tags.includes("ui")) {
383
+ return {
384
+ complexity: "simple",
385
+ modelTier: "fast",
386
+ testStrategy: "three-session-tdd-lite",
387
+ reasoning: "UI plugin decision",
388
+ };
389
+ }
390
+ return null;
391
+ });
392
+
393
+ const registry = new PluginRegistry([
394
+ createMockPlugin("security-plugin", securityRouter),
395
+ createMockPlugin("ui-plugin", uiRouter),
396
+ ]);
397
+
398
+ // Story with both tags
399
+ const story = createTestStory({
400
+ title: "Update security settings UI",
401
+ description: "Redesign security settings page",
402
+ acceptanceCriteria: ["UI updated", "Settings work"],
403
+ tags: ["security", "ui"],
404
+ });
405
+
406
+ const context = createTestContext();
407
+ const decision = await routeStory(story, context, "/tmp", registry);
408
+
409
+ // Security plugin is first, so it should win
410
+ expect(decision.reasoning).toBe("Security plugin decision");
411
+ expect(decision.complexity).toBe("expert");
412
+ });
413
+
414
+ test("plugin router can delegate based on conditional logic", async () => {
415
+ const conditionalRouter = createPluginRouter("conditional-router", (story, context) => {
416
+ // Only handle stories with "critical" tag
417
+ if (story.tags.includes("critical")) {
418
+ return {
419
+ complexity: "expert",
420
+ modelTier: "powerful",
421
+ testStrategy: "three-session-tdd",
422
+ reasoning: "Critical tag forces expert tier",
423
+ };
424
+ }
425
+ // Delegate all other stories to built-in strategy
426
+ return null;
427
+ });
428
+
429
+ const plugin = createMockPlugin("conditional-plugin", conditionalRouter);
430
+ const registry = new PluginRegistry([plugin]);
431
+
432
+ // Non-critical story
433
+ const normalStory = createTestStory({
434
+ title: "Add button",
435
+ description: "Add submit button",
436
+ acceptanceCriteria: ["Button added"],
437
+ tags: ["ui"],
438
+ });
439
+
440
+ const context = createTestContext();
441
+ const normalDecision = await routeStory(normalStory, context, "/tmp", registry);
442
+
443
+ // Should fall back to keyword strategy
444
+ expect(normalDecision.complexity).toBe("simple");
445
+ expect(normalDecision.modelTier).toBe("fast");
446
+
447
+ // Critical story
448
+ const criticalStory = createTestStory({
449
+ title: "Add button",
450
+ description: "Add submit button",
451
+ acceptanceCriteria: ["Button added"],
452
+ tags: ["ui", "critical"],
453
+ });
454
+
455
+ const criticalDecision = await routeStory(criticalStory, context, "/tmp", registry);
456
+
457
+ // Plugin should handle it
458
+ expect(criticalDecision.complexity).toBe("expert");
459
+ expect(criticalDecision.reasoning).toContain("Critical tag");
460
+ });
461
+ });