@nathapp/nax 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitlab-ci.yml +96 -0
- package/BRIEF.md +140 -0
- package/CHANGELOG.md +60 -0
- package/CLAUDE.md +159 -0
- package/README.md +373 -0
- package/US-007-IMPLEMENTATION.md +139 -0
- package/bin/nax.ts +930 -0
- package/biome.json +14 -0
- package/bun.lock +168 -0
- package/bunfig.toml +11 -0
- package/docs/20260216-fix-plan-context-review.md +56 -0
- package/docs/20260216-relentless-vs-ngent-comparison.md +208 -0
- package/docs/20260216-v02-plan.md +136 -0
- package/docs/20260216-v02-review.md +685 -0
- package/docs/20260217-dogfood-findings.md +56 -0
- package/docs/20260217-p2-plus-plan.md +117 -0
- package/docs/20260217-partial-fixes-plan.md +62 -0
- package/docs/20260217-plan-analyze-spec.md +117 -0
- package/docs/20260217-post-impl-review.md +1137 -0
- package/docs/20260217-quick-wins-plan.md +66 -0
- package/docs/20260217-split-runner-plan.md +75 -0
- package/docs/20260217-v03-impl-plan.md +80 -0
- package/docs/20260217-v03-post-impl-review.md +589 -0
- package/docs/20260217-v04-impl-plan.md +86 -0
- package/docs/20260217-v05-post-impl-review.md +850 -0
- package/docs/20260217-v06-post-impl-review.md +817 -0
- package/docs/20260218-adr003-port-plan.md +151 -0
- package/docs/20260218-review-adr003-verification.md +175 -0
- package/docs/20260219-fix-plan-bug16-19.md +79 -0
- package/docs/20260219-fix-plan-bug20-22.md +114 -0
- package/docs/20260219-plan-llm-routing.md +116 -0
- package/docs/20260219-review-bug20-22-fixes.md +135 -0
- package/docs/20260219-routing-baseline-keyword.md +63 -0
- package/docs/20260220-plan-structured-logging-p1.md +80 -0
- package/docs/20260220-plan-structured-logging-p2.md +37 -0
- package/docs/20260220-review-llm-routing.md +180 -0
- package/docs/20260220-review-post-fix-llm-routing.md +70 -0
- package/docs/20260221-fix-plan-relevantfiles-split.md +101 -0
- package/docs/20260221-fix-plan-routing-mode.md +125 -0
- package/docs/20260221-review-v0.9-implementation.md +379 -0
- package/docs/20260222-fix-plan-v091-routing-isolation.md +197 -0
- package/docs/20260223-fix-plan-prompt-audit.md +62 -0
- package/docs/20260224-nax-roadmap-phases.md +189 -0
- package/docs/20260225-phase2-llm-service-layer.md +401 -0
- package/docs/20260225-review-v0.10.1.md +187 -0
- package/docs/20260303-v010-implementation-plan.md +165 -0
- package/docs/CLAUDE.md.bak +191 -0
- package/docs/ROADMAP.md +165 -0
- package/docs/SPEC-rectification.md +0 -0
- package/docs/SPEC.md +324 -0
- package/docs/US-001-plugin-loading-verification.md +152 -0
- package/docs/architecture-analysis.md +1076 -0
- package/docs/bugs/BUG-21-escalation-null-attempts.md +48 -0
- package/docs/bugs-from-dogfood-run-c.md +243 -0
- package/docs/code-review-20260228.md +612 -0
- package/docs/code-review-v0.15.0.md +629 -0
- package/docs/hook-lifecycle-test-plan.md +149 -0
- package/docs/releases/v0.11.0-and-earlier.md +20 -0
- package/docs/releases/v0.12.0.md +15 -0
- package/docs/releases/v0.13.0.md +14 -0
- package/docs/releases/v0.14.0.md +20 -0
- package/docs/releases/v0.14.1.md +36 -0
- package/docs/releases/v0.14.2.md +51 -0
- package/docs/releases/v0.14.3.md +174 -0
- package/docs/releases/v0.14.4.md +94 -0
- package/docs/releases/v0.15.0.md +502 -0
- package/docs/releases/v0.15.1.md +170 -0
- package/docs/releases/v0.15.3.md +193 -0
- package/docs/specs/status-file-v0.10.1.md +812 -0
- package/docs/v0.10-global-config.md +206 -0
- package/docs/v0.10-plugin-system.md +415 -0
- package/docs/v0.10-prompt-optimizer.md +234 -0
- package/docs/v0.3-spec.md +244 -0
- package/docs/v0.4-spec.md +140 -0
- package/docs/v0.5-spec.md +237 -0
- package/docs/v0.6-spec.md +371 -0
- package/docs/v0.7-spec.md +177 -0
- package/docs/v0.8-llm-routing.md +206 -0
- package/docs/v0.8-structured-logging.md +132 -0
- package/docs/v0.9.3-prompt-audit.md +112 -0
- package/examples/plugins/console-reporter/index.test.ts +207 -0
- package/examples/plugins/console-reporter/index.ts +110 -0
- package/nax/config.json +147 -0
- package/nax/features/bugfix-v0171/prd.json +52 -0
- package/nax/features/config-management/prd.json +108 -0
- package/nax/features/config-management/progress.txt +5 -0
- package/nax/features/diagnose/acceptance.test.ts +412 -0
- package/nax/features/diagnose/prd.json +41 -0
- package/nax/features/orchestration-fixes/prd.json +89 -0
- package/nax/features/orchestration-fixes/progress.txt +1 -0
- package/nax/features/plugin-integration/US-007-VERIFICATION.md +259 -0
- package/nax/features/plugin-integration/prd.json +208 -0
- package/nax/features/plugin-integration/progress.txt +5 -0
- package/nax/features/precheck/prd.json +205 -0
- package/nax/features/precheck/progress.txt +15 -0
- package/nax/features/structured-logging/prd.json +199 -0
- package/nax/features/unlock/prd.json +36 -0
- package/package.json +47 -0
- package/src/acceptance/fix-generator.ts +348 -0
- package/src/acceptance/generator.ts +282 -0
- package/src/acceptance/index.ts +30 -0
- package/src/acceptance/types.ts +79 -0
- package/src/agents/claude-decompose.ts +169 -0
- package/src/agents/claude-plan.ts +139 -0
- package/src/agents/claude.ts +324 -0
- package/src/agents/cost.ts +268 -0
- package/src/agents/index.ts +13 -0
- package/src/agents/registry.ts +48 -0
- package/src/agents/types-extended.ts +133 -0
- package/src/agents/types.ts +113 -0
- package/src/agents/validation.ts +69 -0
- package/src/analyze/classifier.ts +305 -0
- package/src/analyze/index.ts +16 -0
- package/src/analyze/scanner.ts +175 -0
- package/src/analyze/types.ts +51 -0
- package/src/cli/accept.ts +108 -0
- package/src/cli/analyze-parser.ts +284 -0
- package/src/cli/analyze.ts +207 -0
- package/src/cli/config.ts +561 -0
- package/src/cli/constitution.ts +109 -0
- package/src/cli/diagnose-analysis.ts +159 -0
- package/src/cli/diagnose-formatter.ts +87 -0
- package/src/cli/diagnose.ts +203 -0
- package/src/cli/generate.ts +127 -0
- package/src/cli/index.ts +37 -0
- package/src/cli/init.ts +188 -0
- package/src/cli/interact.ts +295 -0
- package/src/cli/plan.ts +198 -0
- package/src/cli/plugins.ts +111 -0
- package/src/cli/prompts.ts +295 -0
- package/src/cli/runs.ts +174 -0
- package/src/cli/status-cost.ts +151 -0
- package/src/cli/status-features.ts +338 -0
- package/src/cli/status.ts +13 -0
- package/src/commands/common.ts +171 -0
- package/src/commands/diagnose.ts +17 -0
- package/src/commands/index.ts +8 -0
- package/src/commands/logs.ts +384 -0
- package/src/commands/precheck.ts +86 -0
- package/src/commands/unlock.ts +96 -0
- package/src/config/defaults.ts +160 -0
- package/src/config/index.ts +22 -0
- package/src/config/loader.ts +121 -0
- package/src/config/merger.ts +147 -0
- package/src/config/path-security.ts +121 -0
- package/src/config/paths.ts +27 -0
- package/src/config/schema.ts +56 -0
- package/src/config/schemas.ts +286 -0
- package/src/config/types.ts +423 -0
- package/src/config/validate.ts +103 -0
- package/src/constitution/generator.ts +191 -0
- package/src/constitution/generators/aider.ts +41 -0
- package/src/constitution/generators/claude.ts +35 -0
- package/src/constitution/generators/cursor.ts +36 -0
- package/src/constitution/generators/opencode.ts +38 -0
- package/src/constitution/generators/types.ts +33 -0
- package/src/constitution/generators/windsurf.ts +36 -0
- package/src/constitution/index.ts +10 -0
- package/src/constitution/loader.ts +133 -0
- package/src/constitution/types.ts +31 -0
- package/src/context/auto-detect.ts +227 -0
- package/src/context/builder.ts +246 -0
- package/src/context/elements.ts +83 -0
- package/src/context/formatter.ts +107 -0
- package/src/context/generator.ts +129 -0
- package/src/context/generators/aider.ts +34 -0
- package/src/context/generators/claude.ts +28 -0
- package/src/context/generators/cursor.ts +28 -0
- package/src/context/generators/opencode.ts +30 -0
- package/src/context/generators/windsurf.ts +28 -0
- package/src/context/greenfield.ts +114 -0
- package/src/context/index.ts +33 -0
- package/src/context/injector.ts +279 -0
- package/src/context/test-scanner.ts +370 -0
- package/src/context/types.ts +98 -0
- package/src/errors.ts +67 -0
- package/src/execution/batching.ts +157 -0
- package/src/execution/crash-recovery.ts +373 -0
- package/src/execution/escalation/escalation.ts +44 -0
- package/src/execution/escalation/index.ts +13 -0
- package/src/execution/escalation/tier-escalation.ts +295 -0
- package/src/execution/escalation/tier-outcome.ts +158 -0
- package/src/execution/helpers.ts +38 -0
- package/src/execution/index.ts +45 -0
- package/src/execution/lifecycle/acceptance-loop.ts +272 -0
- package/src/execution/lifecycle/headless-formatter.ts +85 -0
- package/src/execution/lifecycle/index.ts +12 -0
- package/src/execution/lifecycle/parallel-lifecycle.ts +101 -0
- package/src/execution/lifecycle/precheck-runner.ts +140 -0
- package/src/execution/lifecycle/run-cleanup.ts +81 -0
- package/src/execution/lifecycle/run-completion.ts +129 -0
- package/src/execution/lifecycle/run-initialization.ts +141 -0
- package/src/execution/lifecycle/run-lifecycle.ts +312 -0
- package/src/execution/lifecycle/run-setup.ts +204 -0
- package/src/execution/lifecycle/story-hooks.ts +38 -0
- package/src/execution/lifecycle/story-size-prompts.ts +123 -0
- package/src/execution/lock.ts +115 -0
- package/src/execution/parallel-executor.ts +216 -0
- package/src/execution/parallel.ts +400 -0
- package/src/execution/pid-registry.ts +280 -0
- package/src/execution/pipeline-result-handler.ts +388 -0
- package/src/execution/post-verify-rectification.ts +188 -0
- package/src/execution/post-verify.ts +274 -0
- package/src/execution/progress.ts +25 -0
- package/src/execution/prompts.ts +127 -0
- package/src/execution/queue-handler.ts +109 -0
- package/src/execution/rectification.ts +13 -0
- package/src/execution/runner.ts +377 -0
- package/src/execution/sequential-executor.ts +388 -0
- package/src/execution/status-file.ts +264 -0
- package/src/execution/status-writer.ts +139 -0
- package/src/execution/story-context.ts +229 -0
- package/src/execution/test-output-parser.ts +14 -0
- package/src/execution/verification.ts +72 -0
- package/src/hooks/index.ts +2 -0
- package/src/hooks/runner.ts +286 -0
- package/src/hooks/types.ts +67 -0
- package/src/interaction/chain.ts +154 -0
- package/src/interaction/index.ts +60 -0
- package/src/interaction/init.ts +83 -0
- package/src/interaction/plugins/auto.ts +217 -0
- package/src/interaction/plugins/cli.ts +300 -0
- package/src/interaction/plugins/telegram.ts +384 -0
- package/src/interaction/plugins/webhook.ts +258 -0
- package/src/interaction/state.ts +171 -0
- package/src/interaction/triggers.ts +229 -0
- package/src/interaction/types.ts +163 -0
- package/src/logger/formatters.ts +84 -0
- package/src/logger/index.ts +16 -0
- package/src/logger/logger.ts +298 -0
- package/src/logger/types.ts +48 -0
- package/src/logging/formatter.ts +355 -0
- package/src/logging/index.ts +22 -0
- package/src/logging/types.ts +93 -0
- package/src/metrics/aggregator.ts +190 -0
- package/src/metrics/index.ts +14 -0
- package/src/metrics/tracker.ts +200 -0
- package/src/metrics/types.ts +109 -0
- package/src/optimizer/index.ts +62 -0
- package/src/optimizer/noop.optimizer.ts +24 -0
- package/src/optimizer/rule-based.optimizer.ts +248 -0
- package/src/optimizer/types.ts +53 -0
- package/src/pipeline/events.ts +130 -0
- package/src/pipeline/index.ts +19 -0
- package/src/pipeline/runner.ts +161 -0
- package/src/pipeline/stages/acceptance.ts +197 -0
- package/src/pipeline/stages/completion.ts +99 -0
- package/src/pipeline/stages/constitution.ts +63 -0
- package/src/pipeline/stages/context.ts +117 -0
- package/src/pipeline/stages/execution.ts +194 -0
- package/src/pipeline/stages/index.ts +62 -0
- package/src/pipeline/stages/optimizer.ts +74 -0
- package/src/pipeline/stages/prompt.ts +57 -0
- package/src/pipeline/stages/queue-check.ts +103 -0
- package/src/pipeline/stages/review.ts +181 -0
- package/src/pipeline/stages/routing.ts +81 -0
- package/src/pipeline/stages/verify.ts +100 -0
- package/src/pipeline/types.ts +167 -0
- package/src/plugins/index.ts +31 -0
- package/src/plugins/loader.ts +287 -0
- package/src/plugins/registry.ts +168 -0
- package/src/plugins/types.ts +327 -0
- package/src/plugins/validator.ts +352 -0
- package/src/prd/index.ts +172 -0
- package/src/prd/types.ts +202 -0
- package/src/precheck/checks-blockers.ts +391 -0
- package/src/precheck/checks-warnings.ts +142 -0
- package/src/precheck/checks.ts +30 -0
- package/src/precheck/index.ts +247 -0
- package/src/precheck/story-size-gate.ts +144 -0
- package/src/precheck/types.ts +31 -0
- package/src/queue/index.ts +2 -0
- package/src/queue/manager.ts +254 -0
- package/src/queue/types.ts +54 -0
- package/src/review/index.ts +8 -0
- package/src/review/runner.ts +172 -0
- package/src/review/types.ts +66 -0
- package/src/routing/builder.ts +81 -0
- package/src/routing/chain.ts +74 -0
- package/src/routing/index.ts +16 -0
- package/src/routing/loader.ts +58 -0
- package/src/routing/router.ts +303 -0
- package/src/routing/strategies/adaptive.ts +215 -0
- package/src/routing/strategies/index.ts +8 -0
- package/src/routing/strategies/keyword.ts +163 -0
- package/src/routing/strategies/llm-prompts.ts +209 -0
- package/src/routing/strategies/llm.ts +235 -0
- package/src/routing/strategies/manual.ts +50 -0
- package/src/routing/strategy.ts +99 -0
- package/src/tdd/cleanup.ts +111 -0
- package/src/tdd/index.ts +23 -0
- package/src/tdd/isolation.ts +123 -0
- package/src/tdd/orchestrator.ts +383 -0
- package/src/tdd/prompts.ts +270 -0
- package/src/tdd/rectification-gate.ts +183 -0
- package/src/tdd/session-runner.ts +179 -0
- package/src/tdd/types.ts +81 -0
- package/src/tdd/verdict.ts +271 -0
- package/src/tui/App.tsx +265 -0
- package/src/tui/components/AgentPanel.tsx +75 -0
- package/src/tui/components/CostOverlay.tsx +118 -0
- package/src/tui/components/HelpOverlay.tsx +107 -0
- package/src/tui/components/StatusBar.tsx +63 -0
- package/src/tui/components/StoriesPanel.tsx +177 -0
- package/src/tui/hooks/useKeyboard.ts +142 -0
- package/src/tui/hooks/useLayout.ts +137 -0
- package/src/tui/hooks/usePipelineEvents.ts +183 -0
- package/src/tui/hooks/usePty.ts +194 -0
- package/src/tui/index.tsx +38 -0
- package/src/tui/types.ts +76 -0
- package/src/utils/git.ts +83 -0
- package/src/utils/queue-writer.ts +54 -0
- package/src/verification/executor.ts +235 -0
- package/src/verification/gate.ts +207 -0
- package/src/verification/index.ts +12 -0
- package/src/verification/parser.ts +230 -0
- package/src/verification/rectification.ts +108 -0
- package/src/verification/types.ts +113 -0
- package/src/worktree/dispatcher.ts +65 -0
- package/src/worktree/index.ts +2 -0
- package/src/worktree/manager.ts +187 -0
- package/src/worktree/merge.ts +301 -0
- package/src/worktree/types.ts +4 -0
- package/test/TEST_COVERAGE_US001.md +217 -0
- package/test/TEST_COVERAGE_US003.md +84 -0
- package/test/TEST_COVERAGE_US005.md +86 -0
- package/test/US-002-orchestrator.test.ts +246 -0
- package/test/acceptance/cm-003-default-view.test.ts +194 -0
- package/test/execution/pid-registry.test.ts +240 -0
- package/test/execution/post-verify.test.ts +224 -0
- package/test/helpers/timeout.ts +42 -0
- package/test/integration/US-002-TEST-SUMMARY.md +107 -0
- package/test/integration/US-003-TEST-SUMMARY.md +149 -0
- package/test/integration/US-004-TEST-SUMMARY.md +106 -0
- package/test/integration/US-005-TEST-SUMMARY.md +138 -0
- package/test/integration/US-007-TEST-SUMMARY.md +100 -0
- package/test/integration/agent-validation.test.ts +439 -0
- package/test/integration/analyze-integration.test.ts +261 -0
- package/test/integration/analyze-scanner.test.ts +131 -0
- package/test/integration/cli-config-default-edge-cases.test.ts +222 -0
- package/test/integration/cli-config-default-view.test.ts +229 -0
- package/test/integration/cli-config-diff.test.ts +460 -0
- package/test/integration/cli-config.test.ts +736 -0
- package/test/integration/cli-diagnose.test.ts +592 -0
- package/test/integration/cli-logs.test.ts +314 -0
- package/test/integration/cli-plugins.test.ts +678 -0
- package/test/integration/cli-precheck.test.ts +371 -0
- package/test/integration/cli-run-headless.test.ts +173 -0
- package/test/integration/cli.test.ts +75 -0
- package/test/integration/config/merger.test.ts +465 -0
- package/test/integration/config/paths.test.ts +51 -0
- package/test/integration/config-loader.test.ts +265 -0
- package/test/integration/config.test.ts +444 -0
- package/test/integration/context-integration.test.ts +702 -0
- package/test/integration/context-provider-injection.test.ts +506 -0
- package/test/integration/context-verification-integration.test.ts +295 -0
- package/test/integration/e2e.test.ts +896 -0
- package/test/integration/execution.test.ts +625 -0
- package/test/integration/helpers.test.ts +295 -0
- package/test/integration/hooks.test.ts +361 -0
- package/test/integration/interaction-chain-pipeline.test.ts +464 -0
- package/test/integration/isolation.test.ts +143 -0
- package/test/integration/logger.test.ts +461 -0
- package/test/integration/parallel.test.ts +250 -0
- package/test/integration/path-security.test.ts +173 -0
- package/test/integration/pipeline-acceptance.test.ts +302 -0
- package/test/integration/pipeline-events.test.ts +475 -0
- package/test/integration/pipeline.test.ts +658 -0
- package/test/integration/plan.test.ts +157 -0
- package/test/integration/plugin-routing.test.ts +921 -0
- package/test/integration/plugins/config-integration.test.ts +172 -0
- package/test/integration/plugins/config-resolution.test.ts +522 -0
- package/test/integration/plugins/loader.test.ts +641 -0
- package/test/integration/plugins/registry.test.ts +746 -0
- package/test/integration/plugins/validator.test.ts +563 -0
- package/test/integration/prd-pause.test.ts +205 -0
- package/test/integration/prd-resolvers.test.ts +185 -0
- package/test/integration/precheck-integration.test.ts +468 -0
- package/test/integration/precheck.test.ts +805 -0
- package/test/integration/progress.test.ts +34 -0
- package/test/integration/rectification-flow.test.ts +512 -0
- package/test/integration/reporter-lifecycle.test.ts +860 -0
- package/test/integration/review-config-commands.test.ts +319 -0
- package/test/integration/review-config-schema.test.ts +116 -0
- package/test/integration/review-plugin-integration.test.ts +722 -0
- package/test/integration/review.test.ts +149 -0
- package/test/integration/routing-stage-bug-021.test.ts +274 -0
- package/test/integration/routing-stage-greenfield.test.ts +286 -0
- package/test/integration/runner-config-plugins.test.ts +461 -0
- package/test/integration/runner-fixes.test.ts +399 -0
- package/test/integration/runner-plugin-integration.test.ts +543 -0
- package/test/integration/runner.test.ts +1679 -0
- package/test/integration/s5-greenfield-fallback.test.ts +297 -0
- package/test/integration/status-file-integration.test.ts +325 -0
- package/test/integration/status-file.test.ts +379 -0
- package/test/integration/status-writer.test.ts +345 -0
- package/test/integration/story-id-in-events.test.ts +273 -0
- package/test/integration/tdd-cleanup.test.ts +246 -0
- package/test/integration/tdd-orchestrator.test.ts +1762 -0
- package/test/integration/test-scanner.test.ts +403 -0
- package/test/integration/verification-asset-check.test.ts +142 -0
- package/test/integration/verify-stage.test.ts +275 -0
- package/test/integration/worktree/manager.test.ts +218 -0
- package/test/integration/worktree/merge.test.ts +341 -0
- package/test/manual/logging-formatter-demo.ts +158 -0
- package/test/ui/tui-agent-panel.test.tsx +99 -0
- package/test/ui/tui-controls.test.ts +334 -0
- package/test/ui/tui-cost-and-pty.test.ts +189 -0
- package/test/ui/tui-layout.test.ts +378 -0
- package/test/ui/tui-pty-integration.test.tsx +159 -0
- package/test/ui/tui-stories.test.ts +332 -0
- package/test/unit/acceptance.test.ts +186 -0
- package/test/unit/agent-stderr-capture.test.ts +146 -0
- package/test/unit/analyze-classifier.test.ts +215 -0
- package/test/unit/analyze.test.ts +224 -0
- package/test/unit/auto-detect.test.ts +249 -0
- package/test/unit/cli-status.test.ts +417 -0
- package/test/unit/commands/common.test.ts +320 -0
- package/test/unit/commands/logs.test.ts +416 -0
- package/test/unit/commands/unlock.test.ts +319 -0
- package/test/unit/constitution-generators.test.ts +160 -0
- package/test/unit/constitution.test.ts +209 -0
- package/test/unit/context.test.ts +1722 -0
- package/test/unit/cost.test.ts +231 -0
- package/test/unit/crash-recovery.test.ts +308 -0
- package/test/unit/escalation.test.ts +126 -0
- package/test/unit/execution-logging-stderr.test.ts +156 -0
- package/test/unit/execution-stage.test.ts +122 -0
- package/test/unit/fix-generator.test.ts +275 -0
- package/test/unit/formatters.test.ts +469 -0
- package/test/unit/greenfield.test.ts +179 -0
- package/test/unit/helpers.test.ts +317 -0
- package/test/unit/interaction/human-review-trigger.test.ts +164 -0
- package/test/unit/interaction-network-failures.test.ts +389 -0
- package/test/unit/interaction-plugins.test.ts +164 -0
- package/test/unit/isolation.test.ts +134 -0
- package/test/unit/logging/formatter.test.ts +455 -0
- package/test/unit/merge.test.ts +268 -0
- package/test/unit/metrics.test.ts +276 -0
- package/test/unit/optimizer/noop.optimizer.test.ts +125 -0
- package/test/unit/optimizer/rule-based.optimizer.test.ts +358 -0
- package/test/unit/prd-auto-default.test.ts +290 -0
- package/test/unit/prd-failure-category.test.ts +176 -0
- package/test/unit/prd-get-next-story.test.ts +186 -0
- package/test/unit/precheck-checks.test.ts +840 -0
- package/test/unit/precheck-story-size-gate.test.ts +287 -0
- package/test/unit/precheck-types.test.ts +142 -0
- package/test/unit/prompts.test.ts +475 -0
- package/test/unit/queue.test.ts +237 -0
- package/test/unit/rectification.test.ts +284 -0
- package/test/unit/registry.test.ts +287 -0
- package/test/unit/routing.test.ts +937 -0
- package/test/unit/run-lifecycle.test.ts +140 -0
- package/test/unit/storyid-events.test.ts +224 -0
- package/test/unit/tdd-verdict.test.ts +492 -0
- package/test/unit/test-output-parser.test.ts +377 -0
- package/test/unit/verdict.test.ts +324 -0
- package/test/unit/worktree-manager.test.ts +158 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Routing Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests for US-005: Plugin routing strategies integrate into router chain
|
|
5
|
+
*
|
|
6
|
+
* Acceptance Criteria:
|
|
7
|
+
* 1. Plugin routers are tried before the built-in routing strategy
|
|
8
|
+
* 2. First plugin router that returns a non-null result wins
|
|
9
|
+
* 3. If all plugin routers return null, built-in strategy is used as fallback
|
|
10
|
+
* 4. Plugin routers receive the same story context as built-in routers
|
|
11
|
+
* 5. Router errors are caught and logged; fallback to next router in chain
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:test";
|
|
15
|
+
import { DEFAULT_CONFIG } from "../../src/config";
|
|
16
|
+
import * as loggerModule from "../../src/logger";
|
|
17
|
+
import { PluginRegistry } from "../../src/plugins/registry";
|
|
18
|
+
import type { NaxPlugin } from "../../src/plugins/types";
|
|
19
|
+
import type { UserStory } from "../../src/prd/types";
|
|
20
|
+
import { buildStrategyChain } from "../../src/routing/builder";
|
|
21
|
+
import { routeStory } from "../../src/routing/router";
|
|
22
|
+
import type { RoutingContext, RoutingDecision, RoutingStrategy } from "../../src/routing/strategy";
|
|
23
|
+
|
|
24
|
+
// ============================================================================
|
|
25
|
+
// Test Helpers
|
|
26
|
+
// ============================================================================
|
|
27
|
+
|
|
28
|
+
function createTestStory(overrides?: Partial<UserStory>): UserStory {
|
|
29
|
+
return {
|
|
30
|
+
id: "US-TEST",
|
|
31
|
+
title: "Test story",
|
|
32
|
+
description: "Test description",
|
|
33
|
+
acceptanceCriteria: ["AC1", "AC2"],
|
|
34
|
+
tags: [],
|
|
35
|
+
dependencies: [],
|
|
36
|
+
status: "pending",
|
|
37
|
+
passes: false,
|
|
38
|
+
escalations: [],
|
|
39
|
+
attempts: 0,
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createTestContext(overrides?: Partial<RoutingContext>): RoutingContext {
|
|
45
|
+
return {
|
|
46
|
+
config: DEFAULT_CONFIG,
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function createPluginRouter(name: string, routeFn: RoutingStrategy["route"]): RoutingStrategy {
|
|
52
|
+
return {
|
|
53
|
+
name,
|
|
54
|
+
route: routeFn,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createMockPlugin(pluginName: string, router?: RoutingStrategy): NaxPlugin {
|
|
59
|
+
const plugin: NaxPlugin = {
|
|
60
|
+
name: pluginName,
|
|
61
|
+
version: "1.0.0",
|
|
62
|
+
provides: router ? ["router"] : [],
|
|
63
|
+
extensions: {},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
if (router) {
|
|
67
|
+
plugin.extensions.router = router;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return plugin;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// AC1: Plugin routers are tried before the built-in routing strategy
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
describe("Plugin routers chain order", () => {
|
|
78
|
+
test("plugin routers execute before built-in keyword strategy", async () => {
|
|
79
|
+
const executionOrder: string[] = [];
|
|
80
|
+
|
|
81
|
+
const pluginRouter = createPluginRouter("plugin-router", () => {
|
|
82
|
+
executionOrder.push("plugin");
|
|
83
|
+
return null; // Delegate to next strategy
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const plugin = createMockPlugin("test-plugin", pluginRouter);
|
|
87
|
+
const registry = new PluginRegistry([plugin]);
|
|
88
|
+
|
|
89
|
+
// Spy on keyword strategy by tracking when it would be called
|
|
90
|
+
const story = createTestStory({ title: "Simple task" });
|
|
91
|
+
const context = createTestContext();
|
|
92
|
+
|
|
93
|
+
const chain = await buildStrategyChain(DEFAULT_CONFIG, "/tmp", registry);
|
|
94
|
+
await chain.route(story, context);
|
|
95
|
+
|
|
96
|
+
// Plugin router should be called first
|
|
97
|
+
expect(executionOrder[0]).toBe("plugin");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("multiple plugin routers maintain load order", async () => {
|
|
101
|
+
const executionOrder: string[] = [];
|
|
102
|
+
|
|
103
|
+
const router1 = createPluginRouter("plugin-router-1", () => {
|
|
104
|
+
executionOrder.push("plugin-1");
|
|
105
|
+
return null;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const router2 = createPluginRouter("plugin-router-2", () => {
|
|
109
|
+
executionOrder.push("plugin-2");
|
|
110
|
+
return null;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const router3 = createPluginRouter("plugin-router-3", () => {
|
|
114
|
+
executionOrder.push("plugin-3");
|
|
115
|
+
return null;
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const plugin1 = createMockPlugin("plugin-1", router1);
|
|
119
|
+
const plugin2 = createMockPlugin("plugin-2", router2);
|
|
120
|
+
const plugin3 = createMockPlugin("plugin-3", router3);
|
|
121
|
+
|
|
122
|
+
const registry = new PluginRegistry([plugin1, plugin2, plugin3]);
|
|
123
|
+
|
|
124
|
+
const story = createTestStory();
|
|
125
|
+
const context = createTestContext();
|
|
126
|
+
|
|
127
|
+
const chain = await buildStrategyChain(DEFAULT_CONFIG, "/tmp", registry);
|
|
128
|
+
await chain.route(story, context);
|
|
129
|
+
|
|
130
|
+
// Verify order: plugin-1 → plugin-2 → plugin-3
|
|
131
|
+
expect(executionOrder).toEqual(["plugin-1", "plugin-2", "plugin-3"]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("plugin routers are inserted before manual strategy", async () => {
|
|
135
|
+
const executionOrder: string[] = [];
|
|
136
|
+
|
|
137
|
+
const pluginRouter = createPluginRouter("plugin-router", () => {
|
|
138
|
+
executionOrder.push("plugin");
|
|
139
|
+
return null;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const plugin = createMockPlugin("test-plugin", pluginRouter);
|
|
143
|
+
const registry = new PluginRegistry([plugin]);
|
|
144
|
+
|
|
145
|
+
const config = {
|
|
146
|
+
...DEFAULT_CONFIG,
|
|
147
|
+
routing: { strategy: "manual" as const },
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Story without manual routing metadata will cause manual strategy to return null
|
|
151
|
+
const story = createTestStory();
|
|
152
|
+
const context = createTestContext({ config });
|
|
153
|
+
|
|
154
|
+
const chain = await buildStrategyChain(config, "/tmp", registry);
|
|
155
|
+
const strategyNames = chain.getStrategyNames();
|
|
156
|
+
|
|
157
|
+
// Verify: plugin-router → manual → keyword
|
|
158
|
+
expect(strategyNames[0]).toBe("plugin-router");
|
|
159
|
+
expect(strategyNames[1]).toBe("manual");
|
|
160
|
+
expect(strategyNames[2]).toBe("keyword");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("plugin routers are inserted before llm strategy", async () => {
|
|
164
|
+
const pluginRouter = createPluginRouter("plugin-router", () => null);
|
|
165
|
+
const plugin = createMockPlugin("test-plugin", pluginRouter);
|
|
166
|
+
const registry = new PluginRegistry([plugin]);
|
|
167
|
+
|
|
168
|
+
const config = {
|
|
169
|
+
...DEFAULT_CONFIG,
|
|
170
|
+
routing: { strategy: "llm" as const },
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const chain = await buildStrategyChain(config, "/tmp", registry);
|
|
174
|
+
const strategyNames = chain.getStrategyNames();
|
|
175
|
+
|
|
176
|
+
// Verify: plugin-router → llm → keyword
|
|
177
|
+
expect(strategyNames[0]).toBe("plugin-router");
|
|
178
|
+
expect(strategyNames[1]).toBe("llm");
|
|
179
|
+
expect(strategyNames[2]).toBe("keyword");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// AC2: First plugin router that returns a non-null result wins
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
describe("Plugin router precedence", () => {
|
|
188
|
+
test("first plugin router decision is used", async () => {
|
|
189
|
+
const router1 = createPluginRouter("plugin-router-1", () => ({
|
|
190
|
+
complexity: "simple",
|
|
191
|
+
modelTier: "fast",
|
|
192
|
+
testStrategy: "test-after",
|
|
193
|
+
reasoning: "Plugin 1 decision",
|
|
194
|
+
}));
|
|
195
|
+
|
|
196
|
+
const router2 = createPluginRouter("plugin-router-2", () => ({
|
|
197
|
+
complexity: "complex",
|
|
198
|
+
modelTier: "powerful",
|
|
199
|
+
testStrategy: "three-session-tdd",
|
|
200
|
+
reasoning: "Plugin 2 decision (should not be used)",
|
|
201
|
+
}));
|
|
202
|
+
|
|
203
|
+
const plugin1 = createMockPlugin("plugin-1", router1);
|
|
204
|
+
const plugin2 = createMockPlugin("plugin-2", router2);
|
|
205
|
+
const registry = new PluginRegistry([plugin1, plugin2]);
|
|
206
|
+
|
|
207
|
+
const story = createTestStory();
|
|
208
|
+
const context = createTestContext();
|
|
209
|
+
|
|
210
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
211
|
+
|
|
212
|
+
expect(decision.reasoning).toBe("Plugin 1 decision");
|
|
213
|
+
expect(decision.complexity).toBe("simple");
|
|
214
|
+
expect(decision.modelTier).toBe("fast");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("second plugin router is used when first returns null", async () => {
|
|
218
|
+
const router1 = createPluginRouter("plugin-router-1", () => null);
|
|
219
|
+
|
|
220
|
+
const router2 = createPluginRouter("plugin-router-2", () => ({
|
|
221
|
+
complexity: "medium",
|
|
222
|
+
modelTier: "balanced",
|
|
223
|
+
testStrategy: "test-after",
|
|
224
|
+
reasoning: "Plugin 2 decision",
|
|
225
|
+
}));
|
|
226
|
+
|
|
227
|
+
const plugin1 = createMockPlugin("plugin-1", router1);
|
|
228
|
+
const plugin2 = createMockPlugin("plugin-2", router2);
|
|
229
|
+
const registry = new PluginRegistry([plugin1, plugin2]);
|
|
230
|
+
|
|
231
|
+
const story = createTestStory();
|
|
232
|
+
const context = createTestContext();
|
|
233
|
+
|
|
234
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
235
|
+
|
|
236
|
+
expect(decision.reasoning).toBe("Plugin 2 decision");
|
|
237
|
+
expect(decision.complexity).toBe("medium");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("plugin router overrides built-in keyword strategy", async () => {
|
|
241
|
+
const pluginRouter = createPluginRouter("security-router", (story, context) => {
|
|
242
|
+
if (story.tags.includes("security")) {
|
|
243
|
+
return {
|
|
244
|
+
complexity: "expert",
|
|
245
|
+
modelTier: "powerful",
|
|
246
|
+
testStrategy: "three-session-tdd",
|
|
247
|
+
reasoning: "Security-tagged story forced to expert tier by plugin",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return null;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
const plugin = createMockPlugin("security-plugin", pluginRouter);
|
|
254
|
+
const registry = new PluginRegistry([plugin]);
|
|
255
|
+
|
|
256
|
+
// Story that keyword strategy would classify as "simple"
|
|
257
|
+
const story = createTestStory({
|
|
258
|
+
title: "Update button color",
|
|
259
|
+
description: "Change button to red",
|
|
260
|
+
acceptanceCriteria: ["Button is red"],
|
|
261
|
+
tags: ["security"],
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const context = createTestContext();
|
|
265
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
266
|
+
|
|
267
|
+
// Plugin decision wins over keyword strategy
|
|
268
|
+
expect(decision.complexity).toBe("expert");
|
|
269
|
+
expect(decision.modelTier).toBe("powerful");
|
|
270
|
+
expect(decision.reasoning).toContain("plugin");
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("third plugin router is used when first two return null", async () => {
|
|
274
|
+
const router1 = createPluginRouter("plugin-router-1", () => null);
|
|
275
|
+
const router2 = createPluginRouter("plugin-router-2", () => null);
|
|
276
|
+
const router3 = createPluginRouter("plugin-router-3", () => ({
|
|
277
|
+
complexity: "complex",
|
|
278
|
+
modelTier: "powerful",
|
|
279
|
+
testStrategy: "three-session-tdd",
|
|
280
|
+
reasoning: "Plugin 3 decision",
|
|
281
|
+
}));
|
|
282
|
+
|
|
283
|
+
const registry = new PluginRegistry([
|
|
284
|
+
createMockPlugin("plugin-1", router1),
|
|
285
|
+
createMockPlugin("plugin-2", router2),
|
|
286
|
+
createMockPlugin("plugin-3", router3),
|
|
287
|
+
]);
|
|
288
|
+
|
|
289
|
+
const story = createTestStory();
|
|
290
|
+
const context = createTestContext();
|
|
291
|
+
|
|
292
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
293
|
+
|
|
294
|
+
expect(decision.reasoning).toBe("Plugin 3 decision");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// AC3: If all plugin routers return null, built-in strategy is used as fallback
|
|
300
|
+
// ============================================================================
|
|
301
|
+
|
|
302
|
+
describe("Plugin router fallback to built-in strategy", () => {
|
|
303
|
+
test("keyword strategy is used when all plugin routers return null", async () => {
|
|
304
|
+
const router1 = createPluginRouter("plugin-router-1", () => null);
|
|
305
|
+
const router2 = createPluginRouter("plugin-router-2", () => null);
|
|
306
|
+
|
|
307
|
+
const registry = new PluginRegistry([createMockPlugin("plugin-1", router1), createMockPlugin("plugin-2", router2)]);
|
|
308
|
+
|
|
309
|
+
// Simple story that keyword strategy would classify as "simple"
|
|
310
|
+
const story = createTestStory({
|
|
311
|
+
title: "Fix typo",
|
|
312
|
+
description: "Fix typo in README",
|
|
313
|
+
acceptanceCriteria: ["Typo is fixed"],
|
|
314
|
+
tags: [],
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const context = createTestContext();
|
|
318
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
319
|
+
|
|
320
|
+
// Keyword strategy decision (not from plugin)
|
|
321
|
+
expect(decision.complexity).toBe("simple");
|
|
322
|
+
expect(decision.modelTier).toBe("fast");
|
|
323
|
+
expect(decision.testStrategy).toBe("test-after");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("keyword strategy handles complex story when plugins return null", async () => {
|
|
327
|
+
const pluginRouter = createPluginRouter("plugin-router", () => null);
|
|
328
|
+
const registry = new PluginRegistry([createMockPlugin("test-plugin", pluginRouter)]);
|
|
329
|
+
|
|
330
|
+
// Complex security story
|
|
331
|
+
const story = createTestStory({
|
|
332
|
+
title: "Add JWT authentication",
|
|
333
|
+
description: "Implement JWT auth with refresh tokens",
|
|
334
|
+
acceptanceCriteria: ["Token storage", "Refresh logic", "Expiry handling"],
|
|
335
|
+
tags: ["security", "auth"],
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const context = createTestContext();
|
|
339
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
340
|
+
|
|
341
|
+
// Keyword strategy should classify as complex
|
|
342
|
+
expect(decision.complexity).toBe("complex");
|
|
343
|
+
expect(decision.modelTier).toBe("powerful");
|
|
344
|
+
expect(decision.testStrategy).toBe("three-session-tdd");
|
|
345
|
+
expect(decision.reasoning).toContain("security-critical");
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("manual strategy is used as fallback when plugins return null", async () => {
|
|
349
|
+
const pluginRouter = createPluginRouter("plugin-router", () => null);
|
|
350
|
+
const registry = new PluginRegistry([createMockPlugin("test-plugin", pluginRouter)]);
|
|
351
|
+
|
|
352
|
+
const config = {
|
|
353
|
+
...DEFAULT_CONFIG,
|
|
354
|
+
routing: { strategy: "manual" as const },
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const story = createTestStory({
|
|
358
|
+
routing: {
|
|
359
|
+
complexity: "expert",
|
|
360
|
+
modelTier: "powerful",
|
|
361
|
+
testStrategy: "three-session-tdd",
|
|
362
|
+
reasoning: "Manual override",
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
const context = createTestContext({ config });
|
|
367
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
368
|
+
|
|
369
|
+
// Manual strategy decision
|
|
370
|
+
expect(decision.complexity).toBe("expert");
|
|
371
|
+
expect(decision.reasoning).toBe("Manual override");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("empty plugin registry falls back to keyword strategy", async () => {
|
|
375
|
+
const registry = new PluginRegistry([]);
|
|
376
|
+
|
|
377
|
+
const story = createTestStory({
|
|
378
|
+
title: "Update documentation",
|
|
379
|
+
description: "Update README",
|
|
380
|
+
acceptanceCriteria: ["README updated"],
|
|
381
|
+
tags: [],
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const context = createTestContext();
|
|
385
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
386
|
+
|
|
387
|
+
// Keyword strategy decision
|
|
388
|
+
expect(decision.complexity).toBe("simple");
|
|
389
|
+
expect(decision.modelTier).toBe("fast");
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// ============================================================================
|
|
394
|
+
// AC4: Plugin routers receive the same story context as built-in routers
|
|
395
|
+
// ============================================================================
|
|
396
|
+
|
|
397
|
+
describe("Plugin router context", () => {
|
|
398
|
+
test("plugin router receives story object", async () => {
|
|
399
|
+
let receivedStory: UserStory | null = null;
|
|
400
|
+
|
|
401
|
+
const pluginRouter = createPluginRouter("plugin-router", (story) => {
|
|
402
|
+
receivedStory = story;
|
|
403
|
+
return null;
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const plugin = createMockPlugin("test-plugin", pluginRouter);
|
|
407
|
+
const registry = new PluginRegistry([plugin]);
|
|
408
|
+
|
|
409
|
+
const story = createTestStory({
|
|
410
|
+
id: "US-123",
|
|
411
|
+
title: "Test story",
|
|
412
|
+
description: "Test description",
|
|
413
|
+
acceptanceCriteria: ["AC1", "AC2", "AC3"],
|
|
414
|
+
tags: ["ui", "security"],
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const context = createTestContext();
|
|
418
|
+
await routeStory(story, context, "/tmp", registry);
|
|
419
|
+
|
|
420
|
+
// Verify plugin received the story
|
|
421
|
+
expect(receivedStory).not.toBeNull();
|
|
422
|
+
expect(receivedStory?.id).toBe("US-123");
|
|
423
|
+
expect(receivedStory?.title).toBe("Test story");
|
|
424
|
+
expect(receivedStory?.acceptanceCriteria).toEqual(["AC1", "AC2", "AC3"]);
|
|
425
|
+
expect(receivedStory?.tags).toEqual(["ui", "security"]);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("plugin router receives routing context with config", async () => {
|
|
429
|
+
let receivedContext: RoutingContext | null = null;
|
|
430
|
+
|
|
431
|
+
const pluginRouter = createPluginRouter("plugin-router", (story, context) => {
|
|
432
|
+
receivedContext = context;
|
|
433
|
+
return null;
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const plugin = createMockPlugin("test-plugin", pluginRouter);
|
|
437
|
+
const registry = new PluginRegistry([plugin]);
|
|
438
|
+
|
|
439
|
+
const story = createTestStory();
|
|
440
|
+
const context = createTestContext();
|
|
441
|
+
|
|
442
|
+
await routeStory(story, context, "/tmp", registry);
|
|
443
|
+
|
|
444
|
+
// Verify plugin received context with config
|
|
445
|
+
expect(receivedContext).not.toBeNull();
|
|
446
|
+
expect(receivedContext?.config).toBeDefined();
|
|
447
|
+
expect(receivedContext?.config.autoMode).toBeDefined();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("plugin router receives codebase context when available", async () => {
|
|
451
|
+
let receivedContext: RoutingContext | null = null;
|
|
452
|
+
|
|
453
|
+
const pluginRouter = createPluginRouter("plugin-router", (story, context) => {
|
|
454
|
+
receivedContext = context;
|
|
455
|
+
return null;
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const plugin = createMockPlugin("test-plugin", pluginRouter);
|
|
459
|
+
const registry = new PluginRegistry([plugin]);
|
|
460
|
+
|
|
461
|
+
const story = createTestStory();
|
|
462
|
+
const context = createTestContext({
|
|
463
|
+
codebaseContext: "TypeScript project with React frontend",
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
await routeStory(story, context, "/tmp", registry);
|
|
467
|
+
|
|
468
|
+
expect(receivedContext?.codebaseContext).toBe("TypeScript project with React frontend");
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("plugin router receives metrics when available", async () => {
|
|
472
|
+
let receivedContext: RoutingContext | null = null;
|
|
473
|
+
|
|
474
|
+
const pluginRouter = createPluginRouter("plugin-router", (story, context) => {
|
|
475
|
+
receivedContext = context;
|
|
476
|
+
return null;
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const plugin = createMockPlugin("test-plugin", pluginRouter);
|
|
480
|
+
const registry = new PluginRegistry([plugin]);
|
|
481
|
+
|
|
482
|
+
const mockMetrics = {
|
|
483
|
+
totalRuns: 100,
|
|
484
|
+
totalCost: 50.0,
|
|
485
|
+
totalStories: 500,
|
|
486
|
+
firstPassRate: 0.85,
|
|
487
|
+
escalationRate: 0.15,
|
|
488
|
+
avgCostPerStory: 0.1,
|
|
489
|
+
avgCostPerFeature: 1.0,
|
|
490
|
+
modelEfficiency: {},
|
|
491
|
+
complexityAccuracy: {},
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
const story = createTestStory();
|
|
495
|
+
const context = createTestContext({ metrics: mockMetrics });
|
|
496
|
+
|
|
497
|
+
await routeStory(story, context, "/tmp", registry);
|
|
498
|
+
|
|
499
|
+
expect(receivedContext?.metrics).toEqual(mockMetrics);
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
test("multiple plugin routers receive same context", async () => {
|
|
503
|
+
const contexts: RoutingContext[] = [];
|
|
504
|
+
|
|
505
|
+
const router1 = createPluginRouter("plugin-router-1", (story, context) => {
|
|
506
|
+
contexts.push(context);
|
|
507
|
+
return null;
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const router2 = createPluginRouter("plugin-router-2", (story, context) => {
|
|
511
|
+
contexts.push(context);
|
|
512
|
+
return null;
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const registry = new PluginRegistry([createMockPlugin("plugin-1", router1), createMockPlugin("plugin-2", router2)]);
|
|
516
|
+
|
|
517
|
+
const story = createTestStory();
|
|
518
|
+
const context = createTestContext({
|
|
519
|
+
codebaseContext: "Shared context",
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
await routeStory(story, context, "/tmp", registry);
|
|
523
|
+
|
|
524
|
+
// Both plugins should receive the same context
|
|
525
|
+
expect(contexts).toHaveLength(2);
|
|
526
|
+
expect(contexts[0].config).toEqual(contexts[1].config);
|
|
527
|
+
expect(contexts[0].codebaseContext).toBe(contexts[1].codebaseContext);
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// ============================================================================
|
|
532
|
+
// AC5: Router errors are caught and logged; fallback to next router in chain
|
|
533
|
+
// ============================================================================
|
|
534
|
+
|
|
535
|
+
describe("Plugin router error handling", () => {
|
|
536
|
+
test("error in plugin router is caught and next router is tried", async () => {
|
|
537
|
+
const errorRouter = createPluginRouter("error-router", () => {
|
|
538
|
+
throw new Error("Plugin router error");
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const successRouter = createPluginRouter("success-router", () => ({
|
|
542
|
+
complexity: "simple",
|
|
543
|
+
modelTier: "fast",
|
|
544
|
+
testStrategy: "test-after",
|
|
545
|
+
reasoning: "Success router decision",
|
|
546
|
+
}));
|
|
547
|
+
|
|
548
|
+
const registry = new PluginRegistry([
|
|
549
|
+
createMockPlugin("error-plugin", errorRouter),
|
|
550
|
+
createMockPlugin("success-plugin", successRouter),
|
|
551
|
+
]);
|
|
552
|
+
|
|
553
|
+
const story = createTestStory();
|
|
554
|
+
const context = createTestContext();
|
|
555
|
+
|
|
556
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
557
|
+
|
|
558
|
+
// Second router should succeed
|
|
559
|
+
expect(decision.reasoning).toBe("Success router decision");
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("error in plugin router is logged", async () => {
|
|
563
|
+
const loggedErrors: Array<{ category: string; message: string; data?: unknown }> = [];
|
|
564
|
+
|
|
565
|
+
// Mock logger to capture error logs
|
|
566
|
+
const mockLogger = {
|
|
567
|
+
error: (category: string, message: string, data?: unknown) => {
|
|
568
|
+
loggedErrors.push({ category, message, data });
|
|
569
|
+
},
|
|
570
|
+
info: () => {},
|
|
571
|
+
warn: () => {},
|
|
572
|
+
debug: () => {},
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
spyOn(loggerModule, "getSafeLogger").mockReturnValue(mockLogger as any);
|
|
576
|
+
|
|
577
|
+
const errorRouter = createPluginRouter("error-router", () => {
|
|
578
|
+
throw new Error("Plugin router failed");
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const fallbackRouter = createPluginRouter("fallback-router", () => ({
|
|
582
|
+
complexity: "simple",
|
|
583
|
+
modelTier: "fast",
|
|
584
|
+
testStrategy: "test-after",
|
|
585
|
+
reasoning: "Fallback decision",
|
|
586
|
+
}));
|
|
587
|
+
|
|
588
|
+
const registry = new PluginRegistry([
|
|
589
|
+
createMockPlugin("error-plugin", errorRouter),
|
|
590
|
+
createMockPlugin("fallback-plugin", fallbackRouter),
|
|
591
|
+
]);
|
|
592
|
+
|
|
593
|
+
const story = createTestStory();
|
|
594
|
+
const context = createTestContext();
|
|
595
|
+
|
|
596
|
+
await routeStory(story, context, "/tmp", registry);
|
|
597
|
+
|
|
598
|
+
// Verify error was logged
|
|
599
|
+
expect(loggedErrors.length).toBeGreaterThan(0);
|
|
600
|
+
const errorLog = loggedErrors.find(
|
|
601
|
+
(log) => log.message.includes("error-router") || log.message.includes("Plugin router failed"),
|
|
602
|
+
);
|
|
603
|
+
expect(errorLog).toBeDefined();
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("multiple router errors are caught and keyword fallback succeeds", async () => {
|
|
607
|
+
const errorRouter1 = createPluginRouter("error-router-1", () => {
|
|
608
|
+
throw new Error("Router 1 error");
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
const errorRouter2 = createPluginRouter("error-router-2", () => {
|
|
612
|
+
throw new Error("Router 2 error");
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
const registry = new PluginRegistry([
|
|
616
|
+
createMockPlugin("error-plugin-1", errorRouter1),
|
|
617
|
+
createMockPlugin("error-plugin-2", errorRouter2),
|
|
618
|
+
]);
|
|
619
|
+
|
|
620
|
+
const story = createTestStory({
|
|
621
|
+
title: "Simple task",
|
|
622
|
+
description: "Simple description",
|
|
623
|
+
acceptanceCriteria: ["AC1"],
|
|
624
|
+
tags: [],
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
const context = createTestContext();
|
|
628
|
+
|
|
629
|
+
// Should not throw; keyword strategy should succeed
|
|
630
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
631
|
+
|
|
632
|
+
expect(decision.complexity).toBe("simple");
|
|
633
|
+
expect(decision.modelTier).toBe("fast");
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
test("async error in plugin router is caught", async () => {
|
|
637
|
+
const asyncErrorRouter = createPluginRouter("async-error-router", async () => {
|
|
638
|
+
await Bun.sleep(10);
|
|
639
|
+
throw new Error("Async plugin error");
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const successRouter = createPluginRouter("success-router", () => ({
|
|
643
|
+
complexity: "medium",
|
|
644
|
+
modelTier: "balanced",
|
|
645
|
+
testStrategy: "test-after",
|
|
646
|
+
reasoning: "Success after async error",
|
|
647
|
+
}));
|
|
648
|
+
|
|
649
|
+
const registry = new PluginRegistry([
|
|
650
|
+
createMockPlugin("async-error-plugin", asyncErrorRouter),
|
|
651
|
+
createMockPlugin("success-plugin", successRouter),
|
|
652
|
+
]);
|
|
653
|
+
|
|
654
|
+
const story = createTestStory();
|
|
655
|
+
const context = createTestContext();
|
|
656
|
+
|
|
657
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
658
|
+
|
|
659
|
+
// Should succeed with second router
|
|
660
|
+
expect(decision.reasoning).toBe("Success after async error");
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
test("error in last plugin router falls back to keyword strategy", async () => {
|
|
664
|
+
const errorRouter = createPluginRouter("error-router", () => {
|
|
665
|
+
throw new Error("Last plugin router error");
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
const registry = new PluginRegistry([createMockPlugin("error-plugin", errorRouter)]);
|
|
669
|
+
|
|
670
|
+
const story = createTestStory({
|
|
671
|
+
title: "Fix typo",
|
|
672
|
+
description: "Fix typo in README",
|
|
673
|
+
acceptanceCriteria: ["Typo fixed"],
|
|
674
|
+
tags: [],
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
const context = createTestContext();
|
|
678
|
+
|
|
679
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
680
|
+
|
|
681
|
+
// Keyword strategy should succeed
|
|
682
|
+
expect(decision.complexity).toBe("simple");
|
|
683
|
+
expect(decision.modelTier).toBe("fast");
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test("error message includes plugin name for debugging", async () => {
|
|
687
|
+
const loggedErrors: Array<{ category: string; message: string; data?: unknown }> = [];
|
|
688
|
+
|
|
689
|
+
const mockLogger = {
|
|
690
|
+
error: (category: string, message: string, data?: unknown) => {
|
|
691
|
+
loggedErrors.push({ category, message, data });
|
|
692
|
+
},
|
|
693
|
+
info: () => {},
|
|
694
|
+
warn: () => {},
|
|
695
|
+
debug: () => {},
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
spyOn(loggerModule, "getSafeLogger").mockReturnValue(mockLogger as any);
|
|
699
|
+
|
|
700
|
+
const errorRouter = createPluginRouter("my-custom-router", () => {
|
|
701
|
+
throw new Error("Custom error");
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
const successRouter = createPluginRouter("success-router", () => ({
|
|
705
|
+
complexity: "simple",
|
|
706
|
+
modelTier: "fast",
|
|
707
|
+
testStrategy: "test-after",
|
|
708
|
+
reasoning: "Success",
|
|
709
|
+
}));
|
|
710
|
+
|
|
711
|
+
const registry = new PluginRegistry([
|
|
712
|
+
createMockPlugin("custom-plugin", errorRouter),
|
|
713
|
+
createMockPlugin("success-plugin", successRouter),
|
|
714
|
+
]);
|
|
715
|
+
|
|
716
|
+
const story = createTestStory();
|
|
717
|
+
const context = createTestContext();
|
|
718
|
+
|
|
719
|
+
await routeStory(story, context, "/tmp", registry);
|
|
720
|
+
|
|
721
|
+
// Verify error log includes plugin router name
|
|
722
|
+
const errorLog = loggedErrors.find(
|
|
723
|
+
(log) => log.message.includes("my-custom-router") || log.data?.toString().includes("my-custom-router"),
|
|
724
|
+
);
|
|
725
|
+
expect(errorLog).toBeDefined();
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
// ============================================================================
|
|
730
|
+
// Integration Tests: Real-world scenarios
|
|
731
|
+
// ============================================================================
|
|
732
|
+
|
|
733
|
+
describe("Plugin routing integration scenarios", () => {
|
|
734
|
+
test("premium plugin forces security stories to expert tier", async () => {
|
|
735
|
+
const premiumRouter = createPluginRouter("premium-security-router", (story, context) => {
|
|
736
|
+
if (story.tags.includes("security") || story.tags.includes("auth")) {
|
|
737
|
+
return {
|
|
738
|
+
complexity: "expert",
|
|
739
|
+
modelTier: "powerful",
|
|
740
|
+
testStrategy: "three-session-tdd",
|
|
741
|
+
reasoning: "Premium plugin: security/auth always use expert tier",
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
return null;
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
const plugin = createMockPlugin("premium-plugin", premiumRouter);
|
|
748
|
+
const registry = new PluginRegistry([plugin]);
|
|
749
|
+
|
|
750
|
+
// Simple story with security tag
|
|
751
|
+
const story = createTestStory({
|
|
752
|
+
title: "Update login button text",
|
|
753
|
+
description: "Change 'Login' to 'Sign In'",
|
|
754
|
+
acceptanceCriteria: ["Button text updated"],
|
|
755
|
+
tags: ["security", "ui"],
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
const context = createTestContext();
|
|
759
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
760
|
+
|
|
761
|
+
// Plugin should force expert tier despite simple nature
|
|
762
|
+
expect(decision.complexity).toBe("expert");
|
|
763
|
+
expect(decision.modelTier).toBe("powerful");
|
|
764
|
+
expect(decision.reasoning).toContain("Premium plugin");
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
test("cost-optimization plugin downgrades simple docs to fast tier", async () => {
|
|
768
|
+
const costOptimizationRouter = createPluginRouter("cost-optimization-router", (story, context) => {
|
|
769
|
+
if (story.tags.includes("docs") && story.acceptanceCriteria.length <= 2) {
|
|
770
|
+
return {
|
|
771
|
+
complexity: "simple",
|
|
772
|
+
modelTier: "fast",
|
|
773
|
+
testStrategy: "test-after",
|
|
774
|
+
reasoning: "Cost optimization: simple docs use fast tier",
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
return null;
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
const plugin = createMockPlugin("cost-optimization-plugin", costOptimizationRouter);
|
|
781
|
+
const registry = new PluginRegistry([plugin]);
|
|
782
|
+
|
|
783
|
+
const story = createTestStory({
|
|
784
|
+
title: "Update API documentation",
|
|
785
|
+
description: "Add examples to API docs",
|
|
786
|
+
acceptanceCriteria: ["Examples added"],
|
|
787
|
+
tags: ["docs"],
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
const context = createTestContext();
|
|
791
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
792
|
+
|
|
793
|
+
expect(decision.modelTier).toBe("fast");
|
|
794
|
+
expect(decision.reasoning).toContain("Cost optimization");
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test("domain-specific plugin routes database migrations to expert tier", async () => {
|
|
798
|
+
const domainRouter = createPluginRouter("domain-router", (story, context) => {
|
|
799
|
+
const text = [story.title, story.description, ...story.tags].join(" ").toLowerCase();
|
|
800
|
+
if (text.includes("migration") || text.includes("database") || text.includes("schema")) {
|
|
801
|
+
return {
|
|
802
|
+
complexity: "expert",
|
|
803
|
+
modelTier: "powerful",
|
|
804
|
+
testStrategy: "three-session-tdd",
|
|
805
|
+
reasoning: "Domain-specific: database changes require expert review",
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
return null;
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
const plugin = createMockPlugin("domain-plugin", domainRouter);
|
|
812
|
+
const registry = new PluginRegistry([plugin]);
|
|
813
|
+
|
|
814
|
+
const story = createTestStory({
|
|
815
|
+
title: "Add user_email column",
|
|
816
|
+
description: "Add email column to users table migration",
|
|
817
|
+
acceptanceCriteria: ["Column added", "Migration tested"],
|
|
818
|
+
tags: ["database"],
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
const context = createTestContext();
|
|
822
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
823
|
+
|
|
824
|
+
expect(decision.complexity).toBe("expert");
|
|
825
|
+
expect(decision.reasoning).toContain("Domain-specific");
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
test("multiple plugins: first matching plugin wins", async () => {
|
|
829
|
+
const securityRouter = createPluginRouter("security-router", (story) => {
|
|
830
|
+
if (story.tags.includes("security")) {
|
|
831
|
+
return {
|
|
832
|
+
complexity: "expert",
|
|
833
|
+
modelTier: "powerful",
|
|
834
|
+
testStrategy: "three-session-tdd",
|
|
835
|
+
reasoning: "Security plugin decision",
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
return null;
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
const uiRouter = createPluginRouter("ui-router", (story) => {
|
|
842
|
+
if (story.tags.includes("ui")) {
|
|
843
|
+
return {
|
|
844
|
+
complexity: "simple",
|
|
845
|
+
modelTier: "fast",
|
|
846
|
+
testStrategy: "three-session-tdd-lite",
|
|
847
|
+
reasoning: "UI plugin decision",
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
return null;
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
const registry = new PluginRegistry([
|
|
854
|
+
createMockPlugin("security-plugin", securityRouter),
|
|
855
|
+
createMockPlugin("ui-plugin", uiRouter),
|
|
856
|
+
]);
|
|
857
|
+
|
|
858
|
+
// Story with both tags
|
|
859
|
+
const story = createTestStory({
|
|
860
|
+
title: "Update security settings UI",
|
|
861
|
+
description: "Redesign security settings page",
|
|
862
|
+
acceptanceCriteria: ["UI updated", "Settings work"],
|
|
863
|
+
tags: ["security", "ui"],
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
const context = createTestContext();
|
|
867
|
+
const decision = await routeStory(story, context, "/tmp", registry);
|
|
868
|
+
|
|
869
|
+
// Security plugin is first, so it should win
|
|
870
|
+
expect(decision.reasoning).toBe("Security plugin decision");
|
|
871
|
+
expect(decision.complexity).toBe("expert");
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
test("plugin router can delegate based on conditional logic", async () => {
|
|
875
|
+
const conditionalRouter = createPluginRouter("conditional-router", (story, context) => {
|
|
876
|
+
// Only handle stories with "critical" tag
|
|
877
|
+
if (story.tags.includes("critical")) {
|
|
878
|
+
return {
|
|
879
|
+
complexity: "expert",
|
|
880
|
+
modelTier: "powerful",
|
|
881
|
+
testStrategy: "three-session-tdd",
|
|
882
|
+
reasoning: "Critical tag forces expert tier",
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
// Delegate all other stories to built-in strategy
|
|
886
|
+
return null;
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
const plugin = createMockPlugin("conditional-plugin", conditionalRouter);
|
|
890
|
+
const registry = new PluginRegistry([plugin]);
|
|
891
|
+
|
|
892
|
+
// Non-critical story
|
|
893
|
+
const normalStory = createTestStory({
|
|
894
|
+
title: "Add button",
|
|
895
|
+
description: "Add submit button",
|
|
896
|
+
acceptanceCriteria: ["Button added"],
|
|
897
|
+
tags: ["ui"],
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
const context = createTestContext();
|
|
901
|
+
const normalDecision = await routeStory(normalStory, context, "/tmp", registry);
|
|
902
|
+
|
|
903
|
+
// Should fall back to keyword strategy
|
|
904
|
+
expect(normalDecision.complexity).toBe("simple");
|
|
905
|
+
expect(normalDecision.modelTier).toBe("fast");
|
|
906
|
+
|
|
907
|
+
// Critical story
|
|
908
|
+
const criticalStory = createTestStory({
|
|
909
|
+
title: "Add button",
|
|
910
|
+
description: "Add submit button",
|
|
911
|
+
acceptanceCriteria: ["Button added"],
|
|
912
|
+
tags: ["ui", "critical"],
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
const criticalDecision = await routeStory(criticalStory, context, "/tmp", registry);
|
|
916
|
+
|
|
917
|
+
// Plugin should handle it
|
|
918
|
+
expect(criticalDecision.complexity).toBe("expert");
|
|
919
|
+
expect(criticalDecision.reasoning).toContain("Critical tag");
|
|
920
|
+
});
|
|
921
|
+
});
|