@nathapp/nax 0.20.0 → 0.22.0

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.
Files changed (233) hide show
  1. package/.claude/settings.json +15 -0
  2. package/.mcp.json +8 -0
  3. package/docs/20260304-review-nax.md +492 -0
  4. package/docs/ROADMAP.md +65 -18
  5. package/docs/adr/ADR-005-implementation-plan.md +655 -0
  6. package/docs/adr/ADR-005-pipeline-re-architecture.md +464 -0
  7. package/docs/specs/bug-039-orphan-processes.md +131 -0
  8. package/docs/specs/bug-040-review-rectification.md +82 -0
  9. package/docs/specs/bug-041-cross-story-test-isolation.md +88 -0
  10. package/docs/specs/bug-042-verifier-failure-capture.md +117 -0
  11. package/docs/specs/feat-010-smart-runner-git-history.md +96 -0
  12. package/docs/specs/feat-011-file-context-strategy.md +73 -0
  13. package/docs/specs/feat-012-tdd-writer-tier.md +79 -0
  14. package/docs/specs/feat-013-test-after-review.md +89 -0
  15. package/docs/specs/feat-014-heartbeat-observability.md +127 -0
  16. package/memory/topic/feat-010-baseref.md +28 -0
  17. package/memory/topic/feat-013-test-after-deprecation.md +22 -0
  18. package/nax/config.json +7 -4
  19. package/nax/features/bug-039-medium/prd.json +45 -0
  20. package/package.json +2 -2
  21. package/src/agents/claude.ts +109 -15
  22. package/src/config/types.ts +11 -0
  23. package/src/context/builder.ts +9 -1
  24. package/src/execution/dry-run.ts +81 -0
  25. package/src/execution/escalation/tier-outcome.ts +29 -44
  26. package/src/execution/executor-types.ts +65 -0
  27. package/src/execution/index.ts +0 -17
  28. package/src/execution/iteration-runner.ts +132 -0
  29. package/src/execution/lifecycle/index.ts +0 -1
  30. package/src/execution/lifecycle/run-regression.ts +5 -5
  31. package/src/execution/pipeline-result-handler.ts +51 -254
  32. package/src/execution/sequential-executor.ts +72 -315
  33. package/src/execution/story-selector.ts +75 -0
  34. package/src/pipeline/event-bus.ts +276 -0
  35. package/src/pipeline/runner.ts +51 -77
  36. package/src/pipeline/stages/autofix.ts +133 -0
  37. package/src/pipeline/stages/completion.ts +22 -30
  38. package/src/pipeline/stages/index.ts +30 -13
  39. package/src/pipeline/stages/rectify.ts +93 -0
  40. package/src/pipeline/stages/regression.ts +88 -0
  41. package/src/pipeline/stages/review.ts +19 -153
  42. package/src/pipeline/stages/verify.ts +19 -3
  43. package/src/pipeline/subscribers/hooks.ts +133 -0
  44. package/src/pipeline/subscribers/interaction.ts +68 -0
  45. package/src/pipeline/subscribers/reporters.ts +174 -0
  46. package/src/pipeline/types.ts +12 -1
  47. package/src/review/orchestrator.ts +105 -0
  48. package/src/review/runner.ts +39 -4
  49. package/src/routing/router.ts +3 -3
  50. package/src/routing/strategies/keyword.ts +5 -2
  51. package/src/routing/strategies/llm.ts +27 -1
  52. package/src/tdd/prompts.ts +1 -1
  53. package/src/utils/git.ts +49 -25
  54. package/src/verification/executor.ts +8 -2
  55. package/src/verification/index.ts +1 -1
  56. package/src/verification/orchestrator-types.ts +145 -0
  57. package/src/verification/orchestrator.ts +76 -0
  58. package/src/{execution/post-verify-rectification.ts → verification/rectification-loop.ts} +13 -20
  59. package/src/verification/{gate.ts → runners.ts} +17 -105
  60. package/src/verification/smart-runner.ts +6 -10
  61. package/src/verification/strategies/acceptance.ts +133 -0
  62. package/src/verification/strategies/regression.ts +90 -0
  63. package/src/verification/strategies/scoped.ts +123 -0
  64. package/test/COVERAGE-GAPS.md +333 -0
  65. package/test/{acceptance → e2e}/cm-003-default-view.test.ts +1 -0
  66. package/test/{integration/e2e.test.ts → e2e/plan-analyze-run.test.ts} +1 -0
  67. package/test/integration/{agent-validation.test.ts → cli/agent-validation.test.ts} +3 -3
  68. package/test/integration/{cli-config-default-edge-cases.test.ts → cli/cli-config-default-edge-cases.test.ts} +6 -5
  69. package/test/integration/{cli-config-default-view.test.ts → cli/cli-config-default-view.test.ts} +8 -7
  70. package/test/integration/{cli-config-diff.test.ts → cli/cli-config-diff.test.ts} +3 -2
  71. package/test/integration/{cli-config.test.ts → cli/cli-config.test.ts} +3 -2
  72. package/test/integration/{cli-diagnose.test.ts → cli/cli-diagnose.test.ts} +5 -4
  73. package/test/integration/{cli-logs.test.ts → cli/cli-logs.test.ts} +12 -3
  74. package/test/integration/{cli-plugins.test.ts → cli/cli-plugins.test.ts} +4 -3
  75. package/test/integration/{cli-precheck.test.ts → cli/cli-precheck.test.ts} +4 -3
  76. package/test/integration/{cli-run-headless.test.ts → cli/cli-run-headless.test.ts} +3 -2
  77. package/test/integration/{cli.test.ts → cli/cli.test.ts} +2 -1
  78. package/test/integration/{precheck-integration.test.ts → cli/precheck-integration.test.ts} +10 -9
  79. package/test/integration/{precheck-orchestrator.test.ts → cli/precheck-orchestrator.test.ts} +4 -3
  80. package/test/integration/{precheck.test.ts → cli/precheck.test.ts} +5 -4
  81. package/test/integration/{config-loader.test.ts → config/config-loader.test.ts} +2 -1
  82. package/test/integration/{config.test.ts → config/config.test.ts} +2 -2
  83. package/test/integration/config/merger.test.ts +1 -0
  84. package/test/integration/config/paths.test.ts +1 -0
  85. package/test/integration/{security-loader.test.ts → config/security-loader.test.ts} +2 -2
  86. package/test/integration/{context-integration.test.ts → context/context-integration.test.ts} +7 -6
  87. package/test/integration/{path-security.test.ts → context/context-path-security.test.ts} +2 -2
  88. package/test/integration/{context-provider-injection.test.ts → context/context-provider-injection.test.ts} +7 -6
  89. package/test/integration/{context-verification-integration.test.ts → context/context-verification-integration.test.ts} +5 -4
  90. package/test/integration/{s5-greenfield-fallback.test.ts → context/s5-greenfield-fallback.test.ts} +4 -3
  91. package/test/integration/{isolation.test.ts → execution/execution-isolation.test.ts} +1 -1
  92. package/test/integration/{execution.test.ts → execution/execution.test.ts} +8 -8
  93. package/test/integration/{parallel.test.ts → execution/parallel.test.ts} +2 -1
  94. package/test/integration/{prd-pause.test.ts → execution/prd-pause.test.ts} +2 -2
  95. package/test/integration/{prd-resolvers.test.ts → execution/prd-resolvers.test.ts} +3 -2
  96. package/test/integration/{progress.test.ts → execution/progress.test.ts} +1 -1
  97. package/test/integration/execution/runner-batching.test.ts +682 -0
  98. package/test/integration/{runner-config-plugins.test.ts → execution/runner-config-plugins.test.ts} +3 -2
  99. package/test/integration/execution/runner-escalation.test.ts +561 -0
  100. package/test/integration/{runner-fixes.test.ts → execution/runner-fixes.test.ts} +4 -3
  101. package/test/integration/{runner-plugin-integration.test.ts → execution/runner-plugin-integration.test.ts} +6 -5
  102. package/test/integration/execution/runner-queue-and-attempts.test.ts +476 -0
  103. package/test/integration/{status-file-integration.test.ts → execution/status-file-integration.test.ts} +9 -8
  104. package/test/integration/{status-file.test.ts → execution/status-file.test.ts} +3 -2
  105. package/test/integration/{status-writer.test.ts → execution/status-writer.test.ts} +5 -4
  106. package/test/integration/{story-id-in-events.test.ts → execution/story-id-in-events.test.ts} +9 -8
  107. package/test/integration/{interaction-chain-pipeline.test.ts → interaction/interaction-chain-pipeline.test.ts} +26 -14
  108. package/test/integration/{hooks.test.ts → pipeline/hooks.test.ts} +4 -2
  109. package/test/integration/{pipeline-acceptance.test.ts → pipeline/pipeline-acceptance.test.ts} +7 -6
  110. package/test/integration/{pipeline-events.test.ts → pipeline/pipeline-events.test.ts} +7 -6
  111. package/test/integration/{pipeline.test.ts → pipeline/pipeline.test.ts} +9 -7
  112. package/test/integration/{reporter-lifecycle.test.ts → pipeline/reporter-lifecycle.test.ts} +9 -7
  113. package/test/integration/{verify-stage.test.ts → pipeline/verify-stage.test.ts} +7 -5
  114. package/test/integration/{analyze-integration.test.ts → plan/analyze-integration.test.ts} +3 -2
  115. package/test/integration/{analyze-scanner.test.ts → plan/analyze-scanner.test.ts} +8 -7
  116. package/test/integration/{logger.test.ts → plan/logger.test.ts} +1 -1
  117. package/test/integration/{plan.test.ts → plan/plan.test.ts} +3 -3
  118. package/test/integration/plugins/config-integration.test.ts +1 -0
  119. package/test/integration/plugins/config-resolution.test.ts +1 -0
  120. package/test/integration/plugins/loader.test.ts +1 -0
  121. package/test/integration/plugins/{registry.test.ts → plugins-registry.test.ts} +1 -0
  122. package/test/integration/plugins/validator.test.ts +1 -0
  123. package/test/integration/{review-config-commands.test.ts → review/review-config-commands.test.ts} +4 -3
  124. package/test/integration/{review-config-schema.test.ts → review/review-config-schema.test.ts} +3 -2
  125. package/test/integration/{review-plugin-integration.test.ts → review/review-plugin-integration.test.ts} +5 -4
  126. package/test/integration/{review.test.ts → review/review.test.ts} +3 -2
  127. package/test/integration/routing/plugin-routing-advanced.test.ts +461 -0
  128. package/test/integration/{plugin-routing.test.ts → routing/plugin-routing-core.test.ts} +10 -404
  129. package/test/integration/{routing-stage-bug-021.test.ts → routing/routing-stage-bug-021.test.ts} +8 -7
  130. package/test/integration/{routing-stage-greenfield.test.ts → routing/routing-stage-greenfield.test.ts} +7 -6
  131. package/test/integration/{tdd-cleanup.test.ts → tdd/tdd-cleanup.test.ts} +1 -1
  132. package/test/integration/tdd/tdd-orchestrator-core.test.ts +565 -0
  133. package/test/integration/tdd/tdd-orchestrator-failureCategory.test.ts +355 -0
  134. package/test/integration/tdd/tdd-orchestrator-fallback.test.ts +311 -0
  135. package/test/integration/tdd/tdd-orchestrator-lite.test.ts +289 -0
  136. package/test/integration/tdd/tdd-orchestrator-prompts.test.ts +260 -0
  137. package/test/integration/tdd/tdd-orchestrator-verdict.test.ts +536 -0
  138. package/test/integration/tmp/headless-test/test.jsonl +30 -0
  139. package/test/integration/{test-scanner.test.ts → verification/test-scanner.test.ts} +1 -1
  140. package/test/integration/{verification-asset-check.test.ts → verification/verification-asset-check.test.ts} +3 -2
  141. package/test/unit/acceptance.test.ts +1 -0
  142. package/test/unit/agent-stderr-capture.test.ts +1 -0
  143. package/test/unit/agents/claude.test.ts +107 -0
  144. package/test/unit/analyze-classifier.test.ts +1 -0
  145. package/test/unit/auto-detect.test.ts +1 -0
  146. package/test/unit/cli-status.test.ts +1 -0
  147. package/test/unit/commands/common.test.ts +1 -0
  148. package/test/unit/commands/logs.test.ts +1 -0
  149. package/test/unit/commands/unlock.test.ts +1 -0
  150. package/test/unit/config/defaults.test.ts +1 -0
  151. package/test/unit/config/regression-gate-schema.test.ts +1 -0
  152. package/test/unit/config/smart-runner-flag.test.ts +1 -0
  153. package/test/unit/constitution-generators.test.ts +1 -0
  154. package/test/unit/constitution.test.ts +1 -0
  155. package/test/unit/context/context-autodetect.test.ts +297 -0
  156. package/test/unit/context/context-build.test.ts +575 -0
  157. package/test/unit/context/context-coverage.test.ts +236 -0
  158. package/test/unit/context/context-error.test.ts +93 -0
  159. package/test/unit/context/context-estimate-tokens.test.ts +201 -0
  160. package/test/unit/context/context-format.test.ts +302 -0
  161. package/test/unit/context/context-isolation.test.ts +267 -0
  162. package/test/unit/context/context-sort.test.ts +93 -0
  163. package/test/unit/context/context-story.test.ts +108 -0
  164. package/test/{context → unit/context}/prior-failures.test.ts +5 -4
  165. package/test/unit/context.test.ts +7 -3
  166. package/test/unit/crash-recovery.test.ts +1 -0
  167. package/test/unit/escalation.test.ts +1 -0
  168. package/test/unit/execution/lifecycle/run-completion.test.ts +1 -0
  169. package/test/unit/execution/lifecycle/run-regression.test.ts +2 -0
  170. package/test/{execution → unit/execution}/pid-registry.test.ts +2 -1
  171. package/test/{execution → unit/execution}/structured-failure.test.ts +3 -2
  172. package/test/unit/execution-logging-stderr.test.ts +1 -0
  173. package/test/unit/execution-stage.test.ts +1 -0
  174. package/test/unit/fix-generator.test.ts +1 -0
  175. package/test/unit/greenfield.test.ts +1 -0
  176. package/test/unit/interaction/human-review-trigger.test.ts +1 -0
  177. package/test/unit/interaction-network-failures.test.ts +1 -0
  178. package/test/unit/interaction-plugins.test.ts +1 -0
  179. package/test/unit/logging/formatter.test.ts +1 -0
  180. package/test/unit/merge.test.ts +1 -0
  181. package/test/unit/pipeline/event-bus.test.ts +105 -0
  182. package/test/unit/pipeline/routing-partial-override.test.ts +1 -0
  183. package/test/unit/pipeline/runner-retry.test.ts +89 -0
  184. package/test/unit/pipeline/stages/autofix.test.ts +97 -0
  185. package/test/unit/pipeline/stages/rectify.test.ts +101 -0
  186. package/test/unit/pipeline/stages/regression-stage.test.ts +69 -0
  187. package/test/unit/pipeline/stages/verify.test.ts +1 -0
  188. package/test/unit/pipeline/subscribers/hooks.test.ts +45 -0
  189. package/test/unit/pipeline/subscribers/interaction.test.ts +31 -0
  190. package/test/unit/pipeline/subscribers/reporters.test.ts +90 -0
  191. package/test/unit/pipeline/verify-smart-runner.test.ts +2 -1
  192. package/test/unit/prd-auto-default.test.ts +3 -2
  193. package/test/unit/prd-failure-category.test.ts +1 -0
  194. package/test/unit/prd-get-next-story.test.ts +1 -0
  195. package/test/unit/precheck-checks.test.ts +1 -0
  196. package/test/unit/precheck-story-size-gate.test.ts +1 -0
  197. package/test/unit/precheck-types.test.ts +1 -0
  198. package/test/unit/prompts.test.ts +1 -0
  199. package/test/unit/rectification.test.ts +2 -1
  200. package/test/unit/registry.test.ts +1 -0
  201. package/test/unit/routing/routing-stability.test.ts +2 -1
  202. package/test/unit/routing/strategies/llm.test.ts +251 -0
  203. package/test/unit/routing-advanced.test.ts +313 -0
  204. package/test/unit/routing-core.test.ts +341 -0
  205. package/test/unit/routing-strategies.test.ts +442 -0
  206. package/test/unit/storyid-events.test.ts +1 -0
  207. package/test/{ui → unit/ui}/tui-controls.test.ts +8 -7
  208. package/test/{ui → unit/ui}/tui-cost-and-pty.test.ts +4 -3
  209. package/test/{ui → unit/ui}/tui-layout.test.ts +5 -4
  210. package/test/{ui → unit/ui}/tui-stories.test.ts +5 -4
  211. package/test/unit/{isolation.test.ts → unit-isolation.test.ts} +1 -0
  212. package/test/unit/{helpers.test.ts → utils-helpers.test.ts} +1 -0
  213. package/test/unit/verdict.test.ts +1 -0
  214. package/test/unit/verification/orchestrator-types.test.ts +54 -0
  215. package/test/unit/verification/orchestrator.test.ts +66 -0
  216. package/test/unit/verification/smart-runner-config.test.ts +1 -0
  217. package/test/unit/verification/smart-runner-discovery.test.ts +8 -7
  218. package/test/unit/verification/strategies/acceptance.test.ts +33 -0
  219. package/test/unit/verification/strategies/regression.test.ts +87 -0
  220. package/test/unit/verification/strategies/scoped.test.ts +100 -0
  221. package/test/unit/worktree-manager.test.ts +1 -0
  222. package/src/execution/lifecycle/story-hooks.ts +0 -38
  223. package/src/execution/post-verify.ts +0 -193
  224. package/src/execution/rectification.ts +0 -13
  225. package/src/execution/verification.ts +0 -72
  226. package/test/integration/rectification-flow.test.ts +0 -512
  227. package/test/integration/runner.test.ts +0 -1679
  228. package/test/integration/tdd-orchestrator.test.ts +0 -1762
  229. package/test/unit/execution/post-verify-regression.test.ts +0 -362
  230. package/test/unit/execution/post-verify.test.ts +0 -236
  231. package/test/unit/routing.test.ts +0 -1039
  232. /package/test/{integration → helpers}/helpers.test.ts +0 -0
  233. /package/test/integration/worktree/{merge.test.ts → worktree-merge.test.ts} +0 -0
