@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,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM Routing Prompts & Parsers
|
|
3
|
+
*
|
|
4
|
+
* Extracted from llm.ts: prompt building, response parsing, and validation
|
|
5
|
+
* for LLM-based routing decisions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Complexity, ModelTier, NaxConfig, TestStrategy } from "../../config";
|
|
9
|
+
import type { UserStory } from "../../prd/types";
|
|
10
|
+
import type { RoutingDecision } from "../strategy";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build the routing prompt for a single story.
|
|
14
|
+
*
|
|
15
|
+
* @param story - User story to route
|
|
16
|
+
* @param config - nax configuration
|
|
17
|
+
* @returns Formatted prompt string
|
|
18
|
+
*/
|
|
19
|
+
export function buildRoutingPrompt(story: UserStory, config: NaxConfig): string {
|
|
20
|
+
const { title, description, acceptanceCriteria, tags } = story;
|
|
21
|
+
const criteria = acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join("\n");
|
|
22
|
+
|
|
23
|
+
return `You are a code task router. Given a user story, classify its complexity and select the appropriate execution strategy.
|
|
24
|
+
|
|
25
|
+
## Story
|
|
26
|
+
Title: ${title}
|
|
27
|
+
Description: ${description}
|
|
28
|
+
Acceptance Criteria:
|
|
29
|
+
${criteria}
|
|
30
|
+
Tags: ${tags.join(", ")}
|
|
31
|
+
|
|
32
|
+
## Available Tiers
|
|
33
|
+
- fast: Simple changes, typos, config updates, boilerplate. <30 min of coding.
|
|
34
|
+
- balanced: Standard features, moderate logic, straightforward tests. 30-90 min.
|
|
35
|
+
- powerful: Complex architecture, security-critical, multi-file refactors, novel algorithms. >90 min.
|
|
36
|
+
|
|
37
|
+
## Available Test Strategies
|
|
38
|
+
- test-after: Write implementation first, add tests after. For straightforward work.
|
|
39
|
+
- three-session-tdd: Separate test-writer → implementer → verifier sessions. For complex/critical work where test design matters.
|
|
40
|
+
|
|
41
|
+
## Rules
|
|
42
|
+
- Default to the CHEAPEST option that will succeed.
|
|
43
|
+
- three-session-tdd ONLY when: (a) security/auth logic, (b) complex algorithms, (c) public API contracts that consumers depend on.
|
|
44
|
+
- Simple barrel exports, re-exports, or index files are ALWAYS test-after + fast, regardless of keywords.
|
|
45
|
+
- A story touching many files doesn't automatically mean complex — copy-paste refactors are simple.
|
|
46
|
+
|
|
47
|
+
Respond with ONLY this JSON (no markdown, no explanation):
|
|
48
|
+
{"complexity":"simple|medium|complex|expert","modelTier":"fast|balanced|powerful","testStrategy":"test-after|three-session-tdd","reasoning":"<one line>"}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Build batch routing prompt for multiple stories.
|
|
53
|
+
*
|
|
54
|
+
* @param stories - Array of user stories to route
|
|
55
|
+
* @param config - nax configuration
|
|
56
|
+
* @returns Formatted batch prompt string
|
|
57
|
+
*/
|
|
58
|
+
export function buildBatchPrompt(stories: UserStory[], config: NaxConfig): string {
|
|
59
|
+
const storyBlocks = stories
|
|
60
|
+
.map((story, idx) => {
|
|
61
|
+
const criteria = story.acceptanceCriteria.map((c, i) => ` ${i + 1}. ${c}`).join("\n");
|
|
62
|
+
return `${idx + 1}. ${story.id}: ${story.title}
|
|
63
|
+
Description: ${story.description}
|
|
64
|
+
Acceptance Criteria:
|
|
65
|
+
${criteria}
|
|
66
|
+
Tags: ${story.tags.join(", ")}`;
|
|
67
|
+
})
|
|
68
|
+
.join("\n\n");
|
|
69
|
+
|
|
70
|
+
return `You are a code task router. Given multiple user stories, classify each story's complexity and select the appropriate execution strategy.
|
|
71
|
+
|
|
72
|
+
## Stories
|
|
73
|
+
${storyBlocks}
|
|
74
|
+
|
|
75
|
+
## Available Tiers
|
|
76
|
+
- fast: Simple changes, typos, config updates, boilerplate. <30 min of coding.
|
|
77
|
+
- balanced: Standard features, moderate logic, straightforward tests. 30-90 min.
|
|
78
|
+
- powerful: Complex architecture, security-critical, multi-file refactors, novel algorithms. >90 min.
|
|
79
|
+
|
|
80
|
+
## Available Test Strategies
|
|
81
|
+
- test-after: Write implementation first, add tests after. For straightforward work.
|
|
82
|
+
- three-session-tdd: Separate test-writer → implementer → verifier sessions. For complex/critical work where test design matters.
|
|
83
|
+
|
|
84
|
+
## Rules
|
|
85
|
+
- Default to the CHEAPEST option that will succeed.
|
|
86
|
+
- three-session-tdd ONLY when: (a) security/auth logic, (b) complex algorithms, (c) public API contracts that consumers depend on.
|
|
87
|
+
- Simple barrel exports, re-exports, or index files are ALWAYS test-after + fast, regardless of keywords.
|
|
88
|
+
- A story touching many files doesn't automatically mean complex — copy-paste refactors are simple.
|
|
89
|
+
|
|
90
|
+
Respond with ONLY a JSON array (no markdown, no explanation):
|
|
91
|
+
[{"id":"US-001","complexity":"simple|medium|complex|expert","modelTier":"fast|balanced|powerful","testStrategy":"test-after|three-session-tdd","reasoning":"<one line>"}]`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validate a parsed routing object and return a clean RoutingDecision.
|
|
96
|
+
*
|
|
97
|
+
* @param parsed - Parsed JSON object with routing fields
|
|
98
|
+
* @param config - nax configuration (for modelTier validation)
|
|
99
|
+
* @returns Validated routing decision
|
|
100
|
+
* @throws Error if validation fails
|
|
101
|
+
*/
|
|
102
|
+
export function validateRoutingDecision(parsed: Record<string, unknown>, config: NaxConfig): RoutingDecision {
|
|
103
|
+
// Validate required fields
|
|
104
|
+
if (!parsed.complexity || !parsed.modelTier || !parsed.testStrategy || !parsed.reasoning) {
|
|
105
|
+
throw new Error(`Missing required fields in LLM response: ${JSON.stringify(parsed)}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Validate field values
|
|
109
|
+
const validComplexities: Complexity[] = ["simple", "medium", "complex", "expert"];
|
|
110
|
+
const validTestStrategies: TestStrategy[] = ["test-after", "three-session-tdd"];
|
|
111
|
+
|
|
112
|
+
if (!validComplexities.includes(parsed.complexity as Complexity)) {
|
|
113
|
+
throw new Error(`Invalid complexity: ${parsed.complexity}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!validTestStrategies.includes(parsed.testStrategy as TestStrategy)) {
|
|
117
|
+
throw new Error(`Invalid testStrategy: ${parsed.testStrategy}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Validate modelTier exists in config
|
|
121
|
+
if (!config.models[parsed.modelTier as string]) {
|
|
122
|
+
throw new Error(`Invalid modelTier: ${parsed.modelTier} (not in config.models)`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
complexity: parsed.complexity as Complexity,
|
|
127
|
+
modelTier: parsed.modelTier as ModelTier,
|
|
128
|
+
testStrategy: parsed.testStrategy as TestStrategy,
|
|
129
|
+
reasoning: parsed.reasoning as string,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Strip markdown code fences from LLM output. */
|
|
134
|
+
export function stripCodeFences(text: string): string {
|
|
135
|
+
let result = text.trim();
|
|
136
|
+
if (result.startsWith("```")) {
|
|
137
|
+
const lines = result.split("\n");
|
|
138
|
+
result = lines.slice(1, -1).join("\n").trim();
|
|
139
|
+
}
|
|
140
|
+
if (result.startsWith("json")) {
|
|
141
|
+
result = result.slice(4).trim();
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Parse and validate LLM routing response.
|
|
148
|
+
*
|
|
149
|
+
* @param output - Raw LLM output text
|
|
150
|
+
* @param story - User story being routed (for error context)
|
|
151
|
+
* @param config - nax configuration
|
|
152
|
+
* @returns Validated routing decision
|
|
153
|
+
* @throws Error if JSON parsing or validation fails
|
|
154
|
+
*/
|
|
155
|
+
export function parseRoutingResponse(output: string, story: UserStory, config: NaxConfig): RoutingDecision {
|
|
156
|
+
const jsonText = stripCodeFences(output);
|
|
157
|
+
const parsed = JSON.parse(jsonText);
|
|
158
|
+
return validateRoutingDecision(parsed, config);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Parse batch LLM response into a map of decisions.
|
|
163
|
+
*
|
|
164
|
+
* @param output - Raw LLM output text (JSON array)
|
|
165
|
+
* @param stories - User stories being routed
|
|
166
|
+
* @param config - nax configuration
|
|
167
|
+
* @returns Map of story ID to routing decision
|
|
168
|
+
* @throws Error if JSON parsing or validation fails
|
|
169
|
+
*/
|
|
170
|
+
export function parseBatchResponse(
|
|
171
|
+
output: string,
|
|
172
|
+
stories: UserStory[],
|
|
173
|
+
config: NaxConfig,
|
|
174
|
+
): Map<string, RoutingDecision> {
|
|
175
|
+
// Strip markdown code blocks if present
|
|
176
|
+
let jsonText = output.trim();
|
|
177
|
+
if (jsonText.startsWith("```")) {
|
|
178
|
+
const lines = jsonText.split("\n");
|
|
179
|
+
jsonText = lines.slice(1, -1).join("\n").trim();
|
|
180
|
+
}
|
|
181
|
+
if (jsonText.startsWith("json")) {
|
|
182
|
+
jsonText = jsonText.slice(4).trim();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const parsed = JSON.parse(jsonText);
|
|
186
|
+
|
|
187
|
+
if (!Array.isArray(parsed)) {
|
|
188
|
+
throw new Error("Batch LLM response must be a JSON array");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const decisions = new Map<string, RoutingDecision>();
|
|
192
|
+
|
|
193
|
+
for (const entry of parsed) {
|
|
194
|
+
if (!entry.id) {
|
|
195
|
+
throw new Error("Batch entry missing 'id' field");
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const story = stories.find((s) => s.id === entry.id);
|
|
199
|
+
if (!story) {
|
|
200
|
+
throw new Error(`Batch entry has unknown story ID: ${entry.id}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Validate entry directly (no re-serialization needed)
|
|
204
|
+
const decision = validateRoutingDecision(entry, config);
|
|
205
|
+
decisions.set(entry.id, decision);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return decisions;
|
|
209
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM-Based Routing Strategy
|
|
3
|
+
*
|
|
4
|
+
* Routes stories using an LLM to perform semantic analysis of story requirements.
|
|
5
|
+
* Falls back to keyword strategy on failure. Supports batch mode for efficiency.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { NaxConfig } from "../../config";
|
|
9
|
+
import { resolveModel } from "../../config";
|
|
10
|
+
import { getLogger } from "../../logger";
|
|
11
|
+
import type { UserStory } from "../../prd/types";
|
|
12
|
+
import type { RoutingContext, RoutingDecision, RoutingStrategy } from "../strategy";
|
|
13
|
+
import { keywordStrategy } from "./keyword";
|
|
14
|
+
import { buildBatchPrompt, buildRoutingPrompt, parseBatchResponse, parseRoutingResponse } from "./llm-prompts";
|
|
15
|
+
|
|
16
|
+
// Re-export for backward compatibility
|
|
17
|
+
export {
|
|
18
|
+
buildRoutingPrompt,
|
|
19
|
+
buildBatchPrompt,
|
|
20
|
+
validateRoutingDecision,
|
|
21
|
+
stripCodeFences,
|
|
22
|
+
parseRoutingResponse,
|
|
23
|
+
} from "./llm-prompts";
|
|
24
|
+
|
|
25
|
+
/** Module-level cache for routing decisions (PERF-1 fix: max 100 entries LRU) */
|
|
26
|
+
const cachedDecisions = new Map<string, RoutingDecision>();
|
|
27
|
+
const MAX_CACHE_SIZE = 100;
|
|
28
|
+
|
|
29
|
+
/** Clear the routing cache (for testing or new runs) */
|
|
30
|
+
export function clearCache(): void {
|
|
31
|
+
cachedDecisions.clear();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Get the current cache size (for testing) */
|
|
35
|
+
export function getCacheSize(): number {
|
|
36
|
+
return cachedDecisions.size;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Evict oldest entry when cache is full (LRU) */
|
|
40
|
+
function evictOldest(): void {
|
|
41
|
+
const firstKey = cachedDecisions.keys().next().value;
|
|
42
|
+
if (firstKey !== undefined) {
|
|
43
|
+
cachedDecisions.delete(firstKey);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Call LLM via claude CLI with timeout.
|
|
49
|
+
*
|
|
50
|
+
* @param modelTier - Model tier to use for routing call
|
|
51
|
+
* @param prompt - Prompt to send to LLM
|
|
52
|
+
* @param config - nax configuration
|
|
53
|
+
* @returns LLM response text
|
|
54
|
+
* @throws Error on timeout or spawn failure
|
|
55
|
+
*/
|
|
56
|
+
async function callLlm(modelTier: string, prompt: string, config: NaxConfig): Promise<string> {
|
|
57
|
+
const llmConfig = config.routing.llm;
|
|
58
|
+
const timeoutMs = llmConfig?.timeoutMs ?? 15000;
|
|
59
|
+
|
|
60
|
+
// Resolve model tier to actual model identifier
|
|
61
|
+
const modelEntry = config.models[modelTier];
|
|
62
|
+
if (!modelEntry) {
|
|
63
|
+
throw new Error(`Model tier "${modelTier}" not found in config.models`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const modelDef = resolveModel(modelEntry);
|
|
67
|
+
const modelArg = modelDef.model;
|
|
68
|
+
|
|
69
|
+
// Spawn claude CLI with timeout
|
|
70
|
+
const proc = Bun.spawn(["claude", "-p", prompt, "--model", modelArg], {
|
|
71
|
+
stdout: "pipe",
|
|
72
|
+
stderr: "pipe",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Race between completion and timeout, ensuring cleanup on either path
|
|
76
|
+
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
|
77
|
+
|
|
78
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
79
|
+
timeoutId = setTimeout(() => {
|
|
80
|
+
reject(new Error(`LLM call timeout after ${timeoutMs}ms`));
|
|
81
|
+
}, timeoutMs);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const outputPromise = (async () => {
|
|
85
|
+
const [stdout, stderr] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
|
86
|
+
|
|
87
|
+
const exitCode = await proc.exited;
|
|
88
|
+
if (exitCode !== 0) {
|
|
89
|
+
throw new Error(`claude CLI failed with exit code ${exitCode}: ${stderr}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return stdout.trim();
|
|
93
|
+
})();
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const result = await Promise.race([outputPromise, timeoutPromise]);
|
|
97
|
+
clearTimeout(timeoutId);
|
|
98
|
+
return result;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
clearTimeout(timeoutId);
|
|
101
|
+
proc.kill();
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Route multiple stories in a single batch LLM call.
|
|
108
|
+
*
|
|
109
|
+
* This function pre-populates the cache with routing decisions for all stories.
|
|
110
|
+
* Individual route() calls will then hit the cache.
|
|
111
|
+
*
|
|
112
|
+
* @param stories - Array of user stories to route
|
|
113
|
+
* @param context - Routing context
|
|
114
|
+
* @returns Map of story ID to routing decision
|
|
115
|
+
*/
|
|
116
|
+
export async function routeBatch(stories: UserStory[], context: RoutingContext): Promise<Map<string, RoutingDecision>> {
|
|
117
|
+
const config = context.config;
|
|
118
|
+
const llmConfig = config.routing.llm;
|
|
119
|
+
|
|
120
|
+
if (!llmConfig) {
|
|
121
|
+
throw new Error("LLM routing config not found");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const modelTier = llmConfig.model ?? "fast";
|
|
125
|
+
const prompt = buildBatchPrompt(stories, config);
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const output = await callLlm(modelTier, prompt, config);
|
|
129
|
+
const decisions = parseBatchResponse(output, stories, config);
|
|
130
|
+
|
|
131
|
+
// Populate cache (PERF-1 fix: evict oldest if full)
|
|
132
|
+
if (llmConfig.cacheDecisions) {
|
|
133
|
+
for (const [storyId, decision] of decisions.entries()) {
|
|
134
|
+
if (cachedDecisions.size >= MAX_CACHE_SIZE) {
|
|
135
|
+
evictOldest();
|
|
136
|
+
}
|
|
137
|
+
cachedDecisions.set(storyId, decision);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return decisions;
|
|
142
|
+
} catch (err) {
|
|
143
|
+
throw new Error(`Batch LLM routing failed: ${(err as Error).message}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* LLM-based routing strategy.
|
|
149
|
+
*
|
|
150
|
+
* This strategy:
|
|
151
|
+
* - Checks cache first (if enabled)
|
|
152
|
+
* - Calls LLM with story context to classify complexity
|
|
153
|
+
* - Parses structured JSON response
|
|
154
|
+
* - Maps complexity to model tier and test strategy
|
|
155
|
+
* - Falls back to null (keyword fallback) on any failure
|
|
156
|
+
*/
|
|
157
|
+
export const llmStrategy: RoutingStrategy = {
|
|
158
|
+
name: "llm",
|
|
159
|
+
|
|
160
|
+
async route(story: UserStory, context: RoutingContext): Promise<RoutingDecision | null> {
|
|
161
|
+
const config = context.config;
|
|
162
|
+
const llmConfig = config.routing.llm;
|
|
163
|
+
|
|
164
|
+
if (!llmConfig) {
|
|
165
|
+
return null; // LLM routing not configured
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const mode = llmConfig.mode ?? "hybrid";
|
|
169
|
+
|
|
170
|
+
// Check cache first
|
|
171
|
+
if (llmConfig.cacheDecisions && cachedDecisions.has(story.id)) {
|
|
172
|
+
const cached = cachedDecisions.get(story.id);
|
|
173
|
+
if (!cached) {
|
|
174
|
+
throw new Error(`Cached decision not found for story: ${story.id}`);
|
|
175
|
+
}
|
|
176
|
+
const logger = getLogger();
|
|
177
|
+
logger.debug("routing", "LLM cache hit", {
|
|
178
|
+
storyId: story.id,
|
|
179
|
+
complexity: cached.complexity,
|
|
180
|
+
modelTier: cached.modelTier,
|
|
181
|
+
testStrategy: cached.testStrategy,
|
|
182
|
+
});
|
|
183
|
+
return cached;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// One-shot mode: cache miss -> keyword fallback without new LLM call
|
|
187
|
+
if (mode === "one-shot") {
|
|
188
|
+
const logger = getLogger();
|
|
189
|
+
logger.info("routing", "One-shot mode cache miss, falling back to keyword", {
|
|
190
|
+
storyId: story.id,
|
|
191
|
+
});
|
|
192
|
+
return keywordStrategy.route(story, context);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
const modelTier = llmConfig.model ?? "fast";
|
|
197
|
+
const prompt = buildRoutingPrompt(story, config);
|
|
198
|
+
const output = await callLlm(modelTier, prompt, config);
|
|
199
|
+
const decision = parseRoutingResponse(output, story, config);
|
|
200
|
+
|
|
201
|
+
// Cache decision (PERF-1 fix: evict oldest if full)
|
|
202
|
+
if (llmConfig.cacheDecisions) {
|
|
203
|
+
if (cachedDecisions.size >= MAX_CACHE_SIZE) {
|
|
204
|
+
evictOldest();
|
|
205
|
+
}
|
|
206
|
+
cachedDecisions.set(story.id, decision);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Log decision
|
|
210
|
+
const logger = getLogger();
|
|
211
|
+
logger.info("routing", "LLM classified story", {
|
|
212
|
+
storyId: story.id,
|
|
213
|
+
complexity: decision.complexity,
|
|
214
|
+
modelTier: decision.modelTier,
|
|
215
|
+
testStrategy: decision.testStrategy,
|
|
216
|
+
reasoning: decision.reasoning,
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return decision;
|
|
220
|
+
} catch (err) {
|
|
221
|
+
const logger = getLogger();
|
|
222
|
+
const errorMsg = (err as Error).message;
|
|
223
|
+
logger.warn("routing", "LLM routing failed", { storyId: story.id, error: errorMsg });
|
|
224
|
+
|
|
225
|
+
// Fall back to keyword strategy if configured
|
|
226
|
+
if (llmConfig.fallbackToKeywords) {
|
|
227
|
+
logger.info("routing", "Falling back to keyword strategy", { storyId: story.id });
|
|
228
|
+
return null; // Delegate to next strategy (keyword)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Re-throw if no fallback
|
|
232
|
+
throw err;
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manual Routing Strategy
|
|
3
|
+
*
|
|
4
|
+
* Reads routing decision from story.routing metadata in prd.json.
|
|
5
|
+
* Users can manually specify complexity, modelTier, and testStrategy per story.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { UserStory } from "../../prd/types";
|
|
9
|
+
import type { RoutingContext, RoutingDecision, RoutingStrategy } from "../strategy";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Manual routing strategy.
|
|
13
|
+
*
|
|
14
|
+
* If story.routing is present in prd.json, uses that data directly.
|
|
15
|
+
* Otherwise returns null (delegates to next strategy).
|
|
16
|
+
*
|
|
17
|
+
* Use case: Override routing for specific stories that need manual control.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```json
|
|
21
|
+
* {
|
|
22
|
+
* "id": "US-001",
|
|
23
|
+
* "title": "Critical database migration",
|
|
24
|
+
* "routing": {
|
|
25
|
+
* "complexity": "expert",
|
|
26
|
+
* "modelTier": "powerful",
|
|
27
|
+
* "testStrategy": "three-session-tdd",
|
|
28
|
+
* "reasoning": "Manually specified: critical migration"
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const manualStrategy: RoutingStrategy = {
|
|
34
|
+
name: "manual",
|
|
35
|
+
|
|
36
|
+
route(story: UserStory, _context: RoutingContext): RoutingDecision | null {
|
|
37
|
+
// If story has routing metadata with all required fields, use it
|
|
38
|
+
if (story.routing?.complexity && story.routing.modelTier && story.routing.testStrategy) {
|
|
39
|
+
return {
|
|
40
|
+
complexity: story.routing.complexity,
|
|
41
|
+
modelTier: story.routing.modelTier,
|
|
42
|
+
testStrategy: story.routing.testStrategy,
|
|
43
|
+
reasoning: story.routing.reasoning || "Manual routing from prd.json",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// No manual routing specified, delegate to next strategy
|
|
48
|
+
return null;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routing Strategy Interface
|
|
3
|
+
*
|
|
4
|
+
* Pluggable routing system that allows custom model tier selection logic.
|
|
5
|
+
* Strategies can return null to delegate to the next strategy in the chain.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Complexity, ModelTier, NaxConfig, TestStrategy } from "../config";
|
|
9
|
+
import type { UserStory } from "../prd/types";
|
|
10
|
+
|
|
11
|
+
/** Aggregate metrics (v0.5 Phase 1 — not yet implemented) */
|
|
12
|
+
export interface AggregateMetrics {
|
|
13
|
+
totalRuns: number;
|
|
14
|
+
totalCost: number;
|
|
15
|
+
totalStories: number;
|
|
16
|
+
firstPassRate: number;
|
|
17
|
+
escalationRate: number;
|
|
18
|
+
avgCostPerStory: number;
|
|
19
|
+
avgCostPerFeature: number;
|
|
20
|
+
modelEfficiency: Record<
|
|
21
|
+
string,
|
|
22
|
+
{
|
|
23
|
+
attempts: number;
|
|
24
|
+
successes: number;
|
|
25
|
+
passRate: number;
|
|
26
|
+
avgCost: number;
|
|
27
|
+
totalCost: number;
|
|
28
|
+
}
|
|
29
|
+
>;
|
|
30
|
+
complexityAccuracy: Record<
|
|
31
|
+
string,
|
|
32
|
+
{
|
|
33
|
+
predicted: number;
|
|
34
|
+
actualTierUsed: string;
|
|
35
|
+
mismatchRate: number;
|
|
36
|
+
}
|
|
37
|
+
>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Context passed to routing strategies */
|
|
41
|
+
export interface RoutingContext {
|
|
42
|
+
/** Full configuration */
|
|
43
|
+
config: NaxConfig;
|
|
44
|
+
/** Optional codebase context summary */
|
|
45
|
+
codebaseContext?: string;
|
|
46
|
+
/** Optional historical metrics (v0.5 Phase 1) */
|
|
47
|
+
metrics?: AggregateMetrics;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Routing decision returned by strategies */
|
|
51
|
+
export interface RoutingDecision {
|
|
52
|
+
/** Classified complexity */
|
|
53
|
+
complexity: Complexity;
|
|
54
|
+
/** Model tier to use */
|
|
55
|
+
modelTier: ModelTier;
|
|
56
|
+
/** Test strategy */
|
|
57
|
+
testStrategy: TestStrategy;
|
|
58
|
+
/** Reasoning for the decision */
|
|
59
|
+
reasoning: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Routing strategy interface.
|
|
64
|
+
*
|
|
65
|
+
* Strategies can return:
|
|
66
|
+
* - A RoutingDecision if they can route the story
|
|
67
|
+
* - null to delegate to the next strategy in the chain
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* const myStrategy: RoutingStrategy = {
|
|
72
|
+
* name: "domain-specific",
|
|
73
|
+
* route(story, context) {
|
|
74
|
+
* if (story.tags.includes("migration")) {
|
|
75
|
+
* return {
|
|
76
|
+
* complexity: "expert",
|
|
77
|
+
* modelTier: "powerful",
|
|
78
|
+
* testStrategy: "three-session-tdd",
|
|
79
|
+
* reasoning: "Database migrations require expert model",
|
|
80
|
+
* };
|
|
81
|
+
* }
|
|
82
|
+
* return null; // Delegate to next strategy
|
|
83
|
+
* },
|
|
84
|
+
* };
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export interface RoutingStrategy {
|
|
88
|
+
/** Strategy name (for logging) */
|
|
89
|
+
readonly name: string;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Route a user story.
|
|
93
|
+
*
|
|
94
|
+
* @param story - The user story to route
|
|
95
|
+
* @param context - Routing context (config, metrics, codebase)
|
|
96
|
+
* @returns RoutingDecision if strategy can route, null to delegate (sync or async)
|
|
97
|
+
*/
|
|
98
|
+
route(story: UserStory, context: RoutingContext): RoutingDecision | null | Promise<RoutingDecision | null>;
|
|
99
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process tree cleanup utilities for TDD session management.
|
|
3
|
+
*
|
|
4
|
+
* Handles cleanup of orphaned child processes when agent sessions fail.
|
|
5
|
+
* Prevents zombie processes from consuming CPU after agent crashes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getLogger } from "../logger";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get process group ID (PGID) for a given process ID.
|
|
12
|
+
*
|
|
13
|
+
* @param pid - Process ID to get PGID for
|
|
14
|
+
* @returns PGID if found, null if process doesn't exist or has no PGID
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* const pgid = await getPgid(12345);
|
|
19
|
+
* if (pgid) {
|
|
20
|
+
* console.log(`Process 12345 belongs to group ${pgid}`);
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export async function getPgid(pid: number): Promise<number | null> {
|
|
25
|
+
try {
|
|
26
|
+
// Use ps to get PGID for the process
|
|
27
|
+
const proc = Bun.spawn(["ps", "-o", "pgid=", "-p", String(pid)], {
|
|
28
|
+
stdout: "pipe",
|
|
29
|
+
stderr: "pipe",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const exitCode = await proc.exited;
|
|
33
|
+
if (exitCode !== 0) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const output = await new Response(proc.stdout).text();
|
|
38
|
+
const pgid = Number.parseInt(output.trim(), 10);
|
|
39
|
+
|
|
40
|
+
return Number.isNaN(pgid) ? null : pgid;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Clean up an entire process tree by killing all processes in the process group.
|
|
48
|
+
*
|
|
49
|
+
* Uses SIGTERM first (graceful shutdown), then SIGKILL after a delay if processes persist.
|
|
50
|
+
* Handles the case where the process is already dead gracefully.
|
|
51
|
+
*
|
|
52
|
+
* @param pid - Root process ID whose process group should be cleaned up
|
|
53
|
+
* @param gracePeriodMs - Time to wait between SIGTERM and SIGKILL (default: 3000ms)
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* // After agent session fails
|
|
58
|
+
* if (!result.success && result.pid) {
|
|
59
|
+
* await cleanupProcessTree(result.pid);
|
|
60
|
+
* }
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export async function cleanupProcessTree(pid: number, gracePeriodMs = 3000): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
// Get the process group ID
|
|
66
|
+
const pgid = await getPgid(pid);
|
|
67
|
+
|
|
68
|
+
if (!pgid) {
|
|
69
|
+
// Process already dead or has no PGID — nothing to clean up
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Send SIGTERM to all processes in the group (negative PGID)
|
|
74
|
+
try {
|
|
75
|
+
process.kill(-pgid, "SIGTERM");
|
|
76
|
+
} catch (error) {
|
|
77
|
+
// ESRCH means no such process — already dead
|
|
78
|
+
const err = error as NodeJS.ErrnoException;
|
|
79
|
+
if (err.code !== "ESRCH") {
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Wait for graceful shutdown
|
|
86
|
+
await Bun.sleep(gracePeriodMs);
|
|
87
|
+
|
|
88
|
+
// Re-check PGID before SIGKILL to prevent race condition
|
|
89
|
+
// If the original process exited and a new process inherited its PID,
|
|
90
|
+
// we don't want to kill the wrong process group
|
|
91
|
+
const pgidAfterWait = await getPgid(pid);
|
|
92
|
+
|
|
93
|
+
// Only send SIGKILL if:
|
|
94
|
+
// 1. Process still exists (pgidAfterWait is not null)
|
|
95
|
+
// 2. PGID hasn't changed (still the same process group)
|
|
96
|
+
if (pgidAfterWait && pgidAfterWait === pgid) {
|
|
97
|
+
try {
|
|
98
|
+
process.kill(-pgid, "SIGKILL");
|
|
99
|
+
} catch {
|
|
100
|
+
// Ignore errors — processes may have exited during the wait
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// Log but don't throw — cleanup is best-effort
|
|
105
|
+
const logger = getLogger();
|
|
106
|
+
logger.warn("tdd", "Failed to cleanup process tree", {
|
|
107
|
+
pid,
|
|
108
|
+
error: (error as Error).message,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|