@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,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-detection for contextFiles when PRD omits them (BUG-006)
|
|
3
|
+
*
|
|
4
|
+
* Extracts keywords from story title, runs git grep to find matching source files,
|
|
5
|
+
* excludes test/index/generated files, caps at maxFiles.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getLogger } from "../logger";
|
|
9
|
+
|
|
10
|
+
export interface AutoDetectOptions {
|
|
11
|
+
/** Working directory for git grep */
|
|
12
|
+
workdir: string;
|
|
13
|
+
/** Story title to extract keywords from */
|
|
14
|
+
storyTitle: string;
|
|
15
|
+
/** Maximum files to return (default: 5) */
|
|
16
|
+
maxFiles?: number;
|
|
17
|
+
/** Enable import tracing (default: false, reserved for future use) */
|
|
18
|
+
traceImports?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Extract keywords from story title for git grep search.
|
|
23
|
+
*
|
|
24
|
+
* Heuristics:
|
|
25
|
+
* - Remove common words (the, a, an, and, or, to, from, for, with, etc.)
|
|
26
|
+
* - Split on spaces/punctuation
|
|
27
|
+
* - Keep words >= 3 chars
|
|
28
|
+
* - Lowercase for case-insensitive matching
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* extractKeywords("BUG-006: Context auto-detection (contextFiles)")
|
|
32
|
+
* // => ["bug", "006", "context", "auto", "detection", "contextfiles"]
|
|
33
|
+
*/
|
|
34
|
+
export function extractKeywords(title: string): string[] {
|
|
35
|
+
const stopWords = new Set([
|
|
36
|
+
"the",
|
|
37
|
+
"a",
|
|
38
|
+
"an",
|
|
39
|
+
"and",
|
|
40
|
+
"or",
|
|
41
|
+
"to",
|
|
42
|
+
"from",
|
|
43
|
+
"for",
|
|
44
|
+
"with",
|
|
45
|
+
"in",
|
|
46
|
+
"on",
|
|
47
|
+
"at",
|
|
48
|
+
"by",
|
|
49
|
+
"is",
|
|
50
|
+
"are",
|
|
51
|
+
"was",
|
|
52
|
+
"were",
|
|
53
|
+
"be",
|
|
54
|
+
"been",
|
|
55
|
+
"being",
|
|
56
|
+
"have",
|
|
57
|
+
"has",
|
|
58
|
+
"had",
|
|
59
|
+
"do",
|
|
60
|
+
"does",
|
|
61
|
+
"did",
|
|
62
|
+
"will",
|
|
63
|
+
"would",
|
|
64
|
+
"should",
|
|
65
|
+
"could",
|
|
66
|
+
"may",
|
|
67
|
+
"might",
|
|
68
|
+
"can",
|
|
69
|
+
"must",
|
|
70
|
+
"of",
|
|
71
|
+
"as",
|
|
72
|
+
"if",
|
|
73
|
+
"when",
|
|
74
|
+
"bug",
|
|
75
|
+
"fix",
|
|
76
|
+
"add",
|
|
77
|
+
"update",
|
|
78
|
+
"remove",
|
|
79
|
+
"implement",
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
// Split on non-alphanumeric, filter, lowercase
|
|
83
|
+
const words = title
|
|
84
|
+
.toLowerCase()
|
|
85
|
+
.split(/[^a-z0-9]+/)
|
|
86
|
+
.filter((w) => w.length >= 3 && !stopWords.has(w));
|
|
87
|
+
|
|
88
|
+
// Deduplicate
|
|
89
|
+
return Array.from(new Set(words));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Auto-detect context files via git grep when PRD omits contextFiles.
|
|
94
|
+
*
|
|
95
|
+
* Algorithm:
|
|
96
|
+
* 1. Extract keywords from story title
|
|
97
|
+
* 2. Run git grep for each keyword in src/ directories
|
|
98
|
+
* 3. Deduplicate files
|
|
99
|
+
* 4. Exclude test/index/generated files
|
|
100
|
+
* 5. Cap at maxFiles (default: 5)
|
|
101
|
+
*
|
|
102
|
+
* Returns empty array if:
|
|
103
|
+
* - Not a git repository
|
|
104
|
+
* - No keywords extracted
|
|
105
|
+
* - git grep fails
|
|
106
|
+
* - No matching files found
|
|
107
|
+
*
|
|
108
|
+
* @param options - Auto-detect configuration
|
|
109
|
+
* @returns Array of relative file paths (sorted by relevance score)
|
|
110
|
+
*/
|
|
111
|
+
export async function autoDetectContextFiles(options: AutoDetectOptions): Promise<string[]> {
|
|
112
|
+
const { workdir, storyTitle, maxFiles = 5 } = options;
|
|
113
|
+
const logger = getLogger();
|
|
114
|
+
|
|
115
|
+
// Extract keywords
|
|
116
|
+
const keywords = extractKeywords(storyTitle);
|
|
117
|
+
if (keywords.length === 0) {
|
|
118
|
+
logger.debug("auto-detect", "No keywords extracted from story title", { storyTitle });
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
logger.debug("auto-detect", "Extracted keywords", { keywords, storyTitle });
|
|
123
|
+
|
|
124
|
+
// Build git grep command
|
|
125
|
+
// Use -i for case-insensitive, -l for filename-only, -I to skip binary files
|
|
126
|
+
// Search in src/ directories only (exclude test, node_modules, etc.)
|
|
127
|
+
const grepPattern = keywords.join("|"); // OR pattern
|
|
128
|
+
const grepCommand = [
|
|
129
|
+
"git",
|
|
130
|
+
"grep",
|
|
131
|
+
"-i", // case-insensitive
|
|
132
|
+
"-l", // files-with-matches
|
|
133
|
+
"-I", // skip binary files
|
|
134
|
+
"-E", // extended regex
|
|
135
|
+
"-e",
|
|
136
|
+
grepPattern,
|
|
137
|
+
"--", // separator
|
|
138
|
+
"src/", // limit to src directory
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const proc = Bun.spawn(grepCommand, {
|
|
143
|
+
cwd: workdir,
|
|
144
|
+
stdout: "pipe",
|
|
145
|
+
stderr: "pipe",
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const stdout = await new Response(proc.stdout).text();
|
|
149
|
+
const exitCode = await proc.exited;
|
|
150
|
+
|
|
151
|
+
// git grep returns 1 when no matches found (not an error)
|
|
152
|
+
if (exitCode !== 0 && exitCode !== 1) {
|
|
153
|
+
const stderr = await new Response(proc.stderr).text();
|
|
154
|
+
logger.warn("auto-detect", "git grep failed", { exitCode, stderr: stderr.trim() });
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!stdout.trim()) {
|
|
159
|
+
logger.debug("auto-detect", "No files matched keywords", { keywords });
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Parse file list
|
|
164
|
+
const allFiles = stdout
|
|
165
|
+
.trim()
|
|
166
|
+
.split("\n")
|
|
167
|
+
.map((line) => line.trim())
|
|
168
|
+
.filter((line) => line.length > 0);
|
|
169
|
+
|
|
170
|
+
// Filter out test files, index files, generated files
|
|
171
|
+
const filtered = allFiles.filter((filePath) => {
|
|
172
|
+
const lower = filePath.toLowerCase();
|
|
173
|
+
// Exclude test files
|
|
174
|
+
if (lower.includes(".test.") || lower.includes(".spec.") || lower.includes("/test/")) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
// Exclude index files (barrel exports, low signal)
|
|
178
|
+
if (lower.endsWith("/index.ts") || lower.endsWith("/index.js")) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
// Exclude generated files
|
|
182
|
+
if (lower.includes(".generated.") || lower.includes("/__generated__/")) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
return true;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Score and sort by relevance
|
|
189
|
+
// Simple heuristic: count keyword matches in file path
|
|
190
|
+
interface ScoredFile {
|
|
191
|
+
path: string;
|
|
192
|
+
score: number;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const scored: ScoredFile[] = filtered.map((filePath) => {
|
|
196
|
+
const lowerPath = filePath.toLowerCase();
|
|
197
|
+
const score = keywords.filter((kw) => lowerPath.includes(kw)).length;
|
|
198
|
+
return { path: filePath, score };
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Sort by score descending, then alphabetically
|
|
202
|
+
scored.sort((a, b) => {
|
|
203
|
+
if (a.score !== b.score) {
|
|
204
|
+
return b.score - a.score; // Higher score first
|
|
205
|
+
}
|
|
206
|
+
return a.path.localeCompare(b.path); // Alphabetical tiebreaker
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Cap at maxFiles
|
|
210
|
+
const selected = scored.slice(0, maxFiles).map((s) => s.path);
|
|
211
|
+
|
|
212
|
+
logger.info("auto-detect", "Auto-detected context files", {
|
|
213
|
+
keywords,
|
|
214
|
+
totalMatches: allFiles.length,
|
|
215
|
+
afterFilter: filtered.length,
|
|
216
|
+
selected: selected.length,
|
|
217
|
+
files: selected,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return selected;
|
|
221
|
+
} catch (error) {
|
|
222
|
+
logger.warn("auto-detect", "Auto-detection failed", {
|
|
223
|
+
error: error instanceof Error ? error.message : String(error),
|
|
224
|
+
});
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context builder for story-scoped prompt optimization
|
|
3
|
+
*
|
|
4
|
+
* Extracts current story + dependency stories from PRD and builds context within token budget.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import type { NaxConfig } from "../config";
|
|
9
|
+
import { getLogger } from "../logger";
|
|
10
|
+
import type { UserStory } from "../prd";
|
|
11
|
+
import { countStories, getContextFiles } from "../prd";
|
|
12
|
+
import { autoDetectContextFiles } from "./auto-detect";
|
|
13
|
+
import {
|
|
14
|
+
createDependencyContext,
|
|
15
|
+
createErrorContext,
|
|
16
|
+
createFileContext,
|
|
17
|
+
createProgressContext,
|
|
18
|
+
createStoryContext,
|
|
19
|
+
createTestCoverageContext,
|
|
20
|
+
estimateTokens,
|
|
21
|
+
} from "./elements";
|
|
22
|
+
import { generateTestCoverageSummary } from "./test-scanner";
|
|
23
|
+
import type { BuiltContext, ContextBudget, ContextElement, StoryContext } from "./types";
|
|
24
|
+
|
|
25
|
+
// Re-export for backward compatibility
|
|
26
|
+
export {
|
|
27
|
+
estimateTokens,
|
|
28
|
+
createStoryContext,
|
|
29
|
+
createDependencyContext,
|
|
30
|
+
createErrorContext,
|
|
31
|
+
createProgressContext,
|
|
32
|
+
createFileContext,
|
|
33
|
+
createTestCoverageContext,
|
|
34
|
+
} from "./elements";
|
|
35
|
+
export { formatContextAsMarkdown } from "./formatter";
|
|
36
|
+
|
|
37
|
+
/** Sort context elements by priority (descending) and token count (ascending for same priority) */
|
|
38
|
+
export function sortContextElements(elements: ContextElement[]): ContextElement[] {
|
|
39
|
+
return [...elements].sort((a, b) => {
|
|
40
|
+
if (a.priority !== b.priority) return b.priority - a.priority;
|
|
41
|
+
return a.tokens - b.tokens;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Generate progress summary */
|
|
46
|
+
function generateProgressSummary(prd: StoryContext["prd"]): string {
|
|
47
|
+
const counts = countStories(prd);
|
|
48
|
+
const total = counts.total;
|
|
49
|
+
const complete = counts.passed + counts.failed;
|
|
50
|
+
if (counts.failed > 0) {
|
|
51
|
+
return `Progress: ${complete}/${total} stories complete (${counts.passed} passed, ${counts.failed} failed)`;
|
|
52
|
+
}
|
|
53
|
+
return `Progress: ${complete}/${total} stories complete (${counts.passed} passed)`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Generate human-readable summary of built context */
|
|
57
|
+
function generateSummary(elements: ContextElement[], totalTokens: number, truncated: boolean): string {
|
|
58
|
+
const counts: Record<string, number> = {
|
|
59
|
+
story: 0,
|
|
60
|
+
dependency: 0,
|
|
61
|
+
error: 0,
|
|
62
|
+
progress: 0,
|
|
63
|
+
file: 0,
|
|
64
|
+
"test-coverage": 0,
|
|
65
|
+
};
|
|
66
|
+
for (const element of elements) {
|
|
67
|
+
counts[element.type]++;
|
|
68
|
+
}
|
|
69
|
+
const parts: string[] = [];
|
|
70
|
+
if (counts.progress > 0) parts.push(`${counts.progress} progress`);
|
|
71
|
+
if (counts.story > 0) parts.push(`${counts.story} story`);
|
|
72
|
+
if (counts.dependency > 0) parts.push(`${counts.dependency} dependencies`);
|
|
73
|
+
if (counts.error > 0) parts.push(`${counts.error} errors`);
|
|
74
|
+
if (counts.file > 0) parts.push(`${counts.file} files`);
|
|
75
|
+
if (counts["test-coverage"] > 0) parts.push("test coverage");
|
|
76
|
+
|
|
77
|
+
const summary = `Context: ${parts.join(", ")} (${totalTokens} tokens)`;
|
|
78
|
+
return truncated ? `${summary} [TRUNCATED]` : summary;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Build context from PRD + current story within token budget. */
|
|
82
|
+
export async function buildContext(storyContext: StoryContext, budget: ContextBudget): Promise<BuiltContext> {
|
|
83
|
+
const { prd, currentStoryId } = storyContext;
|
|
84
|
+
const elements: ContextElement[] = [];
|
|
85
|
+
|
|
86
|
+
const currentStory = prd.userStories.find((s) => s.id === currentStoryId);
|
|
87
|
+
if (!currentStory) throw new Error(`Story ${currentStoryId} not found in PRD`);
|
|
88
|
+
|
|
89
|
+
// Add progress summary (highest priority)
|
|
90
|
+
elements.push(createProgressContext(generateProgressSummary(prd), 100));
|
|
91
|
+
|
|
92
|
+
// Add prior errors (high priority)
|
|
93
|
+
if (currentStory.priorErrors && Array.isArray(currentStory.priorErrors) && currentStory.priorErrors.length > 0) {
|
|
94
|
+
for (const error of currentStory.priorErrors) {
|
|
95
|
+
elements.push(createErrorContext(error, 90));
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Add current story (high priority)
|
|
100
|
+
elements.push(createStoryContext(currentStory, 80));
|
|
101
|
+
|
|
102
|
+
// Add dependency stories (medium priority)
|
|
103
|
+
addDependencyElements(elements, currentStory, prd);
|
|
104
|
+
|
|
105
|
+
// Add test coverage summary (priority 85)
|
|
106
|
+
await addTestCoverageElement(elements, storyContext, currentStory);
|
|
107
|
+
|
|
108
|
+
// Add relevant source files (priority 60)
|
|
109
|
+
await addFileElements(elements, storyContext, currentStory);
|
|
110
|
+
|
|
111
|
+
// Select elements within budget
|
|
112
|
+
const sorted = sortContextElements(elements);
|
|
113
|
+
const selected: ContextElement[] = [];
|
|
114
|
+
let totalTokens = 0;
|
|
115
|
+
let truncated = false;
|
|
116
|
+
|
|
117
|
+
for (const element of sorted) {
|
|
118
|
+
if (totalTokens + element.tokens <= budget.availableForContext) {
|
|
119
|
+
selected.push(element);
|
|
120
|
+
totalTokens += element.tokens;
|
|
121
|
+
} else {
|
|
122
|
+
truncated = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { elements: selected, totalTokens, truncated, summary: generateSummary(selected, totalTokens, truncated) };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Add dependency story elements to the context. */
|
|
130
|
+
function addDependencyElements(elements: ContextElement[], story: UserStory, prd: StoryContext["prd"]): void {
|
|
131
|
+
if (!story.dependencies || story.dependencies.length === 0) return;
|
|
132
|
+
for (const depId of story.dependencies) {
|
|
133
|
+
const depStory = prd.userStories.find((s) => s.id === depId);
|
|
134
|
+
if (depStory) {
|
|
135
|
+
elements.push(createDependencyContext(depStory, 50));
|
|
136
|
+
} else {
|
|
137
|
+
const logger = getLogger();
|
|
138
|
+
logger.warn("context", "Dependency story not found in PRD", { dependencyId: depId, referencedBy: story.id });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Add test coverage summary element. */
|
|
144
|
+
async function addTestCoverageElement(
|
|
145
|
+
elements: ContextElement[],
|
|
146
|
+
storyContext: StoryContext,
|
|
147
|
+
story: UserStory,
|
|
148
|
+
): Promise<void> {
|
|
149
|
+
if (storyContext.config?.context?.testCoverage?.enabled === false || !storyContext.workdir) return;
|
|
150
|
+
try {
|
|
151
|
+
const tcConfig = storyContext.config?.context?.testCoverage;
|
|
152
|
+
const contextFiles = getContextFiles(story);
|
|
153
|
+
const scanResult = await generateTestCoverageSummary({
|
|
154
|
+
workdir: storyContext.workdir,
|
|
155
|
+
testDir: tcConfig?.testDir,
|
|
156
|
+
testPattern: tcConfig?.testPattern,
|
|
157
|
+
maxTokens: tcConfig?.maxTokens ?? 500,
|
|
158
|
+
detail: tcConfig?.detail ?? "names-and-counts",
|
|
159
|
+
contextFiles: contextFiles.length > 0 ? contextFiles : undefined,
|
|
160
|
+
scopeToStory: tcConfig?.scopeToStory ?? true,
|
|
161
|
+
});
|
|
162
|
+
if (scanResult.summary) {
|
|
163
|
+
elements.push(createTestCoverageContext(scanResult.summary, scanResult.tokens, 85));
|
|
164
|
+
}
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const logger = getLogger();
|
|
167
|
+
logger.warn("context", "Test coverage scan failed", { error: (error as Error).message });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Add relevant source file elements (auto-detected or from story config). */
|
|
172
|
+
async function addFileElements(
|
|
173
|
+
elements: ContextElement[],
|
|
174
|
+
storyContext: StoryContext,
|
|
175
|
+
story: UserStory,
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
const MAX_FILE_SIZE_BYTES = 10 * 1024;
|
|
178
|
+
const MAX_FILES = 5;
|
|
179
|
+
|
|
180
|
+
let contextFiles = getContextFiles(story);
|
|
181
|
+
|
|
182
|
+
// Auto-detect contextFiles if empty and enabled (BUG-006)
|
|
183
|
+
if (
|
|
184
|
+
contextFiles.length === 0 &&
|
|
185
|
+
storyContext.config?.context?.autoDetect?.enabled !== false &&
|
|
186
|
+
storyContext.workdir
|
|
187
|
+
) {
|
|
188
|
+
const autoDetectConfig = storyContext.config?.context?.autoDetect;
|
|
189
|
+
try {
|
|
190
|
+
const detected = await autoDetectContextFiles({
|
|
191
|
+
workdir: storyContext.workdir,
|
|
192
|
+
storyTitle: story.title,
|
|
193
|
+
maxFiles: autoDetectConfig?.maxFiles ?? 5,
|
|
194
|
+
traceImports: autoDetectConfig?.traceImports ?? false,
|
|
195
|
+
});
|
|
196
|
+
if (detected.length > 0) {
|
|
197
|
+
contextFiles = detected;
|
|
198
|
+
const logger = getLogger();
|
|
199
|
+
logger.info("context", "Auto-detected context files", { storyId: story.id, files: detected });
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
const logger = getLogger();
|
|
203
|
+
logger.warn("context", "Context auto-detection failed", {
|
|
204
|
+
storyId: story.id,
|
|
205
|
+
error: error instanceof Error ? error.message : String(error),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (contextFiles.length === 0) return;
|
|
211
|
+
const filesToLoad = contextFiles.slice(0, MAX_FILES);
|
|
212
|
+
const workdir = storyContext.workdir || process.cwd();
|
|
213
|
+
|
|
214
|
+
for (const relativeFilePath of filesToLoad) {
|
|
215
|
+
try {
|
|
216
|
+
const absolutePath = path.resolve(workdir, relativeFilePath);
|
|
217
|
+
const file = Bun.file(absolutePath);
|
|
218
|
+
if (!(await file.exists())) {
|
|
219
|
+
const logger = getLogger();
|
|
220
|
+
logger.warn("context", "Relevant file not found", { filePath: relativeFilePath, storyId: story.id });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (file.size > MAX_FILE_SIZE_BYTES) {
|
|
224
|
+
const logger = getLogger();
|
|
225
|
+
logger.warn("context", "File too large", {
|
|
226
|
+
filePath: relativeFilePath,
|
|
227
|
+
sizeKB: Math.round(file.size / 1024),
|
|
228
|
+
maxKB: 10,
|
|
229
|
+
storyId: story.id,
|
|
230
|
+
});
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
const content = await file.text();
|
|
234
|
+
const ext = path.extname(relativeFilePath).slice(1) || "txt";
|
|
235
|
+
elements.push(
|
|
236
|
+
createFileContext(relativeFilePath, `\`\`\`${ext}\n// File: ${relativeFilePath}\n${content}\n\`\`\``, 60),
|
|
237
|
+
);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const logger = getLogger();
|
|
240
|
+
logger.warn("context", "Error loading file", {
|
|
241
|
+
filePath: relativeFilePath,
|
|
242
|
+
error: error instanceof Error ? error.message : String(error),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Element Factories
|
|
3
|
+
*
|
|
4
|
+
* Extracted from builder.ts: token estimation and context element creation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getLogger } from "../logger";
|
|
8
|
+
import type { UserStory } from "../prd";
|
|
9
|
+
import type { ContextElement } from "./types";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Approximate character-to-token ratio for token estimation.
|
|
13
|
+
* Value of 3 is a middle ground optimized for mixed content (prose + code + markdown).
|
|
14
|
+
* Slightly overestimates tokens, preventing budget overflow.
|
|
15
|
+
*/
|
|
16
|
+
const CHARS_PER_TOKEN = 3;
|
|
17
|
+
|
|
18
|
+
/** Estimate token count for text using character-to-token ratio. */
|
|
19
|
+
export function estimateTokens(text: string): number {
|
|
20
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Create context element from current story */
|
|
24
|
+
export function createStoryContext(story: UserStory, priority: number): ContextElement {
|
|
25
|
+
const content = formatStoryAsText(story);
|
|
26
|
+
return { type: "story", storyId: story.id, content, priority, tokens: estimateTokens(content) };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Create context element from dependency story */
|
|
30
|
+
export function createDependencyContext(story: UserStory, priority: number): ContextElement {
|
|
31
|
+
const content = formatStoryAsText(story);
|
|
32
|
+
return { type: "dependency", storyId: story.id, content, priority, tokens: estimateTokens(content) };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Create context element from error */
|
|
36
|
+
export function createErrorContext(errorMessage: string, priority: number): ContextElement {
|
|
37
|
+
return { type: "error", content: errorMessage, priority, tokens: estimateTokens(errorMessage) };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Create context element from progress summary */
|
|
41
|
+
export function createProgressContext(progressText: string, priority: number): ContextElement {
|
|
42
|
+
return { type: "progress", content: progressText, priority, tokens: estimateTokens(progressText) };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Create context element from file content */
|
|
46
|
+
export function createFileContext(filePath: string, content: string, priority: number): ContextElement {
|
|
47
|
+
return { type: "file", filePath, content, priority, tokens: estimateTokens(content) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Create context element from test coverage summary */
|
|
51
|
+
export function createTestCoverageContext(content: string, tokens: number, priority: number): ContextElement {
|
|
52
|
+
return { type: "test-coverage", content, priority, tokens };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Format story as text for context (defensive checks for malformed PRD data) */
|
|
56
|
+
export function formatStoryAsText(story: UserStory): string {
|
|
57
|
+
const parts: string[] = [];
|
|
58
|
+
parts.push(`## ${story.id}: ${story.title}`);
|
|
59
|
+
parts.push("");
|
|
60
|
+
parts.push(`**Description:** ${story.description}`);
|
|
61
|
+
parts.push("");
|
|
62
|
+
parts.push("**Acceptance Criteria:**");
|
|
63
|
+
|
|
64
|
+
if (story.acceptanceCriteria && Array.isArray(story.acceptanceCriteria)) {
|
|
65
|
+
for (const ac of story.acceptanceCriteria) {
|
|
66
|
+
parts.push(`- ${ac}`);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
const logger = getLogger();
|
|
70
|
+
logger.warn("context", "Story has invalid acceptanceCriteria", {
|
|
71
|
+
storyId: story.id,
|
|
72
|
+
type: typeof story.acceptanceCriteria,
|
|
73
|
+
});
|
|
74
|
+
parts.push("- (No acceptance criteria defined)");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (story.tags && story.tags.length > 0) {
|
|
78
|
+
parts.push("");
|
|
79
|
+
parts.push(`**Tags:** ${story.tags.join(", ")}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return parts.join("\n");
|
|
83
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Markdown Formatter
|
|
3
|
+
*
|
|
4
|
+
* Extracted from builder.ts: formats built context as markdown for agent consumption.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { BuiltContext, ContextElement } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format built context as markdown for agent consumption.
|
|
11
|
+
*
|
|
12
|
+
* Generates markdown with sections for progress, prior errors,
|
|
13
|
+
* test coverage, current story, dependency stories, and relevant files.
|
|
14
|
+
*/
|
|
15
|
+
export function formatContextAsMarkdown(built: BuiltContext): string {
|
|
16
|
+
const sections: string[] = [];
|
|
17
|
+
|
|
18
|
+
sections.push("# Story Context\n");
|
|
19
|
+
sections.push(`${built.summary}\n`);
|
|
20
|
+
|
|
21
|
+
// Group by type
|
|
22
|
+
const byType = new Map<string, ContextElement[]>();
|
|
23
|
+
for (const element of built.elements) {
|
|
24
|
+
const existing = byType.get(element.type) || [];
|
|
25
|
+
existing.push(element);
|
|
26
|
+
byType.set(element.type, existing);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
renderSection(sections, byType, "progress", "## Progress\n", renderSimple);
|
|
30
|
+
renderErrorSection(sections, byType);
|
|
31
|
+
renderSection(sections, byType, "test-coverage", "", renderSimple);
|
|
32
|
+
renderSection(sections, byType, "story", "## Current Story\n", renderSimple);
|
|
33
|
+
renderSection(sections, byType, "dependency", "## Dependency Stories\n", renderSimple);
|
|
34
|
+
renderSection(sections, byType, "file", "## Relevant Source Files\n", renderSimple);
|
|
35
|
+
|
|
36
|
+
return sections.join("\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Render a simple section with elements as-is. */
|
|
40
|
+
function renderSimple(sections: string[], elements: ContextElement[]): void {
|
|
41
|
+
for (const element of elements) {
|
|
42
|
+
sections.push(element.content);
|
|
43
|
+
sections.push("\n");
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Render a typed section if elements exist. */
|
|
48
|
+
function renderSection(
|
|
49
|
+
sections: string[],
|
|
50
|
+
byType: Map<string, ContextElement[]>,
|
|
51
|
+
type: string,
|
|
52
|
+
header: string,
|
|
53
|
+
renderer: (sections: string[], elements: ContextElement[]) => void,
|
|
54
|
+
): void {
|
|
55
|
+
const elements = byType.get(type);
|
|
56
|
+
if (!elements || elements.length === 0) return;
|
|
57
|
+
if (header) sections.push(header);
|
|
58
|
+
renderer(sections, elements);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Render error section with special handling for ASSET_CHECK errors. */
|
|
62
|
+
function renderErrorSection(sections: string[], byType: Map<string, ContextElement[]>): void {
|
|
63
|
+
const errorElements = byType.get("error");
|
|
64
|
+
if (!errorElements || errorElements.length === 0) return;
|
|
65
|
+
|
|
66
|
+
const assetCheckErrors: ContextElement[] = [];
|
|
67
|
+
const otherErrors: ContextElement[] = [];
|
|
68
|
+
|
|
69
|
+
for (const element of errorElements) {
|
|
70
|
+
if (element.content.startsWith("ASSET_CHECK_FAILED:")) {
|
|
71
|
+
assetCheckErrors.push(element);
|
|
72
|
+
} else {
|
|
73
|
+
otherErrors.push(element);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (assetCheckErrors.length > 0) {
|
|
78
|
+
sections.push("## ⚠️ MANDATORY: Missing Files from Previous Attempts\n");
|
|
79
|
+
sections.push("**CRITICAL:** Previous attempts failed because these files were not created.\n");
|
|
80
|
+
sections.push("You MUST create these exact files. Do NOT use alternative filenames.\n\n");
|
|
81
|
+
|
|
82
|
+
for (const element of assetCheckErrors) {
|
|
83
|
+
const match = element.content.match(/Missing files: \[([^\]]+)\]/);
|
|
84
|
+
if (match) {
|
|
85
|
+
const fileList = match[1].split(",").map((f) => f.trim());
|
|
86
|
+
sections.push("**Required files:**\n");
|
|
87
|
+
for (const file of fileList) {
|
|
88
|
+
sections.push(`- \`${file}\``);
|
|
89
|
+
}
|
|
90
|
+
sections.push("\n");
|
|
91
|
+
} else {
|
|
92
|
+
sections.push("```");
|
|
93
|
+
sections.push(element.content);
|
|
94
|
+
sections.push("```\n");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (otherErrors.length > 0) {
|
|
100
|
+
sections.push("## Prior Errors\n");
|
|
101
|
+
for (const element of otherErrors) {
|
|
102
|
+
sections.push("```");
|
|
103
|
+
sections.push(element.content);
|
|
104
|
+
sections.push("```\n");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|