@@ -0,0 +1,22 @@
1
+ # FEAT-013 — Deprecate test-after from auto routing
2
+
3
+ ## Decision
4
+ Remove `test-after` as the default fallback in `auto` mode.
5
+ Change simple/medium stories to route to `three-session-tdd-lite` instead.
6
+
7
+ ## Why
8
+ - `test-after` has high test failure rate — tests rubber-stamp implementation rather than driving design
9
+ - Failed test-after stories escalate to powerful tier anyway → costs more in the end
10
+ - `three-session-tdd-lite` = better quality at moderate cost increase
11
+
12
+ ## New auto routing decision tree
13
+ ```
14
+ security/public-api keywords → three-session-tdd
15
+ complex/expert complexity → three-session-tdd (or lite if ui/cli tags)
16
+ simple/medium (default) → three-session-tdd-lite ← was test-after
17
+ ```
18
+
19
+ ## Backward compatibility
20
+ - `tddStrategy: "off"` in config still routes to `test-after` (explicit opt-in)
21
+ - `tddStrategy: "strict"` → three-session-tdd (unchanged)
22
+ - `tddStrategy: "lite"` → three-session-tdd-lite (unchanged)
package/nax/config.json CHANGED
@@ -60,7 +60,7 @@
60
60
  "iterationDelayMs": 2000,
