@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,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context Generator Types (v0.16.1)
|
|
3
|
+
*
|
|
4
|
+
* Types for generating agent config files from nax/context.md.
|
|
5
|
+
* Replaces ConstitutionContent from the old constitution generator.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Auto-injected project metadata */
|
|
9
|
+
export interface ProjectMetadata {
|
|
10
|
+
/** Project name from manifest file */
|
|
11
|
+
name?: string;
|
|
12
|
+
/** Detected language/runtime (e.g. "TypeScript", "Go", "Rust", "Python") */
|
|
13
|
+
language?: string;
|
|
14
|
+
/** Key dependencies (framework, ORM, test runner, etc.) */
|
|
15
|
+
dependencies: string[];
|
|
16
|
+
/** Test command from nax config */
|
|
17
|
+
testCommand?: string;
|
|
18
|
+
/** Lint command from nax config */
|
|
19
|
+
lintCommand?: string;
|
|
20
|
+
/** Typecheck command from nax config */
|
|
21
|
+
typecheckCommand?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Context content passed to generators */
|
|
25
|
+
export interface ContextContent {
|
|
26
|
+
/** Raw markdown from nax/context.md */
|
|
27
|
+
markdown: string;
|
|
28
|
+
/** Auto-injected project metadata (if enabled) */
|
|
29
|
+
metadata?: ProjectMetadata;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Agent config generator interface */
|
|
33
|
+
export interface AgentContextGenerator {
|
|
34
|
+
/** Generator name (e.g., 'claude', 'opencode', 'cursor') */
|
|
35
|
+
name: string;
|
|
36
|
+
/** Output filename (e.g., 'CLAUDE.md', '.cursorrules') */
|
|
37
|
+
outputFile: string;
|
|
38
|
+
/** Generate agent-specific config file content from context */
|
|
39
|
+
generate(context: ContextContent): string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** All available generator types */
|
|
43
|
+
export type AgentType = "claude" | "opencode" | "cursor" | "windsurf" | "aider";
|
|
44
|
+
|
|
45
|
+
/** Generator registry map */
|
|
46
|
+
export type GeneratorMap = Record<AgentType, AgentContextGenerator>;
|
|
47
|
+
|
|
48
|
+
/** A single context element (file content, error, story summary, etc.) */
|
|
49
|
+
export interface ContextElement {
|
|
50
|
+
/** Element type identifier */
|
|
51
|
+
type: string;
|
|
52
|
+
/** Content text */
|
|
53
|
+
content: string;
|
|
54
|
+
/** Estimated token count */
|
|
55
|
+
tokens: number;
|
|
56
|
+
/** Priority (higher = selected first when budgeting) */
|
|
57
|
+
priority: number;
|
|
58
|
+
/** Story ID (for story/dependency elements) */
|
|
59
|
+
storyId?: string;
|
|
60
|
+
/** File path (for file elements) */
|
|
61
|
+
filePath?: string;
|
|
62
|
+
/** Human-readable label (optional) */
|
|
63
|
+
label?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Token budget for context building */
|
|
67
|
+
export interface ContextBudget {
|
|
68
|
+
/** Total token limit */
|
|
69
|
+
maxTokens: number;
|
|
70
|
+
/** Tokens reserved for instructions/system prompt */
|
|
71
|
+
reservedForInstructions: number;
|
|
72
|
+
/** Tokens available for context elements */
|
|
73
|
+
availableForContext: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Input to the context builder */
|
|
77
|
+
export interface StoryContext {
|
|
78
|
+
/** PRD containing all stories */
|
|
79
|
+
prd: import("../prd/types").PRD;
|
|
80
|
+
/** ID of the current story being worked on */
|
|
81
|
+
currentStoryId: string;
|
|
82
|
+
/** Working directory for file scanning */
|
|
83
|
+
workdir?: string;
|
|
84
|
+
/** nax config (for context settings) */
|
|
85
|
+
config?: import("../config").NaxConfig;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Output of the context builder */
|
|
89
|
+
export interface BuiltContext {
|
|
90
|
+
/** Selected context elements (within budget) */
|
|
91
|
+
elements: ContextElement[];
|
|
92
|
+
/** Total tokens used */
|
|
93
|
+
totalTokens: number;
|
|
94
|
+
/** Whether some elements were truncated due to budget */
|
|
95
|
+
truncated: boolean;
|
|
96
|
+
/** Human-readable summary */
|
|
97
|
+
summary: string;
|
|
98
|
+
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed Error Classes for nax
|
|
3
|
+
*
|
|
4
|
+
* Replaces process.exit(1) patterns with structured errors that can be caught
|
|
5
|
+
* and handled by the CLI layer or tests.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Base error class for all nax errors.
|
|
10
|
+
*/
|
|
11
|
+
export class NaxError extends Error {
|
|
12
|
+
constructor(
|
|
13
|
+
message: string,
|
|
14
|
+
public readonly code: string,
|
|
15
|
+
public readonly context?: Record<string, unknown>,
|
|
16
|
+
) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "NaxError";
|
|
19
|
+
Error.captureStackTrace(this, this.constructor);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Agent not found or not installed.
|
|
25
|
+
*/
|
|
26
|
+
export class AgentNotFoundError extends NaxError {
|
|
27
|
+
constructor(agentName: string, binary?: string) {
|
|
28
|
+
super(`Agent "${agentName}" not found or not installed`, "AGENT_NOT_FOUND", { agentName, binary });
|
|
29
|
+
this.name = "AgentNotFoundError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Agent binary not in PATH.
|
|
35
|
+
*/
|
|
36
|
+
export class AgentNotInstalledError extends NaxError {
|
|
37
|
+
constructor(agentName: string, binary: string) {
|
|
38
|
+
super(`Agent "${agentName}" is not installed or not in PATH: ${binary}`, "AGENT_NOT_INSTALLED", {
|
|
39
|
+
agentName,
|
|
40
|
+
binary,
|
|
41
|
+
});
|
|
42
|
+
this.name = "AgentNotInstalledError";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Feature exceeds story limit.
|
|
48
|
+
*/
|
|
49
|
+
export class StoryLimitExceededError extends NaxError {
|
|
50
|
+
constructor(totalStories: number, limit: number) {
|
|
51
|
+
super(`Feature exceeds story limit: ${totalStories} stories (max: ${limit})`, "STORY_LIMIT_EXCEEDED", {
|
|
52
|
+
totalStories,
|
|
53
|
+
limit,
|
|
54
|
+
});
|
|
55
|
+
this.name = "StoryLimitExceededError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Another nax process is already running.
|
|
61
|
+
*/
|
|
62
|
+
export class LockAcquisitionError extends NaxError {
|
|
63
|
+
constructor(workdir: string) {
|
|
64
|
+
super("Another nax process is already running in this directory", "LOCK_ACQUISITION_FAILED", { workdir });
|
|
65
|
+
this.name = "LockAcquisitionError";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story batching logic
|
|
3
|
+
*
|
|
4
|
+
* Groups consecutive simple-complexity stories into batches for efficient execution.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { UserStory } from "../prd";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default maximum number of stories per batch.
|
|
11
|
+
*
|
|
12
|
+
* Rationale:
|
|
13
|
+
* - Batch size must balance efficiency vs. blast radius
|
|
14
|
+
* - 4 stories is optimal for most simple tasks (e.g., add 4 similar util functions)
|
|
15
|
+
* - Keeps prompts manageable (~1500 tokens per story = ~6000 tokens total context)
|
|
16
|
+
* - If one story in batch fails, only 3 others retry at next tier (acceptable waste)
|
|
17
|
+
* - Larger batches (8+) increase risk of cascading failures and context overload
|
|
18
|
+
*
|
|
19
|
+
* This default can be overridden via config or function parameter.
|
|
20
|
+
*/
|
|
21
|
+
const DEFAULT_MAX_BATCH_SIZE = 4;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Story batch for grouped execution
|
|
25
|
+
*/
|
|
26
|
+
export interface StoryBatch {
|
|
27
|
+
/** Stories in this batch */
|
|
28
|
+
stories: UserStory[];
|
|
29
|
+
/** True if this is a batch of multiple stories, false if single story */
|
|
30
|
+
isBatch: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Group consecutive simple-complexity stories into batches (max 4 per batch).
|
|
35
|
+
* Non-simple stories execute individually.
|
|
36
|
+
*
|
|
37
|
+
* @param stories - Array of user stories to batch
|
|
38
|
+
* @param maxBatchSize - Maximum stories per batch (default: 4)
|
|
39
|
+
* @returns Array of story batches
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```typescript
|
|
43
|
+
* const stories = [simpleStory1, simpleStory2, complexStory, simpleStory3];
|
|
44
|
+
* const batches = groupStoriesIntoBatches(stories);
|
|
45
|
+
* // Returns: [
|
|
46
|
+
* // { stories: [simpleStory1, simpleStory2], isBatch: true },
|
|
47
|
+
* // { stories: [complexStory], isBatch: false },
|
|
48
|
+
* // { stories: [simpleStory3], isBatch: false }
|
|
49
|
+
* // ]
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function groupStoriesIntoBatches(stories: UserStory[], maxBatchSize = DEFAULT_MAX_BATCH_SIZE): StoryBatch[] {
|
|
53
|
+
const batches: StoryBatch[] = [];
|
|
54
|
+
let currentBatch: UserStory[] = [];
|
|
55
|
+
|
|
56
|
+
for (const story of stories) {
|
|
57
|
+
const isSimple = story.routing?.complexity === "simple";
|
|
58
|
+
|
|
59
|
+
if (isSimple && currentBatch.length < maxBatchSize) {
|
|
60
|
+
// Add to current batch
|
|
61
|
+
currentBatch.push(story);
|
|
62
|
+
} else {
|
|
63
|
+
// Flush current batch if it exists
|
|
64
|
+
if (currentBatch.length > 0) {
|
|
65
|
+
batches.push({
|
|
66
|
+
stories: [...currentBatch],
|
|
67
|
+
isBatch: currentBatch.length > 1,
|
|
68
|
+
});
|
|
69
|
+
currentBatch = [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Add non-simple story as individual batch
|
|
73
|
+
if (!isSimple) {
|
|
74
|
+
batches.push({
|
|
75
|
+
stories: [story],
|
|
76
|
+
isBatch: false,
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
// Start new batch with this simple story
|
|
80
|
+
currentBatch.push(story);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Flush remaining batch
|
|
86
|
+
if (currentBatch.length > 0) {
|
|
87
|
+
batches.push({
|
|
88
|
+
stories: [...currentBatch],
|
|
89
|
+
isBatch: currentBatch.length > 1,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return batches;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Precompute the full batch plan from ready stories.
|
|
98
|
+
* This eliminates O(n²) re-checking by computing all batches upfront.
|
|
99
|
+
* Maintains original story order from PRD.
|
|
100
|
+
*
|
|
101
|
+
* @param stories - Array of ready user stories (already filtered for dependencies)
|
|
102
|
+
* @param maxBatchSize - Maximum stories per batch (default: 4)
|
|
103
|
+
* @returns Array of story batches ready for sequential execution
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```typescript
|
|
107
|
+
* const readyStories = getAllReadyStories(prd);
|
|
108
|
+
* const batchPlan = precomputeBatchPlan(readyStories);
|
|
109
|
+
* // Iterate through batches sequentially
|
|
110
|
+
* for (const batch of batchPlan) {
|
|
111
|
+
* await executeBatch(batch);
|
|
112
|
+
* }
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export function precomputeBatchPlan(stories: UserStory[], maxBatchSize = DEFAULT_MAX_BATCH_SIZE): StoryBatch[] {
|
|
116
|
+
const batches: StoryBatch[] = [];
|
|
117
|
+
let currentBatch: UserStory[] = [];
|
|
118
|
+
|
|
119
|
+
for (const story of stories) {
|
|
120
|
+
const isSimple = story.routing?.complexity === "simple" && story.routing?.testStrategy === "test-after";
|
|
121
|
+
|
|
122
|
+
if (isSimple && currentBatch.length < maxBatchSize) {
|
|
123
|
+
// Add to current batch
|
|
124
|
+
currentBatch.push(story);
|
|
125
|
+
} else {
|
|
126
|
+
// Flush current batch if it exists
|
|
127
|
+
if (currentBatch.length > 0) {
|
|
128
|
+
batches.push({
|
|
129
|
+
stories: [...currentBatch],
|
|
130
|
+
isBatch: currentBatch.length > 1,
|
|
131
|
+
});
|
|
132
|
+
currentBatch = [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Add non-simple story as individual batch
|
|
136
|
+
if (!isSimple) {
|
|
137
|
+
batches.push({
|
|
138
|
+
stories: [story],
|
|
139
|
+
isBatch: false,
|
|
140
|
+
});
|
|
141
|
+
} else {
|
|
142
|
+
// Start new batch with this simple story
|
|
143
|
+
currentBatch.push(story);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Flush remaining batch
|
|
149
|
+
if (currentBatch.length > 0) {
|
|
150
|
+
batches.push({
|
|
151
|
+
stories: [...currentBatch],
|
|
152
|
+
isBatch: currentBatch.length > 1,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return batches;
|
|
157
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash Recovery — Signal handlers, heartbeat, and exit summary
|
|
3
|
+
*
|
|
4
|
+
* Implements US-007:
|
|
5
|
+
* - SIGTERM/SIGINT/SIGHUP handlers
|
|
6
|
+
* - Uncaught exception handlers
|
|
7
|
+
* - Fatal log + status.json update to "crashed"
|
|
8
|
+
* - Heartbeat every 60s during agent execution
|
|
9
|
+
* - Exit summary entry on normal exit
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getSafeLogger } from "../logger";
|
|
13
|
+
import type { PidRegistry } from "./pid-registry";
|
|
14
|
+
import type { StatusWriter } from "./status-writer";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Crash recovery context — dependencies injected at setup
|
|
18
|
+
* (BUG-1 fix: use getters to avoid capturing stale closure values)
|
|
19
|
+
*/
|
|
20
|
+
export interface CrashRecoveryContext {
|
|
21
|
+
statusWriter: StatusWriter;
|
|
22
|
+
getTotalCost: () => number;
|
|
23
|
+
getIterations: () => number;
|
|
24
|
+
jsonlFilePath?: string;
|
|
25
|
+
pidRegistry?: PidRegistry;
|
|
26
|
+
// BUG-017: Additional context for run.complete event on SIGTERM
|
|
27
|
+
runId?: string;
|
|
28
|
+
feature?: string;
|
|
29
|
+
getStartTime?: () => number;
|
|
30
|
+
getTotalStories?: () => number;
|
|
31
|
+
getStoriesCompleted?: () => number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Heartbeat timer handle (for cleanup)
|
|
36
|
+
*/
|
|
37
|
+
let heartbeatTimer: Timer | null = null;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Track whether crash handlers have been installed
|
|
41
|
+
*/
|
|
42
|
+
let handlersInstalled = false;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Write fatal log entry to JSONL file
|
|
46
|
+
*/
|
|
47
|
+
async function writeFatalLog(jsonlFilePath: string | undefined, signal: string, error?: Error): Promise<void> {
|
|
48
|
+
if (!jsonlFilePath) return;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const fatalEntry = {
|
|
52
|
+
timestamp: new Date().toISOString(),
|
|
53
|
+
level: "error",
|
|
54
|
+
stage: "crash-recovery",
|
|
55
|
+
message: error ? `Uncaught exception: ${error.message}` : `Process terminated by ${signal}`,
|
|
56
|
+
data: {
|
|
57
|
+
signal,
|
|
58
|
+
...(error && {
|
|
59
|
+
stack: error.stack,
|
|
60
|
+
name: error.name,
|
|
61
|
+
}),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const line = `${JSON.stringify(fatalEntry)}\n`;
|
|
66
|
+
// Use appendFileSync from node:fs to ensure file is created if it doesn't exist
|
|
67
|
+
const { appendFileSync } = await import("node:fs");
|
|
68
|
+
appendFileSync(jsonlFilePath, line, "utf8");
|
|
69
|
+
} catch (err) {
|
|
70
|
+
console.error("[crash-recovery] Failed to write fatal log:", err);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Write run.complete event to JSONL file (BUG-017)
|
|
76
|
+
* Called on SIGTERM to emit final run summary before exit
|
|
77
|
+
*/
|
|
78
|
+
async function writeRunComplete(ctx: CrashRecoveryContext, exitReason: string): Promise<void> {
|
|
79
|
+
if (!ctx.jsonlFilePath || !ctx.runId || !ctx.feature) return;
|
|
80
|
+
|
|
81
|
+
const logger = getSafeLogger();
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const totalCost = ctx.getTotalCost();
|
|
85
|
+
const iterations = ctx.getIterations();
|
|
86
|
+
const startTime = ctx.getStartTime?.() ?? Date.now();
|
|
87
|
+
const durationMs = Date.now() - startTime;
|
|
88
|
+
const totalStories = ctx.getTotalStories?.() ?? 0;
|
|
89
|
+
const storiesCompleted = ctx.getStoriesCompleted?.() ?? 0;
|
|
90
|
+
|
|
91
|
+
const runCompleteEntry = {
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
level: "info",
|
|
94
|
+
stage: "run.complete",
|
|
95
|
+
message: "Feature execution terminated",
|
|
96
|
+
data: {
|
|
97
|
+
runId: ctx.runId,
|
|
98
|
+
feature: ctx.feature,
|
|
99
|
+
success: false,
|
|
100
|
+
exitReason,
|
|
101
|
+
totalCost,
|
|
102
|
+
iterations,
|
|
103
|
+
totalStories,
|
|
104
|
+
storiesCompleted,
|
|
105
|
+
durationMs,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const line = `${JSON.stringify(runCompleteEntry)}\n`;
|
|
110
|
+
const { appendFileSync } = await import("node:fs");
|
|
111
|
+
appendFileSync(ctx.jsonlFilePath, line, "utf8");
|
|
112
|
+
logger?.debug("crash-recovery", "run.complete event written", { exitReason });
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error("[crash-recovery] Failed to write run.complete event:", err);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Update status.json to "crashed" state
|
|
120
|
+
*/
|
|
121
|
+
async function updateStatusToCrashed(
|
|
122
|
+
statusWriter: StatusWriter,
|
|
123
|
+
totalCost: number,
|
|
124
|
+
iterations: number,
|
|
125
|
+
signal: string,
|
|
126
|
+
): Promise<void> {
|
|
127
|
+
try {
|
|
128
|
+
statusWriter.setRunStatus("crashed");
|
|
129
|
+
await statusWriter.update(totalCost, iterations, {
|
|
130
|
+
crashedAt: new Date().toISOString(),
|
|
131
|
+
crashSignal: signal,
|
|
132
|
+
});
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.error("[crash-recovery] Failed to update status.json:", err);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Install signal handlers for crash recovery
|
|
140
|
+
* (MEM-1 fix: return cleanup function to unregister handlers)
|
|
141
|
+
*/
|
|
142
|
+
export function installCrashHandlers(ctx: CrashRecoveryContext): () => void {
|
|
143
|
+
if (handlersInstalled) {
|
|
144
|
+
return () => {}; // Prevent duplicate installations
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const logger = getSafeLogger();
|
|
148
|
+
|
|
149
|
+
// Signal handler
|
|
150
|
+
const handleSignal = async (signal: NodeJS.Signals) => {
|
|
151
|
+
logger?.error("crash-recovery", `Received ${signal}, shutting down...`, { signal });
|
|
152
|
+
|
|
153
|
+
// Kill all spawned agent processes
|
|
154
|
+
if (ctx.pidRegistry) {
|
|
155
|
+
await ctx.pidRegistry.killAll();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Write fatal log
|
|
159
|
+
await writeFatalLog(ctx.jsonlFilePath, signal);
|
|
160
|
+
|
|
161
|
+
// Write run.complete event (BUG-017)
|
|
162
|
+
await writeRunComplete(ctx, signal.toLowerCase());
|
|
163
|
+
|
|
164
|
+
// Update status.json to crashed
|
|
165
|
+
await updateStatusToCrashed(ctx.statusWriter, ctx.getTotalCost(), ctx.getIterations(), signal);
|
|
166
|
+
|
|
167
|
+
// Stop heartbeat
|
|
168
|
+
stopHeartbeat();
|
|
169
|
+
|
|
170
|
+
// Exit cleanly
|
|
171
|
+
process.exit(128 + getSignalNumber(signal));
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const sigtermHandler = () => handleSignal("SIGTERM");
|
|
175
|
+
const sigintHandler = () => handleSignal("SIGINT");
|
|
176
|
+
const sighupHandler = () => handleSignal("SIGHUP");
|
|
177
|
+
|
|
178
|
+
// Install signal handlers
|
|
179
|
+
process.on("SIGTERM", sigtermHandler);
|
|
180
|
+
process.on("SIGINT", sigintHandler);
|
|
181
|
+
process.on("SIGHUP", sighupHandler);
|
|
182
|
+
|
|
183
|
+
// Uncaught exception handler
|
|
184
|
+
const uncaughtExceptionHandler = async (error: Error) => {
|
|
185
|
+
logger?.error("crash-recovery", "Uncaught exception", {
|
|
186
|
+
error: error.message,
|
|
187
|
+
stack: error.stack,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Kill all spawned agent processes
|
|
191
|
+
if (ctx.pidRegistry) {
|
|
192
|
+
await ctx.pidRegistry.killAll();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Write fatal log with stack trace
|
|
196
|
+
await writeFatalLog(ctx.jsonlFilePath, "uncaughtException", error);
|
|
197
|
+
|
|
198
|
+
// Update status.json to crashed
|
|
199
|
+
await updateStatusToCrashed(ctx.statusWriter, ctx.getTotalCost(), ctx.getIterations(), "uncaughtException");
|
|
200
|
+
|
|
201
|
+
// Stop heartbeat
|
|
202
|
+
stopHeartbeat();
|
|
203
|
+
|
|
204
|
+
// Exit with error code
|
|
205
|
+
process.exit(1);
|
|
206
|
+
};
|
|
207
|
+
process.on("uncaughtException", uncaughtExceptionHandler);
|
|
208
|
+
|
|
209
|
+
// Unhandled promise rejection handler
|
|
210
|
+
const unhandledRejectionHandler = async (reason: unknown, promise: Promise<unknown>) => {
|
|
211
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
212
|
+
logger?.error("crash-recovery", "Unhandled promise rejection", {
|
|
213
|
+
error: error.message,
|
|
214
|
+
stack: error.stack,
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// Kill all spawned agent processes
|
|
218
|
+
if (ctx.pidRegistry) {
|
|
219
|
+
await ctx.pidRegistry.killAll();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Write fatal log
|
|
223
|
+
await writeFatalLog(ctx.jsonlFilePath, "unhandledRejection", error);
|
|
224
|
+
|
|
225
|
+
// Update status.json to crashed
|
|
226
|
+
await updateStatusToCrashed(ctx.statusWriter, ctx.getTotalCost(), ctx.getIterations(), "unhandledRejection");
|
|
227
|
+
|
|
228
|
+
// Stop heartbeat
|
|
229
|
+
stopHeartbeat();
|
|
230
|
+
|
|
231
|
+
// Exit with error code
|
|
232
|
+
process.exit(1);
|
|
233
|
+
};
|
|
234
|
+
process.on("unhandledRejection", unhandledRejectionHandler);
|
|
235
|
+
|
|
236
|
+
handlersInstalled = true;
|
|
237
|
+
logger?.debug("crash-recovery", "Crash handlers installed");
|
|
238
|
+
|
|
239
|
+
// Return cleanup function
|
|
240
|
+
return () => {
|
|
241
|
+
process.removeListener("SIGTERM", sigtermHandler);
|
|
242
|
+
process.removeListener("SIGINT", sigintHandler);
|
|
243
|
+
process.removeListener("SIGHUP", sighupHandler);
|
|
244
|
+
process.removeListener("uncaughtException", uncaughtExceptionHandler);
|
|
245
|
+
process.removeListener("unhandledRejection", unhandledRejectionHandler);
|
|
246
|
+
handlersInstalled = false;
|
|
247
|
+
logger?.debug("crash-recovery", "Crash handlers unregistered");
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Start heartbeat timer (60s interval)
|
|
253
|
+
*/
|
|
254
|
+
export function startHeartbeat(
|
|
255
|
+
statusWriter: StatusWriter,
|
|
256
|
+
getTotalCost: () => number,
|
|
257
|
+
getIterations: () => number,
|
|
258
|
+
jsonlFilePath?: string,
|
|
259
|
+
): void {
|
|
260
|
+
const logger = getSafeLogger();
|
|
261
|
+
|
|
262
|
+
// Stop any existing heartbeat first
|
|
263
|
+
stopHeartbeat();
|
|
264
|
+
|
|
265
|
+
heartbeatTimer = setInterval(async () => {
|
|
266
|
+
logger?.debug("crash-recovery", "Heartbeat");
|
|
267
|
+
|
|
268
|
+
// Write heartbeat to JSONL
|
|
269
|
+
if (jsonlFilePath) {
|
|
270
|
+
try {
|
|
271
|
+
const heartbeatEntry = {
|
|
272
|
+
timestamp: new Date().toISOString(),
|
|
273
|
+
level: "debug",
|
|
274
|
+
stage: "heartbeat",
|
|
275
|
+
message: "Process alive",
|
|
276
|
+
data: {
|
|
277
|
+
pid: process.pid,
|
|
278
|
+
memoryUsageMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024),
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
const line = `${JSON.stringify(heartbeatEntry)}\n`;
|
|
282
|
+
const { appendFileSync } = await import("node:fs");
|
|
283
|
+
appendFileSync(jsonlFilePath, line, "utf8");
|
|
284
|
+
} catch (err) {
|
|
285
|
+
logger?.warn("crash-recovery", "Failed to write heartbeat", { error: (err as Error).message });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Update status.json (no-op if nothing changed, but keeps lastHeartbeat fresh)
|
|
290
|
+
try {
|
|
291
|
+
await statusWriter.update(getTotalCost(), getIterations(), {
|
|
292
|
+
lastHeartbeat: new Date().toISOString(),
|
|
293
|
+
});
|
|
294
|
+
} catch (err) {
|
|
295
|
+
logger?.warn("crash-recovery", "Failed to update status during heartbeat", {
|
|
296
|
+
error: (err as Error).message,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}, 60_000); // 60 seconds
|
|
300
|
+
|
|
301
|
+
logger?.debug("crash-recovery", "Heartbeat started (60s interval)");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Stop heartbeat timer
|
|
306
|
+
*/
|
|
307
|
+
export function stopHeartbeat(): void {
|
|
308
|
+
if (heartbeatTimer) {
|
|
309
|
+
clearInterval(heartbeatTimer);
|
|
310
|
+
heartbeatTimer = null;
|
|
311
|
+
getSafeLogger()?.debug("crash-recovery", "Heartbeat stopped");
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Write exit summary entry to JSONL
|
|
317
|
+
*/
|
|
318
|
+
export async function writeExitSummary(
|
|
319
|
+
jsonlFilePath: string | undefined,
|
|
320
|
+
totalCost: number,
|
|
321
|
+
iterations: number,
|
|
322
|
+
storiesCompleted: number,
|
|
323
|
+
durationMs: number,
|
|
324
|
+
): Promise<void> {
|
|
325
|
+
if (!jsonlFilePath) return;
|
|
326
|
+
|
|
327
|
+
const logger = getSafeLogger();
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const summaryEntry = {
|
|
331
|
+
timestamp: new Date().toISOString(),
|
|
332
|
+
level: "info",
|
|
333
|
+
stage: "exit-summary",
|
|
334
|
+
message: "Run completed",
|
|
335
|
+
data: {
|
|
336
|
+
totalCost,
|
|
337
|
+
iterations,
|
|
338
|
+
storiesCompleted,
|
|
339
|
+
durationMs,
|
|
340
|
+
exitedCleanly: true,
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const line = `${JSON.stringify(summaryEntry)}\n`;
|
|
345
|
+
// Use appendFileSync from node:fs to ensure file is created if it doesn't exist
|
|
346
|
+
const { appendFileSync } = await import("node:fs");
|
|
347
|
+
appendFileSync(jsonlFilePath, line, "utf8");
|
|
348
|
+
logger?.debug("crash-recovery", "Exit summary written");
|
|
349
|
+
} catch (err) {
|
|
350
|
+
logger?.warn("crash-recovery", "Failed to write exit summary", { error: (err as Error).message });
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get numeric signal number for exit code
|
|
356
|
+
*/
|
|
357
|
+
function getSignalNumber(signal: NodeJS.Signals): number {
|
|
358
|
+
const signalMap: Record<string, number> = {
|
|
359
|
+
SIGTERM: 15,
|
|
360
|
+
SIGINT: 2,
|
|
361
|
+
SIGHUP: 1,
|
|
362
|
+
};
|
|
363
|
+
return signalMap[signal] ?? 15;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Reset handlers (for testing)
|
|
368
|
+
* @internal
|
|
369
|
+
*/
|
|
370
|
+
export function resetCrashHandlers(): void {
|
|
371
|
+
handlersInstalled = false;
|
|
372
|
+
stopHeartbeat();
|
|
373
|
+
}
|