@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,132 @@
1
+ /**
2
+ * Iteration Runner (ADR-005, Phase 4)
3
+ *
4
+ * Runs a single story through the pipeline.
5
+ * Extracted from sequential-executor.ts to slim it below 120 lines.
6
+ */
7
+
8
+ import { getSafeLogger } from "../logger";
9
+ import type { StoryMetrics } from "../metrics";
10
+ import { runPipeline } from "../pipeline/runner";
11
+ import type { PipelineRunResult } from "../pipeline/runner";
12
+ import { defaultPipeline } from "../pipeline/stages";
13
+ import type { PipelineContext } from "../pipeline/types";
14
+ import type { PRD } from "../prd/types";
15
+ import { captureGitRef } from "../utils/git";
16
+ import { handleDryRun } from "./dry-run";
17
+ import type { SequentialExecutionContext } from "./executor-types";
18
+ import { handlePipelineFailure, handlePipelineSuccess } from "./pipeline-result-handler";
19
+ import type { StorySelection } from "./story-selector";
20
+
21
+ export interface IterationResult {
22
+ prd: PRD;
23
+ storiesCompletedDelta: number;
24
+ costDelta: number;
25
+ prdDirty: boolean;
26
+ finalAction?: string;
27
+ reason?: string;
28
+ }
29
+
30
+ export async function runIteration(
31
+ ctx: SequentialExecutionContext,
32
+ prd: PRD,
33
+ selection: StorySelection,
34
+ iterations: number,
35
+ totalCost: number,
36
+ allStoryMetrics: StoryMetrics[],
37
+ ): Promise<IterationResult> {
38
+ const logger = getSafeLogger();
39
+ const { story, storiesToExecute, routing, isBatchExecution } = selection;
40
+
41
+ if (ctx.dryRun) {
42
+ const dryRunResult = await handleDryRun({
43
+ prd,
44
+ prdPath: ctx.prdPath,
45
+ storiesToExecute,
46
+ routing,
47
+ statusWriter: ctx.statusWriter,
48
+ pluginRegistry: ctx.pluginRegistry,
49
+ runId: ctx.runId,
50
+ totalCost,
51
+ iterations,
52
+ });
53
+ return {
54
+ prd,
55
+ storiesCompletedDelta: dryRunResult.storiesCompletedDelta,
56
+ costDelta: 0,
57
+ prdDirty: dryRunResult.prdDirty,
58
+ };
59
+ }
60
+
61
+ const storyGitRef = await captureGitRef(ctx.workdir);
62
+ const pipelineContext: PipelineContext = {
63
+ config: ctx.config,
64
+ prd,
65
+ story,
66
+ stories: storiesToExecute,
67
+ routing,
68
+ workdir: ctx.workdir,
69
+ featureDir: ctx.featureDir,
70
+ hooks: ctx.hooks,
71
+ plugins: ctx.pluginRegistry,
72
+ storyStartTime: new Date().toISOString(),
73
+ storyGitRef: storyGitRef ?? undefined,
74
+ interaction: ctx.interactionChain ?? undefined,
75
+ };
76
+
77
+ ctx.statusWriter.setPrd(prd);
78
+ ctx.statusWriter.setCurrentStory({
79
+ storyId: story.id,
80
+ title: story.title,
81
+ complexity: routing.complexity,
82
+ tddStrategy: routing.testStrategy,
83
+ model: routing.modelTier,
84
+ attempt: (story.attempts ?? 0) + 1,
85
+ phase: "routing",
86
+ });
87
+ await ctx.statusWriter.update(totalCost, iterations);
88
+
89
+ const pipelineResult = await runPipeline(defaultPipeline, pipelineContext, ctx.eventEmitter);
90
+ const currentPrd = pipelineResult.context.prd;
91
+
92
+ const handlerCtx = {
93
+ config: ctx.config,
94
+ prd: currentPrd,
95
+ prdPath: ctx.prdPath,
96
+ workdir: ctx.workdir,
97
+ featureDir: ctx.featureDir,
98
+ hooks: ctx.hooks,
99
+ feature: ctx.feature,
100
+ totalCost,
101
+ startTime: ctx.startTime,
102
+ runId: ctx.runId,
103
+ pluginRegistry: ctx.pluginRegistry,
104
+ story,
105
+ storiesToExecute,
106
+ routing: pipelineResult.context.routing ?? routing,
107
+ isBatchExecution,
108
+ allStoryMetrics,
109
+ storyGitRef,
110
+ interactionChain: ctx.interactionChain,
111
+ };
112
+
113
+ if (pipelineResult.success) {
114
+ const r = await handlePipelineSuccess(handlerCtx, pipelineResult);
115
+ return {
116
+ prd: r.prd,
117
+ storiesCompletedDelta: r.storiesCompletedDelta,
118
+ costDelta: r.costDelta,
119
+ prdDirty: r.prdDirty,
120
+ finalAction: pipelineResult.finalAction,
121
+ };
122
+ }
123
+ const r = await handlePipelineFailure(handlerCtx, pipelineResult);
124
+ return {
125
+ prd: r.prd,
126
+ storiesCompletedDelta: 0,
127
+ costDelta: 0,
128
+ prdDirty: r.prdDirty,
129
+ finalAction: pipelineResult.finalAction,
130
+ reason: pipelineResult.reason,
131
+ };
132
+ }
@@ -3,7 +3,6 @@
3
3
  */