61
61
  "costLimit": 8.0,
62
62
  "sessionTimeoutSeconds": 7200,
63
- "verificationTimeoutSeconds": 300,
63
+ "verificationTimeoutSeconds": 600,
64
64
  "maxStoriesPerFeature": 15,
65
65
  "rectification": {
66
66
  "enabled": true,
@@ -70,8 +70,11 @@
70
70
  "abortOnIncreasingFailures": true
71
71
  },
72
72
  "regressionGate": {
73
- "enabled": false,
74
- "timeoutSeconds": 300
73
+ "enabled": true,
74
+ "mode": "deferred",
75
+ "timeoutSeconds": 600,
76
+ "acceptOnTimeout": true,
77
+ "maxRectificationAttempts": 2
75
78
  }
76
79
  },
77
80
  "quality": {
@@ -147,4 +150,4 @@
147
150
  "scopeToStory": true
148
151
  }
149
152
  }
150
- }
153
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "project": "nax",
3
+ "feature": "bug-039-medium",
4
+ "branchName": "fix/bug-039-process-cleanup-medium",
5
+ "createdAt": "2026-03-06T04:51:00.000Z",
6
+ "updatedAt": "2026-03-06T04:51:00.000Z",
7
+ "userStories": [
8
+ {
9
+ "id": "US-001",
10
+ "title": "runOnce() SIGKILL follow-up after grace period",
11
+ "description": "In src/agents/claude.ts:runOnce(), the timeout handler only sends SIGTERM but never follows up with SIGKILL. Claude may ignore SIGTERM and run indefinitely. Add a SIGKILL after a 5-second grace period, mirroring the pattern in src/verification/executor.ts:executeWithTimeout(). Also move pidRegistry.unregister(processPid) into a finally block so it is always called even if the timeout path throws an exception.",
12
+ "acceptanceCriteria": [
13
+ "When runOnce() times out, SIGTERM is sent first",
14
+ "After 5 seconds grace period, SIGKILL is sent to ensure the process is dead",
15
+ "pidRegistry.unregister(processPid) is called in a finally block — always runs regardless of success or exception path",
16
+ "Existing runOnce() tests still pass",
17
+ "New test: timeout path always unregisters PID even if SIGTERM/SIGKILL throws"
18
+ ],
19
+ "tags": ["bug", "process", "cleanup"],
20
+ "dependencies": [],
21
+ "status": "pending",
22
+ "passes": false,
23
+ "escalations": [],
24
+ "attempts": 0
25
+ },
26
+ {
27
+ "id": "US-002",
28
+ "title": "LLM routing stream drain fix on timeout",
29
+ "description": "In src/routing/strategies/llm.ts:callLlmOnce(), when the timeout fires, proc.kill() is called but the stdout/stderr ReadableStreams from the Bun.spawn() are never cancelled or drained. This can cause proc.exited to hang indefinitely because Bun's piped streams may block exit. Fix: before calling proc.kill(), cancel both streams with proc.stdout.cancel() and proc.stderr.cancel() wrapped in try/catch. This ensures proc.exited resolves promptly after kill.",
30
+ "acceptanceCriteria": [
31
+ "On LLM timeout, proc.stdout and proc.stderr are cancelled before proc.kill()",
32
+ "proc.exited resolves promptly after timeout kill (does not hang)",
33
+ "clearTimeout(timeoutId) still called in both success and error paths",
34
+ "Existing LLM routing tests still pass",
35
+ "New test: timeout path resolves without hanging (use fake timers or short timeout)"
36
+ ],
37
+ "tags": ["bug", "process", "cleanup", "llm"],
38
+ "dependencies": [],
39
+ "status": "pending",
40
+ "passes": false,
41
+ "escalations": [],
42
+ "attempts": 0
43
+ }
44
+ ]
45
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "AI Coding Agent Orchestrator \u2014 loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,4 +44,4 @@
44
44
  "tdd",
