@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,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Agent Adapter
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { PidRegistry } from "../execution/pid-registry";
|
|
6
|
+
import { getLogger } from "../logger";
|
|
7
|
+
import { buildDecomposePrompt, parseDecomposeOutput } from "./claude-decompose";
|
|
8
|
+
import { runPlan } from "./claude-plan";
|
|
9
|
+
import { estimateCostByDuration, estimateCostFromOutput } from "./cost";
|
|
10
|
+
import type {
|
|
11
|
+
AgentAdapter,
|
|
12
|
+
AgentCapabilities,
|
|
13
|
+
AgentResult,
|
|
14
|
+
AgentRunOptions,
|
|
15
|
+
DecomposeOptions,
|
|
16
|
+
DecomposeResult,
|
|
17
|
+
InteractiveRunOptions,
|
|
18
|
+
PlanOptions,
|
|
19
|
+
PlanResult,
|
|
20
|
+
PtyHandle,
|
|
21
|
+
} from "./types";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Maximum characters to capture from agent stdout.
|
|
25
|
+
*
|
|
26
|
+
* Last 5000 chars typically contain the most relevant info (final status, summary, errors).
|
|
27
|
+
* This limit prevents memory bloat while preserving actionable output.
|
|
28
|
+
*/
|
|
29
|
+
const MAX_AGENT_OUTPUT_CHARS = 5000;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Maximum characters to capture from agent stderr.
|
|
33
|
+
*
|
|
34
|
+
* Last 1000 chars typically contain the actual error message (e.g., 401, 500, crash).
|
|
35
|
+
* Smaller than stdout since stderr is more focused on errors.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_AGENT_STDERR_CHARS = 1000;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Claude Code agent adapter implementation.
|
|
41
|
+
*
|
|
42
|
+
* Implements the AgentAdapter interface for Claude Code CLI,
|
|
43
|
+
* supporting model routing, rate limit retry, and cost tracking.
|
|
44
|
+
*/
|
|
45
|
+
export class ClaudeCodeAdapter implements AgentAdapter {
|
|
46
|
+
readonly name = "claude";
|
|
47
|
+
readonly displayName = "Claude Code";
|
|
48
|
+
readonly binary = "claude";
|
|
49
|
+
|
|
50
|
+
readonly capabilities: AgentCapabilities = {
|
|
51
|
+
supportedTiers: ["fast", "balanced", "powerful"],
|
|
52
|
+
maxContextTokens: 200_000,
|
|
53
|
+
features: new Set(["tdd", "review", "refactor", "batch"]),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
private pidRegistries: Map<string, PidRegistry> = new Map();
|
|
57
|
+
|
|
58
|
+
private getPidRegistry(workdir: string): PidRegistry {
|
|
59
|
+
if (!this.pidRegistries.has(workdir)) {
|
|
60
|
+
this.pidRegistries.set(workdir, new PidRegistry(workdir));
|
|
61
|
+
}
|
|
62
|
+
const registry = this.pidRegistries.get(workdir);
|
|
63
|
+
if (!registry) {
|
|
64
|
+
throw new Error(`PidRegistry not found for workdir: ${workdir}`);
|
|
65
|
+
}
|
|
66
|
+
return registry;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async isInstalled(): Promise<boolean> {
|
|
70
|
+
try {
|
|
71
|
+
const proc = Bun.spawn(["which", this.binary], { stdout: "pipe", stderr: "pipe" });
|
|
72
|
+
const code = await proc.exited;
|
|
73
|
+
return code === 0;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
const logger = getLogger();
|
|
76
|
+
logger?.debug("agent", "Failed to check if agent is installed", { error });
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
buildCommand(options: AgentRunOptions): string[] {
|
|
82
|
+
const model = options.modelDef.model;
|
|
83
|
+
const skipPermissions = options.dangerouslySkipPermissions ?? true;
|
|
84
|
+
const permArgs = skipPermissions ? ["--dangerously-skip-permissions"] : [];
|
|
85
|
+
return [this.binary, "--model", model, ...permArgs, "-p", options.prompt];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async run(options: AgentRunOptions): Promise<AgentResult> {
|
|
89
|
+
const maxRetries = 3;
|
|
90
|
+
let lastError: Error | null = null;
|
|
91
|
+
|
|
92
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
93
|
+
try {
|
|
94
|
+
const result = await this.runOnce(options, attempt);
|
|
95
|
+
|
|
96
|
+
if (result.rateLimited && attempt < maxRetries) {
|
|
97
|
+
const backoffMs = 2 ** attempt * 1000;
|
|
98
|
+
const logger = getLogger();
|
|
99
|
+
logger.warn("agent", "Rate limited, retrying", { backoffSeconds: backoffMs / 1000, attempt, maxRetries });
|
|
100
|
+
await Bun.sleep(backoffMs);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
lastError = error as Error;
|
|
107
|
+
const isSpawnError = lastError.message.includes("spawn") || lastError.message.includes("ENOENT");
|
|
108
|
+
|
|
109
|
+
if (isSpawnError && attempt < maxRetries) {
|
|
110
|
+
const backoffMs = 2 ** attempt * 1000;
|
|
111
|
+
const logger = getLogger();
|
|
112
|
+
logger.warn("agent", "Agent spawn error, retrying", {
|
|
113
|
+
error: lastError.message,
|
|
114
|
+
backoffSeconds: backoffMs / 1000,
|
|
115
|
+
attempt,
|
|
116
|
+
maxRetries,
|
|
117
|
+
});
|
|
118
|
+
await Bun.sleep(backoffMs);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
throw lastError;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
throw lastError || new Error("Agent execution failed after all retries");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build allowed environment variables for spawned agents.
|
|
131
|
+
* SEC-4: Only pass essential env vars to prevent leaking sensitive data.
|
|
132
|
+
*/
|
|
133
|
+
buildAllowedEnv(options: AgentRunOptions): Record<string, string | undefined> {
|
|
134
|
+
const allowed: Record<string, string | undefined> = {};
|
|
135
|
+
|
|
136
|
+
const essentialVars = ["PATH", "HOME", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
|
|
137
|
+
for (const varName of essentialVars) {
|
|
138
|
+
if (process.env[varName]) {
|
|
139
|
+
allowed[varName] = process.env[varName];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"];
|
|
144
|
+
for (const varName of apiKeyVars) {
|
|
145
|
+
if (process.env[varName]) {
|
|
146
|
+
allowed[varName] = process.env[varName];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const allowedPrefixes = ["CLAUDE_", "NAX_", "CLAW_", "TURBO_"];
|
|
151
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
152
|
+
if (allowedPrefixes.some((prefix) => key.startsWith(prefix))) {
|
|
153
|
+
allowed[key] = value;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (options.modelDef.env) {
|
|
158
|
+
Object.assign(allowed, options.modelDef.env);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (options.env) {
|
|
162
|
+
Object.assign(allowed, options.env);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return allowed;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async runOnce(options: AgentRunOptions, _attempt: number): Promise<AgentResult> {
|
|
169
|
+
const cmd = this.buildCommand(options);
|
|
170
|
+
const startTime = Date.now();
|
|
171
|
+
|
|
172
|
+
const proc = Bun.spawn(cmd, {
|
|
173
|
+
cwd: options.workdir,
|
|
174
|
+
stdout: "pipe",
|
|
175
|
+
stderr: "pipe",
|
|
176
|
+
env: this.buildAllowedEnv(options),
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const processPid = proc.pid;
|
|
180
|
+
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
181
|
+
await pidRegistry.register(processPid);
|
|
182
|
+
|
|
183
|
+
let timedOut = false;
|
|
184
|
+
const timeoutId = setTimeout(() => {
|
|
185
|
+
timedOut = true;
|
|
186
|
+
proc.kill("SIGTERM");
|
|
187
|
+
}, options.timeoutSeconds * 1000);
|
|
188
|
+
|
|
189
|
+
const exitCode = await proc.exited;
|
|
190
|
+
clearTimeout(timeoutId);
|
|
191
|
+
|
|
192
|
+
await pidRegistry.unregister(processPid);
|
|
193
|
+
|
|
194
|
+
const stdout = await new Response(proc.stdout).text();
|
|
195
|
+
const stderr = await new Response(proc.stderr).text();
|
|
196
|
+
const durationMs = Date.now() - startTime;
|
|
197
|
+
|
|
198
|
+
const rateLimited =
|
|
199
|
+
stderr.includes("rate limit") ||
|
|
200
|
+
stderr.includes("429") ||
|
|
201
|
+
stdout.includes("rate limit") ||
|
|
202
|
+
stdout.includes("Too many requests");
|
|
203
|
+
|
|
204
|
+
const fullOutput = stdout + stderr;
|
|
205
|
+
let costEstimate = estimateCostFromOutput(options.modelTier, fullOutput);
|
|
206
|
+
const logger = getLogger();
|
|
207
|
+
if (!costEstimate) {
|
|
208
|
+
const fallbackEstimate = estimateCostByDuration(options.modelTier, durationMs);
|
|
209
|
+
costEstimate = {
|
|
210
|
+
cost: fallbackEstimate.cost * 1.5,
|
|
211
|
+
confidence: "fallback",
|
|
212
|
+
};
|
|
213
|
+
logger.warn("agent", "Cost estimation fallback (duration-based)", {
|
|
214
|
+
modelTier: options.modelTier,
|
|
215
|
+
cost: costEstimate.cost,
|
|
216
|
+
});
|
|
217
|
+
} else if (costEstimate.confidence === "estimated") {
|
|
218
|
+
logger.warn("agent", "Cost estimation using regex parsing (estimated confidence)", { cost: costEstimate.cost });
|
|
219
|
+
}
|
|
220
|
+
const cost = costEstimate.cost;
|
|
221
|
+
|
|
222
|
+
const actualExitCode = timedOut ? 124 : exitCode;
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
success: exitCode === 0 && !timedOut,
|
|
226
|
+
exitCode: actualExitCode,
|
|
227
|
+
output: stdout.slice(-MAX_AGENT_OUTPUT_CHARS),
|
|
228
|
+
stderr: stderr.slice(-MAX_AGENT_STDERR_CHARS),
|
|
229
|
+
rateLimited,
|
|
230
|
+
durationMs,
|
|
231
|
+
estimatedCost: cost,
|
|
232
|
+
pid: processPid,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async plan(options: PlanOptions): Promise<PlanResult> {
|
|
237
|
+
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
238
|
+
return runPlan(this.binary, options, pidRegistry, this.buildAllowedEnv.bind(this));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async decompose(options: DecomposeOptions): Promise<DecomposeResult> {
|
|
242
|
+
const prompt = buildDecomposePrompt(options);
|
|
243
|
+
|
|
244
|
+
const cmd = [
|
|
245
|
+
this.binary,
|
|
246
|
+
"--model",
|
|
247
|
+
options.modelDef?.model || "claude-sonnet-4-5",
|
|
248
|
+
"--dangerously-skip-permissions",
|
|
249
|
+
"-p",
|
|
250
|
+
prompt,
|
|
251
|
+
];
|
|
252
|
+
|
|
253
|
+
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
254
|
+
|
|
255
|
+
const proc = Bun.spawn(cmd, {
|
|
256
|
+
cwd: options.workdir,
|
|
257
|
+
stdout: "pipe",
|
|
258
|
+
stderr: "pipe",
|
|
259
|
+
env: this.buildAllowedEnv({
|
|
260
|
+
workdir: options.workdir,
|
|
261
|
+
modelDef: options.modelDef || { provider: "anthropic", model: "claude-sonnet-4-5", env: {} },
|
|
262
|
+
prompt: "",
|
|
263
|
+
modelTier: "balanced",
|
|
264
|
+
timeoutSeconds: 600,
|
|
265
|
+
}),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
await pidRegistry.register(proc.pid);
|
|
269
|
+
|
|
270
|
+
const exitCode = await proc.exited;
|
|
271
|
+
|
|
272
|
+
await pidRegistry.unregister(proc.pid);
|
|
273
|
+
|
|
274
|
+
const stdout = await new Response(proc.stdout).text();
|
|
275
|
+
const stderr = await new Response(proc.stderr).text();
|
|
276
|
+
|
|
277
|
+
if (exitCode !== 0) {
|
|
278
|
+
throw new Error(`Decompose failed with exit code ${exitCode}: ${stderr}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const stories = parseDecomposeOutput(stdout);
|
|
282
|
+
|
|
283
|
+
return { stories };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
runInteractive(options: InteractiveRunOptions): PtyHandle {
|
|
287
|
+
let nodePty: typeof import("node-pty");
|
|
288
|
+
try {
|
|
289
|
+
nodePty = require("node-pty");
|
|
290
|
+
} catch (error) {
|
|
291
|
+
throw new Error(`node-pty not available: ${(error as Error).message}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const model = options.modelDef.model;
|
|
295
|
+
const cmd = [this.binary, "--model", model, options.prompt];
|
|
296
|
+
|
|
297
|
+
const ptyProc = nodePty.spawn(cmd[0], cmd.slice(1), {
|
|
298
|
+
name: "xterm-256color",
|
|
299
|
+
cols: 80,
|
|
300
|
+
rows: 24,
|
|
301
|
+
cwd: options.workdir,
|
|
302
|
+
env: this.buildAllowedEnv(options),
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const pidRegistry = this.getPidRegistry(options.workdir);
|
|
306
|
+
pidRegistry.register(ptyProc.pid).catch(() => {});
|
|
307
|
+
|
|
308
|
+
ptyProc.onData((data) => {
|
|
309
|
+
options.onOutput(Buffer.from(data));
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
ptyProc.onExit((event) => {
|
|
313
|
+
pidRegistry.unregister(ptyProc.pid).catch(() => {});
|
|
314
|
+
options.onExit(event.exitCode);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
write: (data: string) => ptyProc.write(data),
|
|
319
|
+
resize: (cols: number, rows: number) => ptyProc.resize(cols, rows),
|
|
320
|
+
kill: () => ptyProc.kill(),
|
|
321
|
+
pid: ptyProc.pid,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost Tracking
|
|
3
|
+
*
|
|
4
|
+
* Token-based cost estimation for AI coding agents.
|
|
5
|
+
* Parses agent output for token usage and calculates costs.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ModelTier } from "../config/schema";
|
|
9
|
+
|
|
10
|
+
/** Cost rates per 1M tokens (USD) */
|
|
11
|
+
export interface ModelCostRates {
|
|
12
|
+
inputPer1M: number;
|
|
13
|
+
outputPer1M: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Token usage data */
|
|
17
|
+
export interface TokenUsage {
|
|
18
|
+
inputTokens: number;
|
|
19
|
+
outputTokens: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Cost estimate with confidence indicator */
|
|
23
|
+
export interface CostEstimate {
|
|
24
|
+
cost: number;
|
|
25
|
+
confidence: "exact" | "estimated" | "fallback";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Model tier cost rates (as of 2025-01) */
|
|
29
|
+
export const COST_RATES: Record<ModelTier, ModelCostRates> = {
|
|
30
|
+
fast: {
|
|
31
|
+
// Haiku 4.5
|
|
32
|
+
inputPer1M: 0.8,
|
|
33
|
+
outputPer1M: 4.0,
|
|
34
|
+
},
|
|
35
|
+
balanced: {
|
|
36
|
+
// Sonnet 4.5
|
|
37
|
+
inputPer1M: 3.0,
|
|
38
|
+
outputPer1M: 15.0,
|
|
39
|
+
},
|
|
40
|
+
powerful: {
|
|
41
|
+
// Opus 4
|
|
42
|
+
inputPer1M: 15.0,
|
|
43
|
+
outputPer1M: 75.0,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Token usage with confidence indicator.
|
|
49
|
+
*/
|
|
50
|
+
export interface TokenUsageWithConfidence {
|
|
51
|
+
inputTokens: number;
|
|
52
|
+
outputTokens: number;
|
|
53
|
+
confidence: "exact" | "estimated";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse Claude Code output for token usage.
|
|
58
|
+
*
|
|
59
|
+
* Supports multiple formats with varying confidence levels:
|
|
60
|
+
* - JSON structured output → "exact" confidence
|
|
61
|
+
* - Markdown/plain text patterns → "estimated" confidence
|
|
62
|
+
*
|
|
63
|
+
* Uses specific regex patterns to reduce false positives.
|
|
64
|
+
*
|
|
65
|
+
* @param output - Agent stdout + stderr combined
|
|
66
|
+
* @returns Token usage with confidence indicator, or null if tokens cannot be parsed
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* // JSON format (exact)
|
|
71
|
+
* const usage1 = parseTokenUsage('{"usage": {"input_tokens": 1234, "output_tokens": 5678}}');
|
|
72
|
+
* // { inputTokens: 1234, outputTokens: 5678, confidence: 'exact' }
|
|
73
|
+
*
|
|
74
|
+
* // Markdown format (estimated)
|
|
75
|
+
* const usage2 = parseTokenUsage('Input tokens: 1234\nOutput tokens: 5678');
|
|
76
|
+
* // { inputTokens: 1234, outputTokens: 5678, confidence: 'estimated' }
|
|
77
|
+
*
|
|
78
|
+
* // Unparseable
|
|
79
|
+
* const usage3 = parseTokenUsage('No token data here');
|
|
80
|
+
* // null
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export function parseTokenUsage(output: string): TokenUsageWithConfidence | null {
|
|
84
|
+
// Try JSON format first (most reliable) - confidence: exact
|
|
85
|
+
try {
|
|
86
|
+
const jsonMatch = output.match(
|
|
87
|
+
/\{[^}]*"usage"\s*:\s*\{[^}]*"input_tokens"\s*:\s*(\d+)[^}]*"output_tokens"\s*:\s*(\d+)[^}]*\}[^}]*\}/,
|
|
88
|
+
);
|
|
89
|
+
if (jsonMatch) {
|
|
90
|
+
return {
|
|
91
|
+
inputTokens: Number.parseInt(jsonMatch[1], 10),
|
|
92
|
+
outputTokens: Number.parseInt(jsonMatch[2], 10),
|
|
93
|
+
confidence: "exact",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Try parsing as full JSON object
|
|
98
|
+
const lines = output.split("\n");
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
if (line.trim().startsWith("{")) {
|
|
101
|
+
try {
|
|
102
|
+
const parsed = JSON.parse(line);
|
|
103
|
+
if (parsed.usage?.input_tokens && parsed.usage?.output_tokens) {
|
|
104
|
+
return {
|
|
105
|
+
inputTokens: parsed.usage.input_tokens,
|
|
106
|
+
outputTokens: parsed.usage.output_tokens,
|
|
107
|
+
confidence: "exact",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
// Not valid JSON, continue
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// JSON parsing failed, try regex patterns
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Try specific markdown-style patterns (more specific to reduce false positives)
|
|
120
|
+
// Match "Input tokens: 1234" or "input_tokens: 1234" or "INPUT TOKENS: 1234"
|
|
121
|
+
// Use word boundary at start, require colon or space after keyword, then digits
|
|
122
|
+
// confidence: estimated (regex-based)
|
|
123
|
+
const inputMatch = output.match(/\b(?:input|input_tokens)\s*:\s*(\d{2,})|(?:input)\s+(?:tokens?)\s*:\s*(\d{2,})/i);
|
|
124
|
+
const outputMatch = output.match(
|
|
125
|
+
/\b(?:output|output_tokens)\s*:\s*(\d{2,})|(?:output)\s+(?:tokens?)\s*:\s*(\d{2,})/i,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (inputMatch && outputMatch) {
|
|
129
|
+
// Extract token counts (may be in capture group 1 or 2)
|
|
130
|
+
const inputTokens = Number.parseInt(inputMatch[1] || inputMatch[2], 10);
|
|
131
|
+
const outputTokens = Number.parseInt(outputMatch[1] || outputMatch[2], 10);
|
|
132
|
+
|
|
133
|
+
// Sanity check: reject if tokens seem unreasonably large (> 1M each)
|
|
134
|
+
if (inputTokens > 1_000_000 || outputTokens > 1_000_000) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
inputTokens,
|
|
140
|
+
outputTokens,
|
|
141
|
+
confidence: "estimated",
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Estimate cost in USD based on token usage.
|
|
150
|
+
*
|
|
151
|
+
* Calculates total cost using tier-specific rates per 1M tokens.
|
|
152
|
+
*
|
|
153
|
+
* @param modelTier - Model tier (fast/balanced/powerful)
|
|
154
|
+
* @param inputTokens - Number of input tokens consumed
|
|
155
|
+
* @param outputTokens - Number of output tokens generated
|
|
156
|
+
* @returns Total cost in USD
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```ts
|
|
160
|
+
* const cost = estimateCost("balanced", 10000, 5000);
|
|
161
|
+
* // Sonnet 4.5: (10000/1M * $3.00) + (5000/1M * $15.00) = $0.105
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export function estimateCost(
|
|
165
|
+
modelTier: ModelTier,
|
|
166
|
+
inputTokens: number,
|
|
167
|
+
outputTokens: number,
|
|
168
|
+
customRates?: ModelCostRates,
|
|
169
|
+
): number {
|
|
170
|
+
const rates = customRates ?? COST_RATES[modelTier];
|
|
171
|
+
const inputCost = (inputTokens / 1_000_000) * rates.inputPer1M;
|
|
172
|
+
const outputCost = (outputTokens / 1_000_000) * rates.outputPer1M;
|
|
173
|
+
return inputCost + outputCost;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Estimate cost from agent output by parsing token usage.
|
|
178
|
+
*
|
|
179
|
+
* Attempts to extract token counts from stdout/stderr, then calculates cost.
|
|
180
|
+
* Returns null if tokens cannot be parsed (caller should use fallback estimation).
|
|
181
|
+
*
|
|
182
|
+
* @param modelTier - Model tier for cost calculation
|
|
183
|
+
* @param output - Agent stdout + stderr combined
|
|
184
|
+
* @returns Cost estimate with confidence indicator, or null if unparseable
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* ```ts
|
|
188
|
+
* const estimate = estimateCostFromOutput("balanced", agentOutput);
|
|
189
|
+
* if (estimate) {
|
|
190
|
+
* console.log(`Cost: $${estimate.cost.toFixed(4)} (${estimate.confidence})`);
|
|
191
|
+
* } else {
|
|
192
|
+
* // Fall back to duration-based estimation
|
|
193
|
+
* }
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
export function estimateCostFromOutput(modelTier: ModelTier, output: string): CostEstimate | null {
|
|
197
|
+
const usage = parseTokenUsage(output);
|
|
198
|
+
if (!usage) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const cost = estimateCost(modelTier, usage.inputTokens, usage.outputTokens);
|
|
202
|
+
return {
|
|
203
|
+
cost,
|
|
204
|
+
confidence: usage.confidence,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Fallback cost estimation based on runtime duration.
|
|
210
|
+
*
|
|
211
|
+
* Used when token usage cannot be parsed from agent output.
|
|
212
|
+
* Provides conservative estimates using per-minute rates.
|
|
213
|
+
*
|
|
214
|
+
* @param modelTier - Model tier for cost calculation
|
|
215
|
+
* @param durationMs - Agent runtime in milliseconds
|
|
216
|
+
* @returns Cost estimate with 'fallback' confidence
|
|
217
|
+
*
|
|
218
|
+
* @example
|
|
219
|
+
* ```ts
|
|
220
|
+
* const estimate = estimateCostByDuration("balanced", 120000); // 2 minutes
|
|
221
|
+
* // { cost: 0.10, confidence: 'fallback' }
|
|
222
|
+
* // Sonnet: 2 min * $0.05/min = $0.10
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
export function estimateCostByDuration(modelTier: ModelTier, durationMs: number): CostEstimate {
|
|
226
|
+
const costPerMinute: Record<ModelTier, number> = {
|
|
227
|
+
fast: 0.01,
|
|
228
|
+
balanced: 0.05,
|
|
229
|
+
powerful: 0.15,
|
|
230
|
+
};
|
|
231
|
+
const minutes = durationMs / 60000;
|
|
232
|
+
const cost = minutes * costPerMinute[modelTier];
|
|
233
|
+
return {
|
|
234
|
+
cost,
|
|
235
|
+
confidence: "fallback",
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Format cost estimate with confidence indicator for display.
|
|
241
|
+
*
|
|
242
|
+
* @param estimate - Cost estimate with confidence level
|
|
243
|
+
* @returns Formatted cost string with confidence indicator
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```ts
|
|
247
|
+
* formatCostWithConfidence({ cost: 0.12, confidence: 'exact' });
|
|
248
|
+
* // "$0.12"
|
|
249
|
+
*
|
|
250
|
+
* formatCostWithConfidence({ cost: 0.15, confidence: 'estimated' });
|
|
251
|
+
* // "~$0.15"
|
|
252
|
+
*
|
|
253
|
+
* formatCostWithConfidence({ cost: 0.05, confidence: 'fallback' });
|
|
254
|
+
* // "~$0.05 (duration-based)"
|
|
255
|
+
* ```
|
|
256
|
+
*/
|
|
257
|
+
export function formatCostWithConfidence(estimate: CostEstimate): string {
|
|
258
|
+
const formattedCost = `$${estimate.cost.toFixed(2)}`;
|
|
259
|
+
|
|
260
|
+
switch (estimate.confidence) {
|
|
261
|
+
case "exact":
|
|
262
|
+
return formattedCost;
|
|
263
|
+
case "estimated":
|
|
264
|
+
return `~${formattedCost}`;
|
|
265
|
+
case "fallback":
|
|
266
|
+
return `~${formattedCost} (duration-based)`;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type { AgentAdapter, AgentCapabilities, AgentResult, AgentRunOptions } from "./types";
|
|
2
|
+
export { ClaudeCodeAdapter } from "./claude";
|
|
3
|
+
export { getAllAgentNames, getAgent, getInstalledAgents, checkAgentHealth } from "./registry";
|
|
4
|
+
export type { ModelCostRates, TokenUsage, CostEstimate, TokenUsageWithConfidence } from "./cost";
|
|
5
|
+
export {
|
|
6
|
+
COST_RATES,
|
|
7
|
+
parseTokenUsage,
|
|
8
|
+
estimateCost,
|
|
9
|
+
estimateCostFromOutput,
|
|
10
|
+
estimateCostByDuration,
|
|
11
|
+
formatCostWithConfidence,
|
|
12
|
+
} from "./cost";
|
|
13
|
+
export { validateAgentForTier, validateAgentFeature, describeAgentCapabilities } from "./validation";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Registry
|
|
3
|
+
*
|
|
4
|
+
* Discovers and manages available coding agents.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ClaudeCodeAdapter } from "./claude";
|
|
8
|
+
import type { AgentAdapter } from "./types";
|
|
9
|
+
|
|
10
|
+
/** All known agent adapters */
|
|
11
|
+
export const ALL_AGENTS: AgentAdapter[] = [
|
|
12
|
+
new ClaudeCodeAdapter(),
|
|
13
|
+
// Future: new CodexAdapter(),
|
|
14
|
+
// Future: new OpenCodeAdapter(),
|
|
15
|
+
// Future: new GeminiAdapter(),
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/** Get all registered agent names */
|
|
19
|
+
export function getAllAgentNames(): string[] {
|
|
20
|
+
return ALL_AGENTS.map((a) => a.name);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Get a specific agent by name */
|
|
24
|
+
export function getAgent(name: string): AgentAdapter | undefined {
|
|
25
|
+
return ALL_AGENTS.find((a) => a.name === name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Get all installed agents on this machine */
|
|
29
|
+
export async function getInstalledAgents(): Promise<AgentAdapter[]> {
|
|
30
|
+
const results = await Promise.all(
|
|
31
|
+
ALL_AGENTS.map(async (agent) => ({
|
|
32
|
+
agent,
|
|
33
|
+
installed: await agent.isInstalled(),
|
|
34
|
+
})),
|
|
35
|
+
);
|
|
36
|
+
return results.filter((r) => r.installed).map((r) => r.agent);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Check health of all agents */
|
|
40
|
+
export async function checkAgentHealth(): Promise<Array<{ name: string; displayName: string; installed: boolean }>> {
|
|
41
|
+
return Promise.all(
|
|
42
|
+
ALL_AGENTS.map(async (agent) => ({
|
|
43
|
+
name: agent.name,
|
|
44
|
+
displayName: agent.displayName,
|
|
45
|
+
installed: await agent.isInstalled(),
|
|
46
|
+
})),
|
|
47
|
+
);
|
|
48
|
+
}
|