4
4
 
5
5
  export { runAcceptanceLoop, type AcceptanceLoopContext, type AcceptanceLoopResult } from "./acceptance-loop";
6
- export { emitStoryComplete, type StoryCompleteEvent } from "./story-hooks";
7
6
  export { outputRunHeader, outputRunFooter, type RunHeaderOptions, type RunFooterOptions } from "./headless-formatter";
8
7
  export { handleParallelCompletion, type ParallelCompletionOptions } from "./parallel-lifecycle";
9
8
  export { handleRunCompletion, type RunCompletionOptions, type RunCompletionResult } from "./run-completion";
@@ -14,16 +14,16 @@ import type { PRD, UserStory } from "../../prd";
14
14
  import { countStories } from "../../prd";
15
15
  import { hasCommitsForStory } from "../../utils/git";
16
16
  import { parseBunTestOutput } from "../../verification";
17
+ import { runRectificationLoop } from "../../verification/rectification-loop";
18
+ import { fullSuite } from "../../verification/runners";
17
19
  import { reverseMapTestToSource } from "../../verification/smart-runner";
18
- import { runRectificationLoop } from "../post-verify-rectification";
19
- import { runVerification } from "../verification";
20
20
 
21
21
  /**
22
22
  * Injectable dependencies for testing (avoids mock.module() which leaks in Bun 1.x).
23
23
  * @internal - test use only.
24
24
  */