45
45
  "coding"
46
46
  ]
47
- }
47
+ }
@@ -36,6 +36,24 @@ const MAX_AGENT_OUTPUT_CHARS = 5000;
36
36
  */
37
37
  const MAX_AGENT_STDERR_CHARS = 1000;
38
38
 
39
+ /**
40
+ * Grace period in ms between SIGTERM and SIGKILL on timeout.
41
+ * Mirrors the pattern in src/verification/executor.ts:executeWithTimeout().
42
+ */
43
+ const SIGKILL_GRACE_PERIOD_MS = 5000;
44
+
45
+ /**
46
+ * Injectable dependencies for runOnce() — allows tests to verify
47
+ * that PID cleanup (unregister) always runs even if kill() throws.
48
+ *
49
+ * @internal
50
+ */
51
+ export const _runOnceDeps = {
52
+ killProc(proc: { kill(signal?: number | NodeJS.Signals): void }, signal: NodeJS.Signals): void {
53
+ proc.kill(signal);
54
+ },
55
+ };
56
+
39
57
  /**
40
58
  * Claude Code agent adapter implementation.
41
59
  *
@@ -183,25 +201,69 @@ export class ClaudeCodeAdapter implements AgentAdapter {
183
201
  let timedOut = false;
184
202
  const timeoutId = setTimeout(() => {
185
203
  timedOut = true;
186
- proc.kill("SIGTERM");
204
+ try {
205
+ _runOnceDeps.killProc(proc, "SIGTERM" as NodeJS.Signals);
206
+ } catch {
207
+ /* already exited */
208
+ }
209
+ setTimeout(() => {
210
+ try {
211
+ _runOnceDeps.killProc(proc, "SIGKILL" as NodeJS.Signals);
212
+ } catch {
213
+ /* already exited */
214
+ }
215
+ }, SIGKILL_GRACE_PERIOD_MS);
187
216
  }, options.timeoutSeconds * 1000);
