@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,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run Setup — Initial Setup Logic
|
|
3
|
+
*
|
|
4
|
+
* Handles the initial setup phase before the main execution loop:
|
|
5
|
+
* - Status writer initialization
|
|
6
|
+
* - PID registry cleanup
|
|
7
|
+
* - Crash handler installation
|
|
8
|
+
* - Lock acquisition
|
|
9
|
+
* - Plugin loading
|
|
10
|
+
* - PRD loading
|
|
11
|
+
* - Precheck validation
|
|
12
|
+
* - Run initialization
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as os from "node:os";
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
import type { NaxConfig } from "../../config";
|
|
18
|
+
import { LockAcquisitionError } from "../../errors";
|
|
19
|
+
import type { LoadedHooksConfig } from "../../hooks";
|
|
20
|
+
import { fireHook } from "../../hooks";
|
|
21
|
+
import type { InteractionChain } from "../../interaction";
|
|
22
|
+
import { initInteractionChain } from "../../interaction";
|
|
23
|
+
import { getSafeLogger } from "../../logger";
|
|
24
|
+
import { loadPlugins } from "../../plugins/loader";
|
|
25
|
+
import type { PluginRegistry } from "../../plugins/registry";
|
|
26
|
+
import type { PRD } from "../../prd";
|
|
27
|
+
import { loadPRD } from "../../prd";
|
|
28
|
+
import { installCrashHandlers } from "../crash-recovery";
|
|
29
|
+
import { acquireLock, hookCtx } from "../helpers";
|
|
30
|
+
import { PidRegistry } from "../pid-registry";
|
|
31
|
+
import { StatusWriter } from "../status-writer";
|
|
32
|
+
|
|
33
|
+
export interface RunSetupOptions {
|
|
34
|
+
prdPath: string;
|
|
35
|
+
workdir: string;
|
|
36
|
+
config: NaxConfig;
|
|
37
|
+
hooks: LoadedHooksConfig;
|
|
38
|
+
feature: string;
|
|
39
|
+
dryRun: boolean;
|
|
40
|
+
statusFile?: string;
|
|
41
|
+
logFilePath?: string;
|
|
42
|
+
runId: string;
|
|
43
|
+
startedAt: string;
|
|
44
|
+
startTime: number;
|
|
45
|
+
skipPrecheck: boolean;
|
|
46
|
+
headless: boolean;
|
|
47
|
+
formatterMode: "quiet" | "normal" | "verbose" | "json";
|
|
48
|
+
getTotalCost: () => number;
|
|
49
|
+
getIterations: () => number;
|
|
50
|
+
// BUG-017: Additional getters for run.complete event on SIGTERM
|
|
51
|
+
getStoriesCompleted: () => number;
|
|
52
|
+
getTotalStories: () => number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RunSetupResult {
|
|
56
|
+
statusWriter: StatusWriter;
|
|
57
|
+
pidRegistry: PidRegistry;
|
|
58
|
+
cleanupCrashHandlers: () => void;
|
|
59
|
+
pluginRegistry: PluginRegistry;
|
|
60
|
+
prd: PRD;
|
|
61
|
+
storyCounts: {
|
|
62
|
+
total: number;
|
|
63
|
+
passed: number;
|
|
64
|
+
pending: number;
|
|
65
|
+
failed: number;
|
|
66
|
+
};
|
|
67
|
+
interactionChain: InteractionChain | null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Execute initial setup phase
|
|
72
|
+
*/
|
|
73
|
+
export async function setupRun(options: RunSetupOptions): Promise<RunSetupResult> {
|
|
74
|
+
const logger = getSafeLogger();
|
|
75
|
+
const {
|
|
76
|
+
prdPath,
|
|
77
|
+
workdir,
|
|
78
|
+
config,
|
|
79
|
+
hooks,
|
|
80
|
+
feature,
|
|
81
|
+
dryRun,
|
|
82
|
+
statusFile,
|
|
83
|
+
logFilePath,
|
|
84
|
+
runId,
|
|
85
|
+
startedAt,
|
|
86
|
+
startTime,
|
|
87
|
+
skipPrecheck,
|
|
88
|
+
headless,
|
|
89
|
+
formatterMode,
|
|
90
|
+
getTotalCost,
|
|
91
|
+
getIterations,
|
|
92
|
+
} = options;
|
|
93
|
+
|
|
94
|
+
// ── Status writer (encapsulates status file state and write logic) ───────
|
|
95
|
+
const statusWriter = new StatusWriter(statusFile, config, {
|
|
96
|
+
runId,
|
|
97
|
+
feature,
|
|
98
|
+
startedAt,
|
|
99
|
+
dryRun,
|
|
100
|
+
startTimeMs: startTime,
|
|
101
|
+
pid: process.pid,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── PID registry for orphan process cleanup (BUG-002) ───────
|
|
105
|
+
const pidRegistry = new PidRegistry(workdir);
|
|
106
|
+
|
|
107
|
+
// Cleanup stale PIDs from previous crashed runs
|
|
108
|
+
await pidRegistry.cleanupStale();
|
|
109
|
+
|
|
110
|
+
// Install crash handlers for signal recovery (US-007, BUG-1+MEM-1 fix: pass getters, cleanup in finally)
|
|
111
|
+
const cleanupCrashHandlers = installCrashHandlers({
|
|
112
|
+
statusWriter,
|
|
113
|
+
getTotalCost,
|
|
114
|
+
getIterations,
|
|
115
|
+
jsonlFilePath: logFilePath,
|
|
116
|
+
pidRegistry,
|
|
117
|
+
// BUG-017: Pass context for run.complete event on SIGTERM
|
|
118
|
+
runId: options.runId,
|
|
119
|
+
feature: options.feature,
|
|
120
|
+
getStartTime: () => options.startTime,
|
|
121
|
+
getTotalStories: options.getTotalStories,
|
|
122
|
+
getStoriesCompleted: options.getStoriesCompleted,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Acquire lock to prevent concurrent execution
|
|
126
|
+
const lockAcquired = await acquireLock(workdir);
|
|
127
|
+
if (!lockAcquired) {
|
|
128
|
+
logger?.error("execution", "Another nax process is already running in this directory");
|
|
129
|
+
logger?.error("execution", "If you believe this is an error, remove nax.lock manually");
|
|
130
|
+
throw new LockAcquisitionError(workdir);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Load plugins (before try block so it's accessible in finally)
|
|
134
|
+
const globalPluginsDir = path.join(os.homedir(), ".nax", "plugins");
|
|
135
|
+
const projectPluginsDir = path.join(workdir, "nax", "plugins");
|
|
136
|
+
const configPlugins = config.plugins || [];
|
|
137
|
+
const pluginRegistry = await loadPlugins(globalPluginsDir, projectPluginsDir, configPlugins, workdir);
|
|
138
|
+
|
|
139
|
+
// Load PRD (before try block so it's accessible in finally for onRunEnd)
|
|
140
|
+
let prd = await loadPRD(prdPath);
|
|
141
|
+
|
|
142
|
+
// Initialize interaction chain (US-008) — do this BEFORE precheck so story size prompts can use it
|
|
143
|
+
const interactionChain = await initInteractionChain(config, headless);
|
|
144
|
+
|
|
145
|
+
// ── Prime StatusWriter with PRD so precheck-failed can be recorded ─────────
|
|
146
|
+
statusWriter.setPrd(prd);
|
|
147
|
+
|
|
148
|
+
// ── Run precheck validations (unless --skip-precheck) ──────────────────────
|
|
149
|
+
if (!skipPrecheck) {
|
|
150
|
+
const { runPrecheckValidation } = await import("./precheck-runner");
|
|
151
|
+
await runPrecheckValidation({
|
|
152
|
+
config,
|
|
153
|
+
prd,
|
|
154
|
+
workdir,
|
|
155
|
+
logFilePath,
|
|
156
|
+
statusWriter,
|
|
157
|
+
headless,
|
|
158
|
+
formatterMode,
|
|
159
|
+
interactionChain,
|
|
160
|
+
featureName: feature,
|
|
161
|
+
});
|
|
162
|
+
} else {
|
|
163
|
+
logger?.warn("precheck", "Precheck validations skipped (--skip-precheck)");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Log plugins loaded
|
|
167
|
+
logger?.info("plugins", `Loaded ${pluginRegistry.plugins.length} plugins`, {
|
|
168
|
+
plugins: pluginRegistry.plugins.map((p) => ({ name: p.name, version: p.version, provides: p.provides })),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Log run start
|
|
172
|
+
const routingMode = config.routing.llm?.mode ?? "hybrid";
|
|
173
|
+
logger?.info("run.start", `Starting feature: ${feature}`, {
|
|
174
|
+
runId,
|
|
175
|
+
feature,
|
|
176
|
+
workdir,
|
|
177
|
+
dryRun,
|
|
178
|
+
routingMode,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// Fire on-start hook
|
|
182
|
+
await fireHook(hooks, "on-start", hookCtx(feature), workdir);
|
|
183
|
+
|
|
184
|
+
// Initialize run: check agent, reconcile state, validate limits
|
|
185
|
+
const { initializeRun } = await import("./run-initialization");
|
|
186
|
+
const initResult = await initializeRun({
|
|
187
|
+
config,
|
|
188
|
+
prdPath,
|
|
189
|
+
workdir,
|
|
190
|
+
dryRun,
|
|
191
|
+
});
|
|
192
|
+
prd = initResult.prd;
|
|
193
|
+
const counts = initResult.storyCounts;
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
statusWriter,
|
|
197
|
+
pidRegistry,
|
|
198
|
+
cleanupCrashHandlers,
|
|
199
|
+
pluginRegistry,
|
|
200
|
+
prd,
|
|
201
|
+
storyCounts: counts,
|
|
202
|
+
interactionChain,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story Lifecycle Hooks
|
|
3
|
+
*
|
|
4
|
+
* Centralizes reporter notification boilerplate for story lifecycle events:
|
|
5
|
+
* - onStoryComplete (completed/paused/skipped/failed)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getSafeLogger } from "../../logger";
|
|
9
|
+
import type { IReporter } from "../../plugins/types";
|
|
10
|
+
|
|
11
|
+
export interface StoryCompleteEvent {
|
|
12
|
+
runId: string;
|
|
13
|
+
storyId: string;
|
|
14
|
+
status: "completed" | "paused" | "skipped" | "failed";
|
|
15
|
+
durationMs: number;
|
|
16
|
+
cost: number;
|
|
17
|
+
tier: string;
|
|
18
|
+
testStrategy: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Emit onStoryComplete event to all reporters
|
|
23
|
+
*
|
|
24
|
+
* Handles error recovery for individual reporter failures.
|
|
25
|
+
*/
|
|
26
|
+
export async function emitStoryComplete(reporters: IReporter[], event: StoryCompleteEvent): Promise<void> {
|
|
27
|
+
const logger = getSafeLogger();
|
|
28
|
+
|
|
29
|
+
for (const reporter of reporters) {
|
|
30
|
+
if (reporter.onStoryComplete) {
|
|
31
|
+
try {
|
|
32
|
+
await reporter.onStoryComplete(event);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
logger?.warn("plugins", `Reporter '${reporter.name}' onStoryComplete failed`, { error });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Story Size Prompts (v0.16.0)
|
|
3
|
+
*
|
|
4
|
+
* Post-precheck interaction prompts for flagged stories.
|
|
5
|
+
* Asks user to confirm (Approve/Skip/Abort) for each large story.
|
|
6
|
+
* Integrates with interaction chain.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { InteractionChain } from "../../interaction/chain";
|
|
10
|
+
import type { InteractionResponse } from "../../interaction/types";
|
|
11
|
+
import { getSafeLogger } from "../../logger";
|
|
12
|
+
import type { PRD } from "../../prd/types";
|
|
13
|
+
import type { FlaggedStory } from "../../precheck/story-size-gate";
|
|
14
|
+
|
|
15
|
+
/** Prompt result for a single story */
|
|
16
|
+
export interface StoryPromptResult {
|
|
17
|
+
storyId: string;
|
|
18
|
+
action: "approve" | "skip" | "abort";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Summary of story size prompt results */
|
|
22
|
+
export interface StorySizePromptSummary {
|
|
23
|
+
approved: string[];
|
|
24
|
+
skipped: string[];
|
|
25
|
+
aborted: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Prompt user for each flagged story
|
|
30
|
+
*
|
|
31
|
+
* @param flaggedStories - Stories that exceeded size thresholds
|
|
32
|
+
* @param prd - PRD instance (mutated: skips stories if user selects Skip)
|
|
33
|
+
* @param chain - Interaction chain
|
|
34
|
+
* @param featureName - Feature name for context
|
|
35
|
+
* @returns Summary of user decisions
|
|
36
|
+
*/
|
|
37
|
+
export async function promptForFlaggedStories(
|
|
38
|
+
flaggedStories: FlaggedStory[],
|
|
39
|
+
prd: PRD,
|
|
40
|
+
chain: InteractionChain,
|
|
41
|
+
featureName: string,
|
|
42
|
+
): Promise<StorySizePromptSummary> {
|
|
43
|
+
const logger = getSafeLogger();
|
|
44
|
+
const summary: StorySizePromptSummary = {
|
|
45
|
+
approved: [],
|
|
46
|
+
skipped: [],
|
|
47
|
+
aborted: false,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
for (const flagged of flaggedStories) {
|
|
51
|
+
logger?.info("precheck", `Story size gate: prompting for ${flagged.storyId}`, {
|
|
52
|
+
recommendation: flagged.recommendation,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const story = prd.userStories.find((s) => s.id === flagged.storyId);
|
|
56
|
+
if (!story) {
|
|
57
|
+
logger?.warn("precheck", `Story ${flagged.storyId} not found in PRD, skipping prompt`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Build detail message with signal breakdown
|
|
62
|
+
const signalDetails = [
|
|
63
|
+
`- Acceptance Criteria: ${flagged.signals.acCount.value} (threshold: ${flagged.signals.acCount.threshold}) ${flagged.signals.acCount.flagged ? "⚠" : "✓"}`,
|
|
64
|
+
`- Description Length: ${flagged.signals.descriptionLength.value} chars (threshold: ${flagged.signals.descriptionLength.threshold}) ${flagged.signals.descriptionLength.flagged ? "⚠" : "✓"}`,
|
|
65
|
+
`- Bullet Points: ${flagged.signals.bulletPoints.value} (threshold: ${flagged.signals.bulletPoints.threshold}) ${flagged.signals.bulletPoints.flagged ? "⚠" : "✓"}`,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const detail = `${flagged.recommendation}\n\nSignal breakdown:\n${signalDetails.join("\n")}\n\nStory: ${story.title}`;
|
|
69
|
+
|
|
70
|
+
// Send interaction request
|
|
71
|
+
const requestId = `ix-${flagged.storyId}-size-gate`;
|
|
72
|
+
const response: InteractionResponse = await chain.prompt({
|
|
73
|
+
id: requestId,
|
|
74
|
+
type: "choose",
|
|
75
|
+
featureName,
|
|
76
|
+
storyId: flagged.storyId,
|
|
77
|
+
stage: "pre-flight",
|
|
78
|
+
summary: `Story ${flagged.storyId} exceeds size thresholds — proceed?`,
|
|
79
|
+
detail,
|
|
80
|
+
options: [
|
|
81
|
+
{ key: "approve", label: "Approve", description: "Continue with this story despite size warnings" },
|
|
82
|
+
{ key: "skip", label: "Skip", description: "Skip this story and continue with others" },
|
|
83
|
+
{ key: "abort", label: "Abort", description: "Abort the entire run" },
|
|
84
|
+
],
|
|
85
|
+
timeout: 600000, // 10 minutes
|
|
86
|
+
fallback: "escalate",
|
|
87
|
+
createdAt: Date.now(),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Process response
|
|
91
|
+
switch (response.action) {
|
|
92
|
+
case "approve": {
|
|
93
|
+
summary.approved.push(flagged.storyId);
|
|
94
|
+
logger?.info("precheck", `User approved ${flagged.storyId} despite size warnings`);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case "skip": {
|
|
98
|
+
summary.skipped.push(flagged.storyId);
|
|
99
|
+
story.status = "skipped";
|
|
100
|
+
logger?.info("precheck", `User skipped ${flagged.storyId} due to size warnings`);
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case "abort": {
|
|
104
|
+
summary.aborted = true;
|
|
105
|
+
logger?.warn("precheck", `User aborted run due to ${flagged.storyId} size warnings`);
|
|
106
|
+
throw new Error(`Run aborted by user: story ${flagged.storyId} exceeds size thresholds`);
|
|
107
|
+
}
|
|
108
|
+
default: {
|
|
109
|
+
logger?.warn("precheck", `Unknown action ${response.action} for ${flagged.storyId}, aborting`);
|
|
110
|
+
summary.aborted = true;
|
|
111
|
+
throw new Error(`Run aborted: unknown action ${response.action}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
logger?.info("precheck", "Story size gate prompts complete", {
|
|
117
|
+
approved: summary.approved.length,
|
|
118
|
+
skipped: summary.skipped.length,
|
|
119
|
+
aborted: summary.aborted,
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return summary;
|
|
123
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock File Management
|
|
3
|
+
*
|
|
4
|
+
* Extracted from helpers.ts: execution lock acquisition and release.
|
|
5
|
+
* Prevents concurrent runs in the same directory.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { getLogger } from "../logger";
|
|
10
|
+
|
|
11
|
+
/** Safely get logger instance, returns null if not initialized */
|
|
12
|
+
function getSafeLogger() {
|
|
13
|
+
try {
|
|
14
|
+
return getLogger();
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Check if a process with given PID is still alive */
|
|
21
|
+
function isProcessAlive(pid: number): boolean {
|
|
22
|
+
try {
|
|
23
|
+
// kill(pid, 0) checks if process exists without actually sending a signal
|
|
24
|
+
process.kill(pid, 0);
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Acquire execution lock to prevent concurrent runs in same directory.
|
|
33
|
+
* Creates nax.lock file with PID and timestamp.
|
|
34
|
+
* Returns true if lock acquired, false if another process holds it.
|
|
35
|
+
*
|
|
36
|
+
* Handles stale locks from crashed/OOM-killed processes:
|
|
37
|
+
* - Reads PID from existing lock file
|
|
38
|
+
* - Checks if process is still alive using kill(pid, 0)
|
|
39
|
+
* - Removes stale lock if process is dead
|
|
40
|
+
* - Re-acquires lock after removal
|
|
41
|
+
*/
|
|
42
|
+
export async function acquireLock(workdir: string): Promise<boolean> {
|
|
43
|
+
const lockPath = path.join(workdir, "nax.lock");
|
|
44
|
+
const lockFile = Bun.file(lockPath);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
// BUG-2 fix: First check for stale lock before attempting atomic create
|
|
48
|
+
const exists = await lockFile.exists();
|
|
49
|
+
if (exists) {
|
|
50
|
+
// Read lock data
|
|
51
|
+
const lockContent = await lockFile.text();
|
|
52
|
+
const lockData = JSON.parse(lockContent);
|
|
53
|
+
const lockPid = lockData.pid;
|
|
54
|
+
|
|
55
|
+
// Check if the process is still alive
|
|
56
|
+
if (isProcessAlive(lockPid)) {
|
|
57
|
+
// Process is alive, lock is valid
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Process is dead, remove stale lock
|
|
62
|
+
const logger = getSafeLogger();
|
|
63
|
+
logger?.warn("execution", "Removing stale lock", {
|
|
64
|
+
pid: lockPid,
|
|
65
|
+
});
|
|
66
|
+
const fs = await import("node:fs/promises");
|
|
67
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Create lock file atomically using exclusive create (O_CREAT | O_EXCL)
|
|
71
|
+
const lockData = {
|
|
72
|
+
pid: process.pid,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
};
|
|
75
|
+
const fs = await import("node:fs");
|
|
76
|
+
const fd = fs.openSync(lockPath, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY, 0o644);
|
|
77
|
+
fs.writeSync(fd, JSON.stringify(lockData));
|
|
78
|
+
fs.closeSync(fd);
|
|
79
|
+
return true;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// EEXIST means another process won the race
|
|
82
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const logger = getSafeLogger();
|
|
86
|
+
logger?.warn("execution", "Failed to acquire lock", {
|
|
87
|
+
error: (error as Error).message,
|
|
88
|
+
});
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Release execution lock by deleting nax.lock file.
|
|
95
|
+
*
|
|
96
|
+
* @param workdir - Working directory to unlock
|
|
97
|
+
*/
|
|
98
|
+
export async function releaseLock(workdir: string): Promise<void> {
|
|
99
|
+
const lockPath = path.join(workdir, "nax.lock");
|
|
100
|
+
try {
|
|
101
|
+
const file = Bun.file(lockPath);
|
|
102
|
+
const exists = await file.exists();
|
|
103
|
+
if (exists) {
|
|
104
|
+
const proc = Bun.spawn(["rm", lockPath], { stdout: "pipe" });
|
|
105
|
+
await proc.exited;
|
|
106
|
+
// Wait a bit for filesystem to sync (prevents race in tests)
|
|
107
|
+
await Bun.sleep(10);
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const logger = getSafeLogger();
|
|
111
|
+
logger?.warn("execution", "Failed to release lock", {
|
|
112
|
+
error: (error as Error).message,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parallel Execution Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Handles the full parallel execution flow:
|
|
5
|
+
* - Status updates with parallel info
|
|
6
|
+
* - Execute parallel stories
|
|
7
|
+
* - Handle completion or continue to sequential
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as os from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import type { NaxConfig } from "../config";
|
|
13
|
+
import type { LoadedHooksConfig } from "../hooks";
|
|
14
|
+
import { fireHook } from "../hooks";
|
|
15
|
+
import { getSafeLogger } from "../logger";
|
|
16
|
+
import type { StoryMetrics } from "../metrics";
|
|
17
|
+
import type { PipelineEventEmitter } from "../pipeline/events";
|
|
18
|
+
import type { PluginRegistry } from "../plugins/registry";
|
|
19
|
+
import type { PRD } from "../prd";
|
|
20
|
+
import { countStories, isComplete } from "../prd";
|
|
21
|
+
import { getAllReadyStories, hookCtx } from "./helpers";
|
|
22
|
+
import { executeParallel } from "./parallel";
|
|
23
|
+
import type { StatusWriter } from "./status-writer";
|
|
24
|
+
|
|
25
|
+
export interface ParallelExecutorOptions {
|
|
26
|
+
prdPath: string;
|
|
27
|
+
workdir: string;
|
|
28
|
+
config: NaxConfig;
|
|
29
|
+
hooks: LoadedHooksConfig;
|
|
30
|
+
feature: string;
|
|
31
|
+
featureDir?: string;
|
|
32
|
+
parallelCount: number;
|
|
33
|
+
eventEmitter?: PipelineEventEmitter;
|
|
34
|
+
statusWriter: StatusWriter;
|
|
35
|
+
runId: string;
|
|
36
|
+
startedAt: string;
|
|
37
|
+
startTime: number;
|
|
38
|
+
totalCost: number;
|
|
39
|
+
iterations: number;
|
|
40
|
+
storiesCompleted: number;
|
|
41
|
+
allStoryMetrics: StoryMetrics[];
|
|
42
|
+
pluginRegistry: PluginRegistry;
|
|
43
|
+
formatterMode: "quiet" | "normal" | "verbose" | "json";
|
|
44
|
+
headless: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ParallelExecutorResult {
|
|
48
|
+
prd: PRD;
|
|
49
|
+
totalCost: number;
|
|
50
|
+
storiesCompleted: number;
|
|
51
|
+
completed: boolean;
|
|
52
|
+
durationMs?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Execute parallel stories if --parallel is set
|
|
57
|
+
*/
|
|
58
|
+
export async function runParallelExecution(
|
|
59
|
+
options: ParallelExecutorOptions,
|
|
60
|
+
initialPrd: PRD,
|
|
61
|
+
): Promise<ParallelExecutorResult> {
|
|
62
|
+
const logger = getSafeLogger();
|
|
63
|
+
const {
|
|
64
|
+
prdPath,
|
|
65
|
+
workdir,
|
|
66
|
+
config,
|
|
67
|
+
hooks,
|
|
68
|
+
feature,
|
|
69
|
+
featureDir,
|
|
70
|
+
parallelCount,
|
|
71
|
+
eventEmitter,
|
|
72
|
+
statusWriter,
|
|
73
|
+
runId,
|
|
74
|
+
startedAt,
|
|
75
|
+
startTime,
|
|
76
|
+
pluginRegistry,
|
|
77
|
+
formatterMode,
|
|
78
|
+
headless,
|
|
79
|
+
} = options;
|
|
80
|
+
|
|
81
|
+
let { totalCost, iterations, storiesCompleted, allStoryMetrics } = options;
|
|
82
|
+
let prd = initialPrd;
|
|
83
|
+
|
|
84
|
+
const readyStories = getAllReadyStories(prd);
|
|
85
|
+
if (readyStories.length === 0) {
|
|
86
|
+
logger?.info("parallel", "No stories ready for parallel execution");
|
|
87
|
+
return { prd, totalCost, storiesCompleted, completed: false };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const maxConcurrency = parallelCount === 0 ? os.cpus().length : Math.max(1, parallelCount);
|
|
91
|
+
|
|
92
|
+
logger?.info("parallel", "Starting parallel execution mode", {
|
|
93
|
+
totalStories: readyStories.length,
|
|
94
|
+
maxConcurrency,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Update status with parallel info
|
|
98
|
+
statusWriter.setPrd(prd);
|
|
99
|
+
await statusWriter.update(totalCost, iterations, {
|
|
100
|
+
parallel: {
|
|
101
|
+
enabled: true,
|
|
102
|
+
maxConcurrency,
|
|
103
|
+
activeStories: readyStories.map((s) => ({
|
|
104
|
+
storyId: s.id,
|
|
105
|
+
worktreePath: path.join(workdir, ".nax-wt", s.id),
|
|
106
|
+
})),
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const parallelResult = await executeParallel(
|
|
112
|
+
readyStories,
|
|
113
|
+
prdPath,
|
|
114
|
+
workdir,
|
|
115
|
+
config,
|
|
116
|
+
hooks,
|
|
117
|
+
pluginRegistry,
|
|
118
|
+
prd,
|
|
119
|
+
featureDir,
|
|
120
|
+
parallelCount,
|
|
121
|
+
eventEmitter,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
prd = parallelResult.updatedPrd;
|
|
125
|
+
storiesCompleted += parallelResult.storiesCompleted;
|
|
126
|
+
totalCost += parallelResult.totalCost;
|
|
127
|
+
|
|
128
|
+
logger?.info("parallel", "Parallel execution complete", {
|
|
129
|
+
storiesCompleted: parallelResult.storiesCompleted,
|
|
130
|
+
totalCost: parallelResult.totalCost,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Clear parallel status
|
|
134
|
+
statusWriter.setPrd(prd);
|
|
135
|
+
await statusWriter.update(totalCost, iterations, {
|
|
136
|
+
parallel: {
|
|
137
|
+
enabled: true,
|
|
138
|
+
maxConcurrency,
|
|
139
|
+
activeStories: [],
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
} catch (error) {
|
|
143
|
+
logger?.error("parallel", "Parallel execution failed", {
|
|
144
|
+
error: error instanceof Error ? error.message : String(error),
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Clear parallel status on error
|
|
148
|
+
await statusWriter.update(totalCost, iterations, {
|
|
149
|
+
parallel: undefined,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if all stories are complete after parallel execution
|
|
156
|
+
if (isComplete(prd)) {
|
|
157
|
+
logger?.info("execution", "All stories complete!", {
|
|
158
|
+
feature,
|
|
159
|
+
totalCost,
|
|
160
|
+
});
|
|
161
|
+
await fireHook(hooks, "on-complete", hookCtx(feature, { status: "complete", cost: totalCost }), workdir);
|
|
162
|
+
|
|
163
|
+
// Skip to metrics and cleanup
|
|
164
|
+
const durationMs = Date.now() - startTime;
|
|
165
|
+
const runCompletedAt = new Date().toISOString();
|
|
166
|
+
|
|
167
|
+
const { handleParallelCompletion } = await import("./lifecycle/parallel-lifecycle");
|
|
168
|
+
await handleParallelCompletion({
|
|
169
|
+
runId,
|
|
170
|
+
feature,
|
|
171
|
+
startedAt,
|
|
172
|
+
completedAt: runCompletedAt,
|
|
173
|
+
prd,
|
|
174
|
+
allStoryMetrics,
|
|
175
|
+
totalCost,
|
|
176
|
+
storiesCompleted,
|
|
177
|
+
durationMs,
|
|
178
|
+
workdir,
|
|
179
|
+
pluginRegistry,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const finalCounts = countStories(prd);
|
|
183
|
+
statusWriter.setPrd(prd);
|
|
184
|
+
statusWriter.setCurrentStory(null);
|
|
185
|
+
statusWriter.setRunStatus("completed");
|
|
186
|
+
await statusWriter.update(totalCost, iterations);
|
|
187
|
+
|
|
188
|
+
// ── Output run footer in headless mode (parallel path) ──────────────
|
|
189
|
+
if (headless && formatterMode !== "json") {
|
|
190
|
+
const { outputRunFooter } = await import("./lifecycle/headless-formatter");
|
|
191
|
+
outputRunFooter({
|
|
192
|
+
finalCounts: {
|
|
193
|
+
total: finalCounts.total,
|
|
194
|
+
passed: finalCounts.passed,
|
|
195
|
+
failed: finalCounts.failed,
|
|
196
|
+
skipped: finalCounts.skipped,
|
|
197
|
+
},
|
|
198
|
+
durationMs,
|
|
199
|
+
totalCost,
|
|
200
|
+
startedAt,
|
|
201
|
+
completedAt: runCompletedAt,
|
|
202
|
+
formatterMode,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
prd,
|
|
208
|
+
totalCost,
|
|
209
|
+
storiesCompleted,
|
|
210
|
+
completed: true,
|
|
211
|
+
durationMs,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { prd, totalCost, storiesCompleted, completed: false };
|
|
216
|
+
}
|