25
25
  export const _regressionDeps = {
26
- runVerification,
26
+ runVerification: fullSuite,
27
27
  runRectificationLoop,
28
28
  parseBunTestOutput,
29
29
  reverseMapTestToSource,
@@ -133,7 +133,7 @@ export async function runDeferredRegression(options: DeferredRegressionOptions):
133
133
 
134
134
  // Step 1: Run full test suite
135
135
  const fullSuiteResult = await _regressionDeps.runVerification({
136
- workingDirectory: workdir,
136
+ workdir: workdir,
137
137
  command: testCommand,
138
138
  timeoutSeconds,
139
139
  forceExit: config.quality.forceExit,
@@ -268,7 +268,7 @@ export async function runDeferredRegression(options: DeferredRegressionOptions):
268
268
  // Step 4: Re-run full suite to confirm
269
269
  logger?.info("regression", "Re-running full suite after rectification");
270
270
  const retryResult = await _regressionDeps.runVerification({
271
- workingDirectory: workdir,
271
+ workdir: workdir,
272
272
  command: testCommand,
273
273
  timeoutSeconds,
274
274
  forceExit: config.quality.forceExit,
@@ -1,31 +1,27 @@
1
1
  /**
2
- * Pipeline Result Handlers
2
+ * Pipeline Result Handlers (ADR-005, Phase 4)
3
3
  *
4
- * Extracted from sequential-executor.ts: handles pipeline success, failure,
5
- * and dry-run outcomes after a story has been executed through the pipeline.
4
+ * Handles pipeline success, failure outcomes after story execution.
5
+ * Dry-run handling: see execution/dry-run.ts
6
+ * applyCachedRouting: removed (P4-001 — pipeline routing stage is sole source)
6
7
  */
7
8
 
8
9
  import type { NaxConfig } from "../config";
9
- import { type LoadedHooksConfig, fireHook } from "../hooks";
10
+ import type { LoadedHooksConfig } from "../hooks";
10
11
  import type { InteractionChain } from "../interaction/chain";
11
- import { executeTrigger, isTriggerEnabled } from "../interaction/triggers";
12
12
  import { getSafeLogger } from "../logger";
13
13
  import type { StoryMetrics } from "../metrics";
14
+ import { pipelineEventBus } from "../pipeline/event-bus";
14
15
  import type { PipelineRunResult } from "../pipeline/runner";
15
16
  import type { PluginRegistry } from "../plugins";
16
17
  import { countStories, markStoryFailed, markStoryPaused, savePRD } from "../prd";
17
18
  import type { PRD, UserStory } from "../prd/types";
18
19
  import type { routeTask } from "../routing";
19
20
  import { handleTierEscalation } from "./escalation";
20
- import { hookCtx } from "./helpers";
21
- import { emitStoryComplete } from "./lifecycle/story-hooks";
22
- import { runPostAgentVerification } from "./post-verify";
23
21
  import { appendProgress } from "./progress";
24
- import type { StatusWriter } from "./status-writer";
25
22
 
26
- /** Context needed by pipeline result handlers */
27
23
  export interface PipelineHandlerContext {
28
- config: import("../config").NaxConfig;
24
+ config: NaxConfig;
29
25
  prd: PRD;
30
26
  prdPath: string;
31
27
  workdir: string;
@@ -41,7 +37,6 @@ export interface PipelineHandlerContext {
41
37
  routing: ReturnType<typeof routeTask>;
42
38
  isBatchExecution: boolean;
43
39
  allStoryMetrics: StoryMetrics[];
44
- timeoutRetryCountMap: Map<string, number>;
45
40
  storyGitRef: string | null | undefined;
46
41
  interactionChain?: InteractionChain | null;
47
42
  }
@@ -53,68 +48,40 @@ export interface PipelineSuccessResult {
53
48
  prdDirty: boolean;
54
49
  }
55
50
 
56
- /**
57
- * Handle a successful pipeline execution:
58
- * - Post-agent verification
59
- * - Emit story completion events
60
- * - Log progress
61
- */
62
51
  export async function handlePipelineSuccess(
63
52
  ctx: PipelineHandlerContext,
64
53
  pipelineResult: PipelineRunResult,
65
54
  ): Promise<PipelineSuccessResult> {
66
55
  const logger = getSafeLogger();
67
56
  const costDelta = pipelineResult.context.agentResult?.estimatedCost || 0;
68
- let prd = ctx.prd;
57
+ const prd = ctx.prd;
69
58
 
70
- // Collect story metrics
71
59
  if (pipelineResult.context.storyMetrics) {
72
60
  ctx.allStoryMetrics.push(...pipelineResult.context.storyMetrics);
73
61
  }
74
62
 
75
- // Post-agent verification
76
- const verifyResult = await runPostAgentVerification({
77
- config: ctx.config,
78
- prd,
79
- prdPath: ctx.prdPath,
80
- workdir: ctx.workdir,
81
- featureDir: ctx.featureDir,
82
- story: ctx.story,
83
- storiesToExecute: ctx.storiesToExecute,
84
- allStoryMetrics: ctx.allStoryMetrics,
85
- timeoutRetryCountMap: ctx.timeoutRetryCountMap,
86
- });
87
- const verificationPassed = verifyResult.passed;
88
- prd = verifyResult.prd;
89
-
90
- let storiesCompletedDelta = 0;
91
- if (verificationPassed) {
92
- storiesCompletedDelta = ctx.storiesToExecute.length;
93
-
94
- const reporters = ctx.pluginRegistry.getReporters();
95
- for (const completedStory of ctx.storiesToExecute) {
96
- logger?.info("story.complete", "Story completed successfully", {
97
- storyId: completedStory.id,
98
- storyTitle: completedStory.title,
99
- totalCost: ctx.totalCost + costDelta,
100
- durationMs: Date.now() - ctx.startTime,
101
- });
63
+ const storiesCompletedDelta = ctx.storiesToExecute.length;
64
+ for (const completedStory of ctx.storiesToExecute) {
65
+ logger?.info("story.complete", "Story completed successfully", {
66
+ storyId: completedStory.id,
67
+ storyTitle: completedStory.title,
68
+ totalCost: ctx.totalCost + costDelta,
69
+ durationMs: Date.now() - ctx.startTime,
70
+ });
102
71
 
103
- await emitStoryComplete(reporters, {
104
- runId: ctx.runId,
105
- storyId: completedStory.id,
106
- status: "completed",
107
- durationMs: Date.now() - ctx.startTime,
108
- cost: costDelta,
109
- tier: ctx.routing.modelTier,
110
- testStrategy: ctx.routing.testStrategy,
111
- });
112
- }
72
+ pipelineEventBus.emit({
73
+ type: "story:completed",
74
+ storyId: completedStory.id,
75
+ story: completedStory,
76
+ passed: true,
77
+ durationMs: Date.now() - ctx.startTime,
78
+ cost: costDelta,
79
+ modelTier: ctx.routing.modelTier,
80
+ testStrategy: ctx.routing.testStrategy,
81
+ });
113
82
  }
114
83
 
115
- // Display progress
116
84
  const updatedCounts = countStories(prd);
117
- const elapsedMs = Date.now() - ctx.startTime;
118
85
  logger?.info("progress", "Progress update", {
119
86
  totalStories: updatedCounts.total,
120
87
  passedStories: updatedCounts.passed,
@@ -122,7 +89,7 @@ export async function handlePipelineSuccess(
122
89
  pendingStories: updatedCounts.pending,
123
90
  totalCost: ctx.totalCost + costDelta,
124
91
  costLimit: ctx.config.execution.costLimit,
125
- elapsedMs,
92
+ elapsedMs: Date.now() - ctx.startTime,
126
93
  });
127
94
 
128
95
  return { storiesCompletedDelta, costDelta, prd, prdDirty: true };
@@ -133,16 +100,11 @@ export interface PipelineFailureResult {
133
100
  prdDirty: boolean;
134
101
  }
135
102
 
136
- /**
137
- * Handle a failed pipeline execution based on finalAction:
138
- * pause, skip, fail, or escalate.
139
- */
140
103
  export async function handlePipelineFailure(
141
104
  ctx: PipelineHandlerContext,
142
105
  pipelineResult: PipelineRunResult,
143
106
  ): Promise<PipelineFailureResult> {
144
107
  const logger = getSafeLogger();
145
- const reporters = ctx.pluginRegistry.getReporters();
146
108
  let prd = ctx.prd;
147
109
  let prdDirty = false;
148
110
 
@@ -151,114 +113,49 @@ export async function handlePipelineFailure(
151
113
  markStoryPaused(prd, ctx.story.id);
152
114
  await savePRD(prd, ctx.prdPath);
153
115
  prdDirty = true;
154
-
155
- logger?.warn("pipeline", "Story paused", {
116
+ logger?.warn("pipeline", "Story paused", { storyId: ctx.story.id, reason: pipelineResult.reason });
117
+ pipelineEventBus.emit({
118
+ type: "story:paused",
156
119
  storyId: ctx.story.id,
157
- reason: pipelineResult.reason,
158
- });
159
-
160
- await fireHook(
161
- ctx.hooks,
162
- "on-pause",
163
- hookCtx(ctx.feature, {
164
- storyId: ctx.story.id,
165
- reason: pipelineResult.reason || "Pipeline paused",
166
- cost: ctx.totalCost,
167
- }),
168
- ctx.workdir,
169
- );
170
-
171
- await emitStoryComplete(reporters, {
172
- runId: ctx.runId,
173
- storyId: ctx.story.id,
174
- status: "paused",
175
- durationMs: Date.now() - ctx.startTime,
176
- cost: pipelineResult.context.agentResult?.estimatedCost || 0,
177
- tier: ctx.routing.modelTier,
178
- testStrategy: ctx.routing.testStrategy,
120
+ reason: pipelineResult.reason || "Pipeline paused",
121
+ cost: ctx.totalCost,
179
122
  });
180
123
  break;
181
124
 
182
125
  case "skip":
183
- logger?.warn("pipeline", "Story skipped", {
184
- storyId: ctx.story.id,
185
- reason: pipelineResult.reason,
186
- });
126
+ logger?.warn("pipeline", "Story skipped", { storyId: ctx.story.id, reason: pipelineResult.reason });
187
127
  prdDirty = true;
188
-
189
- await emitStoryComplete(reporters, {
190
- runId: ctx.runId,
191
- storyId: ctx.story.id,
192
- status: "skipped",
193
- durationMs: Date.now() - ctx.startTime,
194
- cost: 0,
195
- tier: ctx.routing.modelTier,
196
- testStrategy: ctx.routing.testStrategy,
197
- });
198
128
  break;
199
129
 
200
130
  case "fail":
201
131
  markStoryFailed(prd, ctx.story.id, pipelineResult.context.tddFailureCategory);
202
132
  await savePRD(prd, ctx.prdPath);
203
133
  prdDirty = true;
204
-
205
- logger?.error("pipeline", "Story failed", {
206
- storyId: ctx.story.id,
207
- reason: pipelineResult.reason,
208
- });
134
+ logger?.error("pipeline", "Story failed", { storyId: ctx.story.id, reason: pipelineResult.reason });
209
135
 
210
136
  if (ctx.featureDir) {
211
137
  await appendProgress(ctx.featureDir, ctx.story.id, "failed", `${ctx.story.title} — ${pipelineResult.reason}`);
212
138
  }
213
139
 
214
- // Fire human-review trigger when story has exceeded max retries and interaction chain is available
215
- if (
216
- ctx.interactionChain &&
217
- ctx.story.attempts !== undefined &&
218
- ctx.story.attempts >= ctx.config.execution.rectification.maxRetries &&
219
- isTriggerEnabled("human-review", ctx.config)
220
- ) {
221
- try {
222
- await executeTrigger(
223
- "human-review",
224
- {
225
- featureName: ctx.feature,
226
- storyId: ctx.story.id,
227
- iteration: ctx.story.attempts,
228
- reason: pipelineResult.reason || "Max retries exceeded",
229
- },
230
- ctx.config,
231
- ctx.interactionChain,
232
- );
233
- } catch (err) {
234
- logger?.warn("pipeline", "human-review trigger failed", {
235
- storyId: ctx.story.id,
236
- error: String(err),
237
- });
238
- }
239
- }
240
-
241
- await fireHook(
242
- ctx.hooks,
243
- "on-story-fail",
244
- hookCtx(ctx.feature, {
245
- storyId: ctx.story.id,
246
- status: "failed",
247
- reason: pipelineResult.reason || "Pipeline failed",
248
- cost: ctx.totalCost,
249
- }),
250
- ctx.workdir,
251
- );
252
-
253
- await emitStoryComplete(reporters, {
254
- runId: ctx.runId,
140
+ pipelineEventBus.emit({
141
+ type: "story:failed",
255
142
  storyId: ctx.story.id,
256
- status: "failed",
257
- durationMs: Date.now() - ctx.startTime,
258
- cost: pipelineResult.context.agentResult?.estimatedCost || 0,
259
- tier: ctx.routing.modelTier,
260
- testStrategy: ctx.routing.testStrategy,
143
+ story: ctx.story,
144
+ reason: pipelineResult.reason || "Pipeline failed",
145
+ countsTowardEscalation: true,
146
+ feature: ctx.feature,
147
+ attempts: ctx.story.attempts,
261
148
  });
149
+
150
+ if (ctx.story.attempts !== undefined && ctx.story.attempts >= ctx.config.execution.rectification.maxRetries) {
151
+ pipelineEventBus.emit({
152
+ type: "human-review:requested",
153
+ storyId: ctx.story.id,
154
+ reason: pipelineResult.reason || "Max retries exceeded",
155
+ feature: ctx.feature,
156
+ attempts: ctx.story.attempts,
157
+ });
158
+ }
262
159
  break;
263
160
 
264
161
  case "escalate": {
@@ -277,7 +174,6 @@ export async function handlePipelineFailure(
277
174
  totalCost: ctx.totalCost,
278
175
  workdir: ctx.workdir,
279
176
  });
280
-
281
177
  prd = escalationResult.prd;
282
178
  prdDirty = escalationResult.prdDirty;
283
179
  break;
@@ -286,102 +182,3 @@ export async function handlePipelineFailure(
286
182
 
287
183
  return { prd, prdDirty };
288
184
  }
289
-
290
- /**
291
- * Apply cached routing overrides from story.routing to a fresh routing decision.
292
- */
293
- export function applyCachedRouting(
294
- routing: ReturnType<typeof routeTask>,
295
- story: UserStory,
296
- config: NaxConfig,
297
- ): ReturnType<typeof routeTask> {
298
- if (!story.routing) return routing;
299
- const overrides: Partial<ReturnType<typeof routeTask>> = {};
300
- if (story.routing.complexity) {
301
- overrides.complexity = story.routing.complexity;
302
- }
303
- // BUG-013 fix: Use story.routing.modelTier directly if present (set by escalation).
304
- // Only derive from complexity if modelTier is not explicitly set.
305
- if (story.routing.modelTier) {
306
- overrides.modelTier = story.routing.modelTier as ReturnType<typeof routeTask>["modelTier"];
307
- } else if (story.routing.complexity) {
308
- const tierFromComplexity = config.autoMode.complexityRouting[story.routing.complexity] ?? "balanced";
309
- overrides.modelTier = tierFromComplexity as ReturnType<typeof routeTask>["modelTier"];
310
- }
311
- if (story.routing.testStrategy) {
312
- overrides.testStrategy = story.routing.testStrategy;
313
- }
314
- return { ...routing, ...overrides };
315
- }
316
-
317
- /** Context for dry-run iteration handling */
318
- export interface DryRunContext {
319
- prd: PRD;
320
- prdPath: string;
321
- storiesToExecute: UserStory[];
322
- routing: ReturnType<typeof routeTask>;
323
- statusWriter: StatusWriter;
324
- pluginRegistry: PluginRegistry;
325
- runId: string;
326
- totalCost: number;
327
- iterations: number;
328
- }
329
-
330
- export interface DryRunResult {
331
- storiesCompletedDelta: number;
332
- prdDirty: boolean;
333
- }
334
-
335
- /** Handle dry-run iteration: log what would happen, mark stories passed. */
336
- export async function handleDryRun(ctx: DryRunContext): Promise<DryRunResult> {
337
- const logger = getSafeLogger();
338
-
339
- ctx.statusWriter.setPrd(ctx.prd);
340
- ctx.statusWriter.setCurrentStory({
341
- storyId: ctx.storiesToExecute[0].id,
342
- title: ctx.storiesToExecute[0].title,
343
- complexity: ctx.routing.complexity,
344
- tddStrategy: ctx.routing.testStrategy,
345
- model: ctx.routing.modelTier,
346
- attempt: (ctx.storiesToExecute[0].attempts ?? 0) + 1,
347
- phase: "routing",
348
- });
349
- await ctx.statusWriter.update(ctx.totalCost, ctx.iterations);
350
-
351
- for (const s of ctx.storiesToExecute) {
352
- logger?.info("execution", "[DRY RUN] Would execute agent here", {
353
- storyId: s.id,
354
- storyTitle: s.title,
355
- modelTier: ctx.routing.modelTier,
356
- complexity: ctx.routing.complexity,
357
- testStrategy: ctx.routing.testStrategy,
358
- });
359
- }
360
-
361
- // Mark stories as passed so the loop progresses
362
- for (const s of ctx.storiesToExecute) {
363
- const { markStoryPassed } = await import("../prd");
364
- markStoryPassed(ctx.prd, s.id);
365
- }
366
- await savePRD(ctx.prd, ctx.prdPath);
367
-
368
- // Emit onStoryComplete events for dry-run
369
- const reporters = ctx.pluginRegistry.getReporters();
370
- for (const s of ctx.storiesToExecute) {
371
- await emitStoryComplete(reporters, {
372
- runId: ctx.runId,
373
- storyId: s.id,
374
- status: "completed",
375
- durationMs: 0,
376
- cost: 0,
377
- tier: ctx.routing.modelTier,
378
- testStrategy: ctx.routing.testStrategy,
379
- });
380
- }
381
-
382
- ctx.statusWriter.setPrd(ctx.prd);
383
- ctx.statusWriter.setCurrentStory(null);
384
- await ctx.statusWriter.update(ctx.totalCost, ctx.iterations);
385
-
386
- return { storiesCompletedDelta: ctx.storiesToExecute.length, prdDirty: true };
387
- }