188
217
 
189
- const exitCode = await proc.exited;
190
- clearTimeout(timeoutId);
191
-
192
- await pidRegistry.unregister(processPid);
218
+ let exitCode: number;
219
+ try {
220
+ // Hard deadline: if proc.exited doesn't resolve after kill signals are sent
221
+ // (Bun subprocess edge case on some environments), fall back to -1 so the
222
+ // caller can move on. timedOut flag ensures the result is still marked 124.
223
+ const hardDeadlineMs = options.timeoutSeconds * 1000 + SIGKILL_GRACE_PERIOD_MS + 3000;
224
+ exitCode = await Promise.race([
225
+ proc.exited,
226
+ new Promise<number>((resolve) => setTimeout(() => resolve(-1), hardDeadlineMs)),
227
+ ]);
228
+
229
+ // If hard deadline fired, the subprocess may still be alive (Bun SIGKILL edge case).
230
+ // Force-kill via OS-level signal so that the stdout pipe closes and we don't block.
231
+ if (exitCode === -1) {
232
+ try {
233
+ process.kill(processPid, "SIGKILL");
234
+ } catch {
235
+ /* already gone */
236
+ }
237
+ // Note: process.kill(-pid) (process group) only works if the child called
238
+ // setpgid/setsid. Bun does not do this, so this will silently throw ESRCH.
239
+ // Left here as a best-effort safety net for environments that do set a pgid.
240
+ try {
241
+ process.kill(-processPid, "SIGKILL");
242
+ } catch {
243
+ /* no process group — expected in most environments */
244
+ }
245
+ }
246
+ } finally {
247
+ clearTimeout(timeoutId);
248
+ await pidRegistry.unregister(processPid);
249
+ }
193
250
 
194
- const stdout = await new Response(proc.stdout).text();
195
- const stderr = await new Response(proc.stderr).text();
251
+ // Use a deadline on stdout read if the subprocess pipe is still open
252
+ // (e.g. hard-deadline fired but process didn't fully die), don't block forever.
253
+ const stdout = await Promise.race([
254
+ new Response(proc.stdout).text(),
255
+ new Promise<string>((resolve) => setTimeout(() => resolve(""), 5000)),
256
+ ]);
257
+ const stderr = proc.stderr ? await new Response(proc.stderr).text() : "";
196
258
  const durationMs = Date.now() - startTime;
