@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,400 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel Execution — Worktree-based concurrent story execution
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates parallel story execution using git worktrees: groups stories
|
|
5
|
+
* by dependencies, creates worktrees, dispatches concurrent pipelines,
|
|
6
|
+
* merges in dependency order, and cleans up worktrees.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import type { NaxConfig } from "../config";
|
|
12
|
+
import type { LoadedHooksConfig } from "../hooks";
|
|
13
|
+
import { getSafeLogger } from "../logger";
|
|
14
|
+
import type { PipelineEventEmitter } from "../pipeline/events";
|
|
15
|
+
import { runPipeline } from "../pipeline/runner";
|
|
16
|
+
import { defaultPipeline } from "../pipeline/stages";
|
|
17
|
+
import type { PipelineContext, RoutingResult } from "../pipeline/types";
|
|
18
|
+
import type { PluginRegistry } from "../plugins/registry";
|
|
19
|
+
import type { PRD, UserStory } from "../prd";
|
|
20
|
+
import { markStoryFailed, markStoryPassed, savePRD } from "../prd";
|
|
21
|
+
import { routeTask } from "../routing";
|
|
22
|
+
import { WorktreeManager } from "../worktree/manager";
|
|
23
|
+
import { MergeEngine, type StoryDependencies } from "../worktree/merge";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Result from parallel execution of a batch of stories
|
|
27
|
+
*/
|
|
28
|
+
export interface ParallelBatchResult {
|
|
29
|
+
/** Stories that completed successfully */
|
|
30
|
+
successfulStories: UserStory[];
|
|
31
|
+
/** Stories that failed */
|
|
32
|
+
failedStories: Array<{ story: UserStory; error: string }>;
|
|
33
|
+
/** Total cost accumulated */
|
|
34
|
+
totalCost: number;
|
|
35
|
+
/** Stories with merge conflicts */
|
|
36
|
+
conflictedStories: Array<{ storyId: string; conflictFiles: string[] }>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Group stories into dependency batches; stories in each batch can run in parallel.
|
|
41
|
+
*/
|
|
42
|
+
function groupStoriesByDependencies(stories: UserStory[]): UserStory[][] {
|
|
43
|
+
const batches: UserStory[][] = [];
|
|
44
|
+
const processed = new Set<string>();
|
|
45
|
+
const storyMap = new Map(stories.map((s) => [s.id, s]));
|
|
46
|
+
|
|
47
|
+
// Keep processing until all stories are batched
|
|
48
|
+
while (processed.size < stories.length) {
|
|
49
|
+
const batch: UserStory[] = [];
|
|
50
|
+
|
|
51
|
+
for (const story of stories) {
|
|
52
|
+
if (processed.has(story.id)) continue;
|
|
53
|
+
|
|
54
|
+
// Check if all dependencies are satisfied
|
|
55
|
+
const depsCompleted = story.dependencies.every((dep) => processed.has(dep) || !storyMap.has(dep));
|
|
56
|
+
|
|
57
|
+
if (depsCompleted) {
|
|
58
|
+
batch.push(story);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (batch.length === 0) {
|
|
63
|
+
// No stories ready — circular dependency or missing dep
|
|
64
|
+
const remaining = stories.filter((s) => !processed.has(s.id));
|
|
65
|
+
const logger = getSafeLogger();
|
|
66
|
+
logger?.error("parallel", "Cannot resolve story dependencies", {
|
|
67
|
+
remainingStories: remaining.map((s) => s.id),
|
|
68
|
+
});
|
|
69
|
+
throw new Error("Circular dependency or missing dependency detected");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Mark batch stories as processed
|
|
73
|
+
for (const story of batch) {
|
|
74
|
+
processed.add(story.id);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
batches.push(batch);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return batches;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build dependency map for merge engine
|
|
85
|
+
*/
|
|
86
|
+
function buildDependencyMap(stories: UserStory[]): StoryDependencies {
|
|
87
|
+
const deps: StoryDependencies = {};
|
|
88
|
+
for (const story of stories) {
|
|
89
|
+
deps[story.id] = story.dependencies;
|
|
90
|
+
}
|
|
91
|
+
return deps;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Execute a single story in its worktree
|
|
96
|
+
*/
|
|
97
|
+
async function executeStoryInWorktree(
|
|
98
|
+
story: UserStory,
|
|
99
|
+
worktreePath: string,
|
|
100
|
+
context: Omit<PipelineContext, "story" | "stories" | "workdir" | "routing">,
|
|
101
|
+
routing: RoutingResult,
|
|
102
|
+
eventEmitter?: PipelineEventEmitter,
|
|
103
|
+
): Promise<{ success: boolean; cost: number; error?: string }> {
|
|
104
|
+
const logger = getSafeLogger();
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const pipelineContext: PipelineContext = {
|
|
108
|
+
...context,
|
|
109
|
+
story,
|
|
110
|
+
stories: [story],
|
|
111
|
+
workdir: worktreePath,
|
|
112
|
+
routing,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
logger?.debug("parallel", "Executing story in worktree", {
|
|
116
|
+
storyId: story.id,
|
|
117
|
+
worktreePath,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const result = await runPipeline(defaultPipeline, pipelineContext, eventEmitter);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
success: result.success,
|
|
124
|
+
cost: result.context.agentResult?.estimatedCost || 0,
|
|
125
|
+
error: result.success ? undefined : result.reason,
|
|
126
|
+
};
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
success: false,
|
|
130
|
+
cost: 0,
|
|
131
|
+
error: error instanceof Error ? error.message : String(error),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Execute a batch of independent stories in parallel
|
|
138
|
+
*/
|
|
139
|
+
async function executeParallelBatch(
|
|
140
|
+
stories: UserStory[],
|
|
141
|
+
projectRoot: string,
|
|
142
|
+
config: NaxConfig,
|
|
143
|
+
prd: PRD,
|
|
144
|
+
context: Omit<PipelineContext, "story" | "stories" | "workdir" | "routing">,
|
|
145
|
+
maxConcurrency: number,
|
|
146
|
+
eventEmitter?: PipelineEventEmitter,
|
|
147
|
+
): Promise<ParallelBatchResult> {
|
|
148
|
+
const logger = getSafeLogger();
|
|
149
|
+
const worktreeManager = new WorktreeManager();
|
|
150
|
+
const results: ParallelBatchResult = {
|
|
151
|
+
successfulStories: [],
|
|
152
|
+
failedStories: [],
|
|
153
|
+
totalCost: 0,
|
|
154
|
+
conflictedStories: [],
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Create worktrees for all stories in batch
|
|
158
|
+
const worktreeSetup: Array<{ story: UserStory; worktreePath: string }> = [];
|
|
159
|
+
|
|
160
|
+
for (const story of stories) {
|
|
161
|
+
const worktreePath = join(projectRoot, ".nax-wt", story.id);
|
|
162
|
+
try {
|
|
163
|
+
await worktreeManager.create(projectRoot, story.id);
|
|
164
|
+
worktreeSetup.push({ story, worktreePath });
|
|
165
|
+
|
|
166
|
+
logger?.info("parallel", "Created worktree for story", {
|
|
167
|
+
storyId: story.id,
|
|
168
|
+
worktreePath,
|
|
169
|
+
});
|
|
170
|
+
} catch (error) {
|
|
171
|
+
results.failedStories.push({
|
|
172
|
+
story,
|
|
173
|
+
error: `Failed to create worktree: ${error instanceof Error ? error.message : String(error)}`,
|
|
174
|
+
});
|
|
175
|
+
logger?.error("parallel", "Failed to create worktree", {
|
|
176
|
+
storyId: story.id,
|
|
177
|
+
error: error instanceof Error ? error.message : String(error),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Execute stories in parallel with concurrency limit
|
|
183
|
+
const executing: Promise<void>[] = [];
|
|
184
|
+
let activeCount = 0;
|
|
185
|
+
|
|
186
|
+
for (const { story, worktreePath } of worktreeSetup) {
|
|
187
|
+
const routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, config);
|
|
188
|
+
|
|
189
|
+
const executePromise = executeStoryInWorktree(story, worktreePath, context, routing as RoutingResult, eventEmitter)
|
|
190
|
+
.then((result) => {
|
|
191
|
+
results.totalCost += result.cost;
|
|
192
|
+
|
|
193
|
+
if (result.success) {
|
|
194
|
+
results.successfulStories.push(story);
|
|
195
|
+
logger?.info("parallel", "Story execution succeeded", {
|
|
196
|
+
storyId: story.id,
|
|
197
|
+
cost: result.cost,
|
|
198
|
+
});
|
|
199
|
+
} else {
|
|
200
|
+
results.failedStories.push({ story, error: result.error || "Unknown error" });
|
|
201
|
+
logger?.error("parallel", "Story execution failed", {
|
|
202
|
+
storyId: story.id,
|
|
203
|
+
error: result.error,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
.finally(() => {
|
|
208
|
+
activeCount--;
|
|
209
|
+
// BUG-4 fix: Remove completed promise from executing array
|
|
210
|
+
const index = executing.indexOf(executePromise);
|
|
211
|
+
if (index > -1) {
|
|
212
|
+
executing.splice(index, 1);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
executing.push(executePromise);
|
|
217
|
+
activeCount++;
|
|
218
|
+
|
|
219
|
+
// Wait if we've hit the concurrency limit
|
|
220
|
+
if (activeCount >= maxConcurrency) {
|
|
221
|
+
await Promise.race(executing);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Wait for all remaining executions
|
|
226
|
+
await Promise.all(executing);
|
|
227
|
+
|
|
228
|
+
return results;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Determine max concurrency from parallel option
|
|
233
|
+
* - undefined: sequential mode (should not call this function)
|
|
234
|
+
* - 0: auto-detect (use CPU count)
|
|
235
|
+
* - N > 0: use N
|
|
236
|
+
*/
|
|
237
|
+
function resolveMaxConcurrency(parallel: number): number {
|
|
238
|
+
if (parallel === 0) {
|
|
239
|
+
return os.cpus().length;
|
|
240
|
+
}
|
|
241
|
+
return Math.max(1, parallel);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Execute stories in parallel using worktree pipeline
|
|
246
|
+
*
|
|
247
|
+
* High-level flow:
|
|
248
|
+
* 1. Group stories by dependencies into batches
|
|
249
|
+
* 2. For each batch:
|
|
250
|
+
* a. Create worktrees for all stories
|
|
251
|
+
* b. Execute pipeline in parallel (respecting maxConcurrency)
|
|
252
|
+
* c. Merge successful branches in topological order
|
|
253
|
+
* d. Clean up worktrees on success, preserve on failure
|
|
254
|
+
* 3. Update PRD with results
|
|
255
|
+
*/
|
|
256
|
+
export async function executeParallel(
|
|
257
|
+
stories: UserStory[],
|
|
258
|
+
prdPath: string,
|
|
259
|
+
projectRoot: string,
|
|
260
|
+
config: NaxConfig,
|
|
261
|
+
hooks: LoadedHooksConfig,
|
|
262
|
+
plugins: PluginRegistry,
|
|
263
|
+
prd: PRD,
|
|
264
|
+
featureDir: string | undefined,
|
|
265
|
+
parallel: number,
|
|
266
|
+
eventEmitter?: PipelineEventEmitter,
|
|
267
|
+
): Promise<{ storiesCompleted: number; totalCost: number; updatedPrd: PRD }> {
|
|
268
|
+
const logger = getSafeLogger();
|
|
269
|
+
const maxConcurrency = resolveMaxConcurrency(parallel);
|
|
270
|
+
const worktreeManager = new WorktreeManager();
|
|
271
|
+
const mergeEngine = new MergeEngine(worktreeManager);
|
|
272
|
+
|
|
273
|
+
logger?.info("parallel", "Starting parallel execution", {
|
|
274
|
+
totalStories: stories.length,
|
|
275
|
+
maxConcurrency,
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Group stories by dependencies
|
|
279
|
+
const batches = groupStoriesByDependencies(stories);
|
|
280
|
+
logger?.info("parallel", "Grouped stories into batches", {
|
|
281
|
+
batchCount: batches.length,
|
|
282
|
+
batches: batches.map((b, i) => ({ index: i, storyCount: b.length, storyIds: b.map((s) => s.id) })),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
let storiesCompleted = 0;
|
|
286
|
+
let totalCost = 0;
|
|
287
|
+
const currentPrd = prd;
|
|
288
|
+
|
|
289
|
+
// Execute each batch sequentially (stories within each batch run in parallel)
|
|
290
|
+
for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
|
|
291
|
+
const batch = batches[batchIndex];
|
|
292
|
+
logger?.info("parallel", `Executing batch ${batchIndex + 1}/${batches.length}`, {
|
|
293
|
+
storyCount: batch.length,
|
|
294
|
+
storyIds: batch.map((s) => s.id),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Build context for this batch (shared across all stories in batch)
|
|
298
|
+
const baseContext = {
|
|
299
|
+
config,
|
|
300
|
+
prd: currentPrd,
|
|
301
|
+
featureDir,
|
|
302
|
+
hooks,
|
|
303
|
+
plugins,
|
|
304
|
+
storyStartTime: new Date().toISOString(),
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
// Execute batch in parallel
|
|
308
|
+
const batchResult = await executeParallelBatch(
|
|
309
|
+
batch,
|
|
310
|
+
projectRoot,
|
|
311
|
+
config,
|
|
312
|
+
currentPrd,
|
|
313
|
+
baseContext,
|
|
314
|
+
maxConcurrency,
|
|
315
|
+
eventEmitter,
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
totalCost += batchResult.totalCost;
|
|
319
|
+
|
|
320
|
+
// Merge successful stories in topological order
|
|
321
|
+
if (batchResult.successfulStories.length > 0) {
|
|
322
|
+
const successfulIds = batchResult.successfulStories.map((s) => s.id);
|
|
323
|
+
const deps = buildDependencyMap(batch);
|
|
324
|
+
|
|
325
|
+
logger?.info("parallel", "Merging successful stories", {
|
|
326
|
+
storyIds: successfulIds,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
const mergeResults = await mergeEngine.mergeAll(projectRoot, successfulIds, deps);
|
|
330
|
+
|
|
331
|
+
// Process merge results
|
|
332
|
+
for (const mergeResult of mergeResults) {
|
|
333
|
+
if (mergeResult.success) {
|
|
334
|
+
// Update PRD: mark story as passed
|
|
335
|
+
markStoryPassed(currentPrd, mergeResult.storyId);
|
|
336
|
+
storiesCompleted++;
|
|
337
|
+
|
|
338
|
+
logger?.info("parallel", "Story merged successfully", {
|
|
339
|
+
storyId: mergeResult.storyId,
|
|
340
|
+
retryCount: mergeResult.retryCount,
|
|
341
|
+
});
|
|
342
|
+
} else {
|
|
343
|
+
// Merge conflict — mark story as failed
|
|
344
|
+
markStoryFailed(currentPrd, mergeResult.storyId);
|
|
345
|
+
batchResult.conflictedStories.push({
|
|
346
|
+
storyId: mergeResult.storyId,
|
|
347
|
+
conflictFiles: mergeResult.conflictFiles || [],
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
logger?.error("parallel", "Merge conflict", {
|
|
351
|
+
storyId: mergeResult.storyId,
|
|
352
|
+
conflictFiles: mergeResult.conflictFiles,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// Keep worktree for manual resolution
|
|
356
|
+
logger?.warn("parallel", "Worktree preserved for manual conflict resolution", {
|
|
357
|
+
storyId: mergeResult.storyId,
|
|
358
|
+
worktreePath: join(projectRoot, ".nax-wt", mergeResult.storyId),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Mark failed stories in PRD and clean up their worktrees
|
|
365
|
+
for (const { story, error } of batchResult.failedStories) {
|
|
366
|
+
markStoryFailed(currentPrd, story.id);
|
|
367
|
+
|
|
368
|
+
logger?.error("parallel", "Cleaning up failed story worktree", {
|
|
369
|
+
storyId: story.id,
|
|
370
|
+
error,
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
try {
|
|
374
|
+
await worktreeManager.remove(projectRoot, story.id);
|
|
375
|
+
} catch (cleanupError) {
|
|
376
|
+
logger?.warn("parallel", "Failed to clean up worktree", {
|
|
377
|
+
storyId: story.id,
|
|
378
|
+
error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Save PRD after each batch
|
|
384
|
+
await savePRD(currentPrd, prdPath);
|
|
385
|
+
|
|
386
|
+
logger?.info("parallel", `Batch ${batchIndex + 1} complete`, {
|
|
387
|
+
successful: batchResult.successfulStories.length,
|
|
388
|
+
failed: batchResult.failedStories.length,
|
|
389
|
+
conflicts: batchResult.conflictedStories.length,
|
|
390
|
+
batchCost: batchResult.totalCost,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
logger?.info("parallel", "Parallel execution complete", {
|
|
395
|
+
storiesCompleted,
|
|
396
|
+
totalCost,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
return { storiesCompleted, totalCost, updatedPrd: currentPrd };
|
|
400
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PID Registry — Track and cleanup spawned agent processes
|
|
3
|
+
*
|
|
4
|
+
* Implements BUG-002:
|
|
5
|
+
* - Track PIDs of spawned Claude Code processes
|
|
6
|
+
* - Write .nax-pids file for persistence across crashes
|
|
7
|
+
* - Support killAll() for crash signal handlers
|
|
8
|
+
* - Support cleanupStale() for startup cleanup
|
|
9
|
+
* - Use process groups (setsid) on Linux, direct kill on macOS
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { getSafeLogger } from "../logger";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* PID registry file name
|
|
17
|
+
*/
|
|
18
|
+
const PID_REGISTRY_FILE = ".nax-pids";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* PID registry entry
|
|
22
|
+
*/
|
|
23
|
+
interface PidEntry {
|
|
24
|
+
pid: number;
|
|
25
|
+
spawnedAt: string;
|
|
26
|
+
workdir: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* PID Registry — Track spawned agent processes and cleanup orphans
|
|
31
|
+
*
|
|
32
|
+
* Maintains a .nax-pids file in the workdir to track spawned processes.
|
|
33
|
+
* On crash, signal handlers call killAll() to terminate all tracked PIDs.
|
|
34
|
+
* On startup, runner calls cleanupStale() to kill any orphaned processes from previous runs.
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* const registry = new PidRegistry("/path/to/project");
|
|
39
|
+
* await registry.register(12345);
|
|
40
|
+
* // ... later, on crash or shutdown
|
|
41
|
+
* await registry.killAll();
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export class PidRegistry {
|
|
45
|
+
private readonly workdir: string;
|
|
46
|
+
private readonly pidsFilePath: string;
|
|
47
|
+
private readonly pids: Set<number> = new Set();
|
|
48
|
+
private readonly platform: NodeJS.Platform;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create a new PID registry for the given workdir.
|
|
52
|
+
*
|
|
53
|
+
* @param workdir - Working directory where .nax-pids will be stored
|
|
54
|
+
* @param platform - Optional platform override (for testing)
|
|
55
|
+
*/
|
|
56
|
+
constructor(workdir: string, platform?: NodeJS.Platform) {
|
|
57
|
+
this.workdir = workdir;
|
|
58
|
+
this.pidsFilePath = `${workdir}/${PID_REGISTRY_FILE}`;
|
|
59
|
+
this.platform = platform ?? process.platform;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Register a spawned process PID.
|
|
64
|
+
*
|
|
65
|
+
* Adds the PID to the in-memory set and writes to .nax-pids file.
|
|
66
|
+
*
|
|
67
|
+
* @param pid - Process ID to register
|
|
68
|
+
*/
|
|
69
|
+
async register(pid: number): Promise<void> {
|
|
70
|
+
const logger = getSafeLogger();
|
|
71
|
+
this.pids.add(pid);
|
|
72
|
+
|
|
73
|
+
const entry: PidEntry = {
|
|
74
|
+
pid,
|
|
75
|
+
spawnedAt: new Date().toISOString(),
|
|
76
|
+
workdir: this.workdir,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Read existing content or create empty file
|
|
81
|
+
let existingContent = "";
|
|
82
|
+
if (existsSync(this.pidsFilePath)) {
|
|
83
|
+
existingContent = await Bun.file(this.pidsFilePath).text();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Append to .nax-pids file (one JSON entry per line)
|
|
87
|
+
const line = `${JSON.stringify(entry)}\n`;
|
|
88
|
+
await Bun.write(this.pidsFilePath, existingContent + line);
|
|
89
|
+
logger?.debug("pid-registry", `Registered PID ${pid}`, { pid });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
logger?.warn("pid-registry", `Failed to write PID ${pid} to registry`, {
|
|
92
|
+
error: (err as Error).message,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Unregister a process PID (e.g., after clean exit).
|
|
99
|
+
*
|
|
100
|
+
* Removes the PID from the in-memory set and rewrites .nax-pids file.
|
|
101
|
+
*
|
|
102
|
+
* @param pid - Process ID to unregister
|
|
103
|
+
*/
|
|
104
|
+
async unregister(pid: number): Promise<void> {
|
|
105
|
+
const logger = getSafeLogger();
|
|
106
|
+
this.pids.delete(pid);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Rewrite .nax-pids file without the unregistered PID
|
|
110
|
+
await this.writePidsFile();
|
|
111
|
+
logger?.debug("pid-registry", `Unregistered PID ${pid}`, { pid });
|
|
112
|
+
} catch (err) {
|
|
113
|
+
logger?.warn("pid-registry", `Failed to unregister PID ${pid}`, {
|
|
114
|
+
error: (err as Error).message,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Kill all registered processes.
|
|
121
|
+
*
|
|
122
|
+
* Called by crash signal handlers to cleanup spawned agent processes.
|
|
123
|
+
* Uses process groups (setsid) on Linux, direct kill on macOS.
|
|
124
|
+
*
|
|
125
|
+
* On Linux: kill -TERM -<pid> kills the entire process group
|
|
126
|
+
* On macOS: kill -TERM <pid> kills the process directly
|
|
127
|
+
*/
|
|
128
|
+
async killAll(): Promise<void> {
|
|
129
|
+
const logger = getSafeLogger();
|
|
130
|
+
const pids = Array.from(this.pids);
|
|
131
|
+
|
|
132
|
+
if (pids.length === 0) {
|
|
133
|
+
logger?.debug("pid-registry", "No PIDs to kill");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
logger?.info("pid-registry", `Killing ${pids.length} registered processes`, { pids });
|
|
138
|
+
|
|
139
|
+
const killPromises = pids.map((pid) => this.killPid(pid));
|
|
140
|
+
await Promise.allSettled(killPromises);
|
|
141
|
+
|
|
142
|
+
// Clear the registry file
|
|
143
|
+
try {
|
|
144
|
+
await Bun.write(this.pidsFilePath, "");
|
|
145
|
+
this.pids.clear();
|
|
146
|
+
logger?.info("pid-registry", "All registered PIDs killed and registry cleared");
|
|
147
|
+
} catch (err) {
|
|
148
|
+
logger?.warn("pid-registry", "Failed to clear registry file", {
|
|
149
|
+
error: (err as Error).message,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Cleanup stale PIDs from previous runs.
|
|
156
|
+
*
|
|
157
|
+
* Called at runner startup before lock acquisition.
|
|
158
|
+
* Reads .nax-pids file and kills any still-running processes.
|
|
159
|
+
*/
|
|
160
|
+
async cleanupStale(): Promise<void> {
|
|
161
|
+
const logger = getSafeLogger();
|
|
162
|
+
|
|
163
|
+
if (!existsSync(this.pidsFilePath)) {
|
|
164
|
+
logger?.debug("pid-registry", "No stale PIDs file found");
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
const content = await Bun.file(this.pidsFilePath).text();
|
|
170
|
+
const lines = content
|
|
171
|
+
.split("\n")
|
|
172
|
+
.filter((line) => line.trim())
|
|
173
|
+
.map((line) => {
|
|
174
|
+
try {
|
|
175
|
+
return JSON.parse(line) as PidEntry;
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
})
|
|
180
|
+
.filter((entry): entry is PidEntry => entry !== null);
|
|
181
|
+
|
|
182
|
+
if (lines.length === 0) {
|
|
183
|
+
logger?.debug("pid-registry", "No stale PIDs to cleanup");
|
|
184
|
+
await Bun.write(this.pidsFilePath, "");
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const stalePids = lines.map((entry) => entry.pid);
|
|
189
|
+
logger?.info("pid-registry", `Cleaning up ${stalePids.length} stale PIDs from previous run`, {
|
|
190
|
+
pids: stalePids,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const killPromises = stalePids.map((pid) => this.killPid(pid));
|
|
194
|
+
await Promise.allSettled(killPromises);
|
|
195
|
+
|
|
196
|
+
// Clear the registry file after cleanup
|
|
197
|
+
await Bun.write(this.pidsFilePath, "");
|
|
198
|
+
logger?.info("pid-registry", "Stale PIDs cleanup completed");
|
|
199
|
+
} catch (err) {
|
|
200
|
+
logger?.warn("pid-registry", "Failed to cleanup stale PIDs", {
|
|
201
|
+
error: (err as Error).message,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Kill a single PID.
|
|
208
|
+
*
|
|
209
|
+
* Uses process groups on Linux (kill -TERM -<pid>), direct kill on macOS (kill -TERM <pid>).
|
|
210
|
+
* Ignores ESRCH (process not found) errors.
|
|
211
|
+
*
|
|
212
|
+
* @param pid - Process ID to kill
|
|
213
|
+
*/
|
|
214
|
+
private async killPid(pid: number): Promise<void> {
|
|
215
|
+
const logger = getSafeLogger();
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
// Check if process exists first
|
|
219
|
+
const checkProc = Bun.spawn(["kill", "-0", String(pid)], {
|
|
220
|
+
stdout: "pipe",
|
|
221
|
+
stderr: "pipe",
|
|
222
|
+
});
|
|
223
|
+
const checkCode = await checkProc.exited;
|
|
224
|
+
|
|
225
|
+
if (checkCode !== 0) {
|
|
226
|
+
// Process doesn't exist, skip
|
|
227
|
+
logger?.debug("pid-registry", `PID ${pid} not found (already exited)`, { pid });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// On Linux, use process groups (kill -TERM -<pid>)
|
|
232
|
+
// On macOS, use direct kill (kill -TERM <pid>)
|
|
233
|
+
const killArgs = this.platform === "linux" ? ["kill", "-TERM", `-${pid}`] : ["kill", "-TERM", String(pid)];
|
|
234
|
+
|
|
235
|
+
const killProc = Bun.spawn(killArgs, {
|
|
236
|
+
stdout: "pipe",
|
|
237
|
+
stderr: "pipe",
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
const killCode = await killProc.exited;
|
|
241
|
+
|
|
242
|
+
if (killCode === 0) {
|
|
243
|
+
logger?.debug("pid-registry", `Killed PID ${pid}`, { pid });
|
|
244
|
+
} else {
|
|
245
|
+
const stderr = await new Response(killProc.stderr).text();
|
|
246
|
+
logger?.warn("pid-registry", `Failed to kill PID ${pid}`, {
|
|
247
|
+
pid,
|
|
248
|
+
exitCode: killCode,
|
|
249
|
+
stderr: stderr.trim(),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
} catch (err) {
|
|
253
|
+
logger?.warn("pid-registry", `Error killing PID ${pid}`, {
|
|
254
|
+
pid,
|
|
255
|
+
error: (err as Error).message,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Rewrite .nax-pids file with current in-memory PIDs.
|
|
262
|
+
*/
|
|
263
|
+
private async writePidsFile(): Promise<void> {
|
|
264
|
+
const entries = Array.from(this.pids).map((pid) => ({
|
|
265
|
+
pid,
|
|
266
|
+
spawnedAt: new Date().toISOString(),
|
|
267
|
+
workdir: this.workdir,
|
|
268
|
+
}));
|
|
269
|
+
|
|
270
|
+
const content = entries.map((entry) => JSON.stringify(entry)).join("\n");
|
|
271
|
+
await Bun.write(this.pidsFilePath, content ? `${content}\n` : "");
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get all registered PIDs (for testing)
|
|
276
|
+
*/
|
|
277
|
+
getPids(): number[] {
|
|
278
|
+
return Array.from(this.pids);
|
|
279
|
+
}
|
|
280
|
+
}
|