197
259
 
260
+ // Claude Code emits rate limit messages as part of its output.
261
+ const fullOutput = stdout + stderr;
198
262
  const rateLimited =
199
- stderr.includes("rate limit") ||
200
- stderr.includes("429") ||
201
- stdout.includes("rate limit") ||
202
- stdout.includes("Too many requests");
263
+ fullOutput.toLowerCase().includes("rate limit") ||
264
+ fullOutput.includes("429") ||
265
+ fullOutput.toLowerCase().includes("too many requests");
203
266
 
204
- const fullOutput = stdout + stderr;
205
267
  let costEstimate = estimateCostFromOutput(options.modelTier, fullOutput);
206
268
  const logger = getLogger();
207
269
  if (!costEstimate) {
@@ -267,11 +329,43 @@ export class ClaudeCodeAdapter implements AgentAdapter {
267
329
 
268
330
  await pidRegistry.register(proc.pid);
269
331
 
270
- const exitCode = await proc.exited;
332
+ // BUG-039: Hard timeout for decompose — prevents infinite hang if claude hangs
333
+ const DECOMPOSE_TIMEOUT_MS = 300_000; // 5 minutes
334
+ let timedOut = false;
335
+ const decomposeTimerId = setTimeout(() => {
336
+ timedOut = true;
337
+ try {
338
+ proc.kill("SIGTERM");
339
+ } catch {
340
+ /* already exited */
341
+ }
342
+ setTimeout(() => {
343
+ try {
344
+ proc.kill("SIGKILL");
345
+ } catch {
346
+ /* already exited */
347
+ }
348
+ }, 5000);
349
+ }, DECOMPOSE_TIMEOUT_MS);
350
+
351
+ let exitCode: number;
352
+ try {
353
+ exitCode = await proc.exited;
354
+ } finally {
355
+ clearTimeout(decomposeTimerId);
356
+ await pidRegistry.unregister(proc.pid);
357
+ }
271
358
 
272
- await pidRegistry.unregister(proc.pid);
359
+ if (timedOut) {
360
+ throw new Error(`Decompose timed out after ${DECOMPOSE_TIMEOUT_MS / 1000}s`);
361
+ }
273
362
 
274
- const stdout = await new Response(proc.stdout).text();
363
+ // Use a deadline on stdout read if the subprocess pipe is still open
364
+ // (e.g. hard-deadline fired but process didn't fully die), don't block forever.
365
+ const stdout = await Promise.race([
366
+ new Response(proc.stdout).text(),
367
+ new Promise<string>((resolve) => setTimeout(() => resolve(""), 5000)),
368
+ ]);
275
369
  const stderr = await new Response(proc.stderr).text();
276
370
 
277
371
  if (exitCode !== 0) {
@@ -140,6 +140,17 @@ export interface QualityConfig {
140
140
  typecheck?: string;
141
141
  lint?: string;
142
142
  test?: string;
143
+ /** Auto-fix lint errors (e.g., "biome check --fix") */
144
+ lintFix?: string;
145
+ /** Auto-fix formatting (e.g., "biome format --write") */
146
+ formatFix?: string;
147
+ };
148
+ /** Auto-fix configuration (Phase 2) */
149
+ autofix?: {
150
+ /** Whether to auto-fix lint/format errors before escalating (default: true) */
151
+ enabled?: boolean;
152
+ /** Max auto-fix attempts (default: 2) */
153
+ maxAttempts?: number;
143
154
  };
144
155
  /** Append --forceExit to test command to prevent open handle hangs (default: false) */
145
156
  forceExit: boolean;
@@ -232,13 +232,21 @@ async function addFileElements(
232
232
  continue;
233
233
  }
234
234
  if (file.size > MAX_FILE_SIZE_BYTES) {
235
+ // FEAT-011: File too large to inline — pass path-only so agent can read it if needed
235
236
  const logger = getLogger();
236
- logger.warn("context", "File too large", {
237
+ logger.warn("context", "File too large for inline — using path-only", {
237
238
  filePath: relativeFilePath,
238
239
  sizeKB: Math.round(file.size / 1024),
239
240
  maxKB: 10,
240
241
  storyId: story.id,
241
242
  });
243
+ elements.push(
244
+ createFileContext(
245
+ relativeFilePath,
246
+ `_File too large to inline (${Math.round(file.size / 1024)}KB). Path: \`${relativeFilePath}\` — read it directly if needed._`,
247
+ 5,
248
+ ),
249
+ );
242
250
  continue;
243
251
  }
244
252
  const content = await file.text();
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Dry Run Handler (ADR-005, Phase 4)
3
+ *
4
+ * Extracted from pipeline-result-handler.ts to slim that file below 200 lines.
5
+ */
6
+
7
+ import { getSafeLogger } from "../logger";
8
+ import { pipelineEventBus } from "../pipeline/event-bus";
9
+ import type { PluginRegistry } from "../plugins";
10
+ import { markStoryPassed, savePRD } from "../prd";
11
+ import type { PRD, UserStory } from "../prd/types";
12
+ import type { routeTask } from "../routing";
13
+ import type { StatusWriter } from "./status-writer";
14
+
15
+ export interface DryRunContext {
16
+ prd: PRD;
17
+ prdPath: string;
18
+ storiesToExecute: UserStory[];
19
+ routing: ReturnType<typeof routeTask>;
20
+ statusWriter: StatusWriter;
21
+ pluginRegistry: PluginRegistry;
22
+ runId: string;
23
+ totalCost: number;
24
+ iterations: number;
25
+ }
26
+
27
+ export interface DryRunResult {
28
+ storiesCompletedDelta: number;
29
+ prdDirty: boolean;
30
+ }
31
+
32
+ /** Handle dry-run iteration: log what would happen, mark stories passed. */
33
+ export async function handleDryRun(ctx: DryRunContext): Promise<DryRunResult> {
34
+ const logger = getSafeLogger();
35
+
36
+ ctx.statusWriter.setPrd(ctx.prd);
37
+ ctx.statusWriter.setCurrentStory({
38
+ storyId: ctx.storiesToExecute[0].id,
39
+ title: ctx.storiesToExecute[0].title,
40
+ complexity: ctx.routing.complexity,
41
+ tddStrategy: ctx.routing.testStrategy,
42
+ model: ctx.routing.modelTier,
43
+ attempt: (ctx.storiesToExecute[0].attempts ?? 0) + 1,
44
+ phase: "routing",
45
+ });
46
+ await ctx.statusWriter.update(ctx.totalCost, ctx.iterations);
47
+
48
+ for (const s of ctx.storiesToExecute) {
49
+ logger?.info("execution", "[DRY RUN] Would execute agent here", {
50
+ storyId: s.id,
51
+ storyTitle: s.title,
52
+ modelTier: ctx.routing.modelTier,
53
+ complexity: ctx.routing.complexity,
54
+ testStrategy: ctx.routing.testStrategy,
55
+ });
56
+ }
57
+
58
+ for (const s of ctx.storiesToExecute) {
59
+ markStoryPassed(ctx.prd, s.id);
60
+ }
61
+ await savePRD(ctx.prd, ctx.prdPath);
62
+
63
+ for (const s of ctx.storiesToExecute) {
64
+ pipelineEventBus.emit({
65
+ type: "story:completed",
66
+ storyId: s.id,
67
+ story: s,
68
+ passed: true,
69
+ durationMs: 0,
70
+ cost: 0,
71
+ modelTier: ctx.routing.modelTier,
72
+ testStrategy: ctx.routing.testStrategy,
73
+ });
74
+ }
75
+
76
+ ctx.statusWriter.setPrd(ctx.prd);
77
+ ctx.statusWriter.setCurrentStory(null);
78
+ await ctx.statusWriter.update(ctx.totalCost, ctx.iterations);
79
+
80
+ return { storiesCompletedDelta: ctx.storiesToExecute.length, prdDirty: true };
81
+ }
@@ -3,13 +3,14 @@
3
3
  *
4
4
  * Extracted from tier-escalation.ts: handles outcomes when escalation
5
5
  * is not possible (no tier available or max attempts reached).
6
+ *
7
+ * Phase 3 (ADR-005): Replaced direct fireHook() calls with event bus emissions.
6
8
  */
7
9
 
8
- import { fireHook } from "../../hooks";
9
10
  import { getSafeLogger } from "../../logger";
11
+ import { pipelineEventBus } from "../../pipeline/event-bus";
10
12
  import { markStoryFailed, markStoryPaused, savePRD } from "../../prd";
11
13
  import type { FailureCategory } from "../../tdd/types";
12
- import { hookCtx } from "../helpers";
13
14
  import { appendProgress } from "../progress";
14
15
  import type { EscalationHandlerContext, EscalationHandlerResult } from "./tier-escalation";
15
16
  import { resolveMaxAttemptsOutcome } from "./tier-escalation";
@@ -43,16 +44,12 @@ export async function handleNoTierAvailable(
43
44
  );
44
45
  }
45
46
 
46
- await fireHook(
47
- ctx.hooks,
48
- "on-pause",
49
- hookCtx(ctx.feature, {
50
- storyId: ctx.story.id,
51
- reason: `Execution stopped (${failureCategory ?? "unknown"} requires human review)`,
52
- cost: ctx.totalCost,
53
- }),
54
- ctx.workdir,
55
- );
47
+ pipelineEventBus.emit({
48
+ type: "story:paused",
49
+ storyId: ctx.story.id,
50
+ reason: `Execution stopped (${failureCategory ?? "unknown"} requires human review)`,
51
+ cost: ctx.totalCost,
52
+ });
56
53
 
57
54
  return { outcome: "paused", prdDirty: true, prd: pausedPrd };
58
55
  }
@@ -70,17 +67,13 @@ export async function handleNoTierAvailable(
70
67
  await appendProgress(ctx.featureDir, ctx.story.id, "failed", `${ctx.story.title} — Execution failed`);
71
68
  }
72
69
 
73
- await fireHook(
74
- ctx.hooks,
75
- "on-story-fail",
76
- hookCtx(ctx.feature, {
77
- storyId: ctx.story.id,
78
- status: "failed",
79
- reason: "Execution failed",
80
- cost: ctx.totalCost,
81
- }),
82
- ctx.workdir,
83
- );
70
+ pipelineEventBus.emit({
71
+ type: "story:failed",
72
+ storyId: ctx.story.id,
73
+ story: ctx.story,
74
+ reason: "Execution failed",
75
+ countsTowardEscalation: true,
76
+ });
84
77
 
85
78
  return { outcome: "failed", prdDirty: true, prd: failedPrd };
86
79
  }
@@ -114,16 +107,12 @@ export async function handleMaxAttemptsReached(
114
107
  );
115
108
  }
116
109
 
117
- await fireHook(
118
- ctx.hooks,
119
- "on-pause",
120
- hookCtx(ctx.feature, {
121
- storyId: ctx.story.id,
122
- reason: `Max attempts reached (${failureCategory ?? "unknown"} requires human review)`,
123
- cost: ctx.totalCost,
124
- }),
125
- ctx.workdir,
126
- );
110
+ pipelineEventBus.emit({
111
+ type: "story:paused",
112
+ storyId: ctx.story.id,
113
+ reason: `Max attempts reached (${failureCategory ?? "unknown"} requires human review)`,
114
+ cost: ctx.totalCost,
115
+ });
127
116
 
128
117
  return { outcome: "paused", prdDirty: true, prd: pausedPrd };
129
118
  }
@@ -142,17 +131,13 @@ export async function handleMaxAttemptsReached(
142
131
  await appendProgress(ctx.featureDir, ctx.story.id, "failed", `${ctx.story.title} — Max attempts reached`);
143
132
  }
144
133
 
145
- await fireHook(
146
- ctx.hooks,
147
- "on-story-fail",
148
- hookCtx(ctx.feature, {
149
- storyId: ctx.story.id,
150
- status: "failed",
151
- reason: "Max attempts reached",
152
- cost: ctx.totalCost,
153
- }),
154
- ctx.workdir,
155
- );
134
+ pipelineEventBus.emit({
135
+ type: "story:failed",
136
+ storyId: ctx.story.id,
137
+ story: ctx.story,
138
+ reason: "Max attempts reached",
139
+ countsTowardEscalation: true,
140
+ });
156
141
 
157
142
  return { outcome: "failed", prdDirty: true, prd: failedPrd };
158
143
  }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Sequential Executor Types (ADR-005, Phase 4)
3
+ *
4
+ * Extracted from sequential-executor.ts to slim it below 200 lines.
5
+ */
6
+
7
+ import type { NaxConfig } from "../config";
8
+ import type { LoadedHooksConfig } from "../hooks";
9
+ import type { InteractionChain } from "../interaction/chain";
10
+ import type { StoryMetrics } from "../metrics";
11
+ import type { PipelineEventEmitter } from "../pipeline/events";
12
+ import type { RoutingResult } from "../pipeline/types";
13
+ import type { PluginRegistry } from "../plugins";
14
+ import type { PRD, UserStory } from "../prd/types";
15
+ import type { StoryBatch } from "./batching";
16
+ import type { StatusWriter } from "./status-writer";
17
+
18
+ export interface SequentialExecutionContext {
19
+ prdPath: string;
20
+ workdir: string;
21
+ config: NaxConfig;
22
+ hooks: LoadedHooksConfig;
23
+ feature: string;
24
+ featureDir?: string;
25
+ dryRun: boolean;
26
+ useBatch: boolean;
27
+ pluginRegistry: PluginRegistry;
28
+ eventEmitter?: PipelineEventEmitter;
29
+ statusWriter: StatusWriter;
30
+ logFilePath?: string;
31
+ runId: string;
32
+ startTime: number;
33
+ batchPlan: StoryBatch[];
34
+ interactionChain?: InteractionChain | null;
35
+ }
36
+
37
+ export interface SequentialExecutionResult {
38
+ prd: PRD;
39
+ iterations: number;
40
+ storiesCompleted: number;
41
+ totalCost: number;
42
+ allStoryMetrics: StoryMetrics[];
43
+ exitReason: "completed" | "cost-limit" | "max-iterations" | "stalled" | "no-stories";
44
+ }
45
+
46
+ /**
47
+ * Build a preview routing from cached story.routing or config defaults.
48
+ * The pipeline routing stage performs full classification and overwrites ctx.routing.
49
+ * This preview is used only for logging, status display, and event emission.
50
+ */
51
+ export function buildPreviewRouting(story: UserStory, config: NaxConfig): RoutingResult {
52
+ const cached = story.routing;
53
+ const defaultComplexity = "medium" as const;
54
+ const defaultTier = "balanced" as const;
55
+ const defaultStrategy = "test-after" as const;
56
+ return {
57
+ complexity: (cached?.complexity as RoutingResult["complexity"]) ?? defaultComplexity,
58
+ modelTier:
59
+ (cached?.modelTier as RoutingResult["modelTier"]) ??
60
+ (config.autoMode.complexityRouting?.[defaultComplexity] as RoutingResult["modelTier"]) ??
61
+ defaultTier,
62
+ testStrategy: (cached?.testStrategy as RoutingResult["testStrategy"]) ?? defaultStrategy,
63
+ reasoning: cached ? "cached from story.routing" : "preview (pending pipeline routing stage)",
64
+ };
65
+ }
@@ -5,23 +5,6 @@ export { appendProgress } from "./progress";
5
5
  export { buildSingleSessionPrompt, buildBatchPrompt } from "./prompts";
6
6
  export { groupStoriesIntoBatches, type StoryBatch } from "./batching";
7
7
  export { escalateTier, getTierConfig, calculateMaxIterations } from "./escalation";
8
- export {
9
- verifyAssets,
10
- executeWithTimeout,
11
- parseTestOutput,
12
- getEnvironmentalEscalationThreshold,
13
- normalizeEnvironment,
14
- buildTestCommand,
15
- appendOpenHandlesFlag,
16
- appendForceExitFlag,
17
- runVerification,
18
- type VerificationResult,
19
- type VerificationStatus,
20
- type TestOutputAnalysis,
21
- type AssetVerificationResult,
22
- type TimeoutExecutionResult,
23
- } from "./verification";
24
- export { runPostAgentVerification, type PostVerifyOptions, type PostVerifyResult } from "./post-verify";
25
8
  export { readQueueFile, clearQueueFile } from "./queue-handler";
26
9
  export {
27
10
  hookCtx,