@nathapp/nax 0.38.0 → 0.38.2

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 (75) hide show
  1. package/dist/nax.js +3294 -2907
  2. package/package.json +2 -2
  3. package/src/agents/claude-complete.ts +72 -0
  4. package/src/agents/claude-execution.ts +189 -0
  5. package/src/agents/claude-interactive.ts +77 -0
  6. package/src/agents/claude-plan.ts +23 -8
  7. package/src/agents/claude.ts +64 -349
  8. package/src/analyze/classifier.ts +2 -1
  9. package/src/cli/config-descriptions.ts +206 -0
  10. package/src/cli/config-diff.ts +103 -0
  11. package/src/cli/config-display.ts +285 -0
  12. package/src/cli/config-get.ts +55 -0
  13. package/src/cli/config.ts +7 -618
  14. package/src/cli/plugins.ts +15 -4
  15. package/src/cli/prompts-export.ts +58 -0
  16. package/src/cli/prompts-init.ts +200 -0
  17. package/src/cli/prompts-main.ts +237 -0
  18. package/src/cli/prompts-tdd.ts +78 -0
  19. package/src/cli/prompts.ts +10 -541
  20. package/src/commands/logs-formatter.ts +201 -0
  21. package/src/commands/logs-reader.ts +171 -0
  22. package/src/commands/logs.ts +11 -362
  23. package/src/config/loader.ts +4 -15
  24. package/src/config/runtime-types.ts +451 -0
  25. package/src/config/schema-types.ts +53 -0
  26. package/src/config/schemas.ts +2 -0
  27. package/src/config/types.ts +49 -486
  28. package/src/context/auto-detect.ts +2 -1
  29. package/src/context/builder.ts +3 -2
  30. package/src/execution/crash-heartbeat.ts +77 -0
  31. package/src/execution/crash-recovery.ts +23 -365
  32. package/src/execution/crash-signals.ts +149 -0
  33. package/src/execution/crash-writer.ts +154 -0
  34. package/src/execution/lifecycle/run-setup.ts +7 -1
  35. package/src/execution/parallel-coordinator.ts +278 -0
  36. package/src/execution/parallel-executor-rectification-pass.ts +117 -0
  37. package/src/execution/parallel-executor-rectify.ts +135 -0
  38. package/src/execution/parallel-executor.ts +19 -211
  39. package/src/execution/parallel-worker.ts +148 -0
  40. package/src/execution/parallel.ts +5 -404
  41. package/src/execution/pid-registry.ts +3 -8
  42. package/src/execution/runner-completion.ts +160 -0
  43. package/src/execution/runner-execution.ts +221 -0
  44. package/src/execution/runner-setup.ts +82 -0
  45. package/src/execution/runner.ts +53 -202
  46. package/src/execution/timeout-handler.ts +100 -0
  47. package/src/hooks/runner.ts +11 -21
  48. package/src/metrics/tracker.ts +7 -30
  49. package/src/pipeline/runner.ts +2 -1
  50. package/src/pipeline/stages/completion.ts +0 -1
  51. package/src/pipeline/stages/context.ts +2 -1
  52. package/src/plugins/extensions.ts +225 -0
  53. package/src/plugins/loader.ts +40 -4
  54. package/src/plugins/types.ts +18 -221
  55. package/src/prd/index.ts +2 -1
  56. package/src/prd/validate.ts +41 -0
  57. package/src/precheck/checks-blockers.ts +15 -419
  58. package/src/precheck/checks-cli.ts +68 -0
  59. package/src/precheck/checks-config.ts +102 -0
  60. package/src/precheck/checks-git.ts +87 -0
  61. package/src/precheck/checks-system.ts +163 -0
  62. package/src/review/orchestrator.ts +19 -6
  63. package/src/review/runner.ts +17 -5
  64. package/src/routing/chain.ts +2 -1
  65. package/src/routing/loader.ts +2 -5
  66. package/src/tdd/orchestrator.ts +2 -1
  67. package/src/tdd/verdict-reader.ts +266 -0
  68. package/src/tdd/verdict.ts +6 -271
  69. package/src/utils/errors.ts +12 -0
  70. package/src/utils/git.ts +12 -5
  71. package/src/utils/json-file.ts +72 -0
  72. package/src/verification/executor.ts +2 -1
  73. package/src/verification/smart-runner.ts +23 -3
  74. package/src/worktree/manager.ts +9 -3
  75. package/src/worktree/merge.ts +3 -2
@@ -19,139 +19,14 @@ import type { StoryMetrics } from "../metrics";
19
19
  import type { PipelineEventEmitter } from "../pipeline/events";
20
20
  import type { PluginRegistry } from "../plugins/registry";
21
21
  import type { PRD } from "../prd";
22
- import { countStories, isComplete, markStoryPassed } from "../prd";
22
+ import { countStories, isComplete } from "../prd";
23
+ import { errorMessage } from "../utils/errors";
23
24
  import { getAllReadyStories, hookCtx } from "./helpers";
24
25
  import { executeParallel } from "./parallel";
26
+ import { type ParallelStoryMetrics, runRectificationPass } from "./parallel-executor-rectification-pass";
27
+ import { type ConflictedStoryInfo, rectifyConflictedStory } from "./parallel-executor-rectify";
25
28
  import type { StatusWriter } from "./status-writer";
26
29
 
27
- /** StoryMetrics extended with execution-path source */
28
- export type ParallelStoryMetrics = StoryMetrics & {
29
- source: "parallel" | "sequential" | "rectification";
30
- rectifiedFromConflict?: boolean;
31
- originalCost?: number;
32
- rectificationCost?: number;
33
- };
34
-
35
- /** A story that conflicted during the initial parallel merge pass */
36
- export interface ConflictedStoryInfo {
37
- storyId: string;
38
- conflictFiles: string[];
39
- originalCost: number;
40
- }
41
-
42
- /** Result from attempting to rectify a single conflicted story */
43
- export type RectificationResult =
44
- | { success: true; storyId: string; cost: number }
45
- | {
46
- success: false;
47
- storyId: string;
48
- cost: number;
49
- finalConflict: boolean;
50
- pipelineFailure?: boolean;
51
- conflictFiles?: string[];
52
- };
53
-
54
- /** Options passed to rectifyConflictedStory */
55
- export interface RectifyConflictedStoryOptions extends ConflictedStoryInfo {
56
- workdir: string;
57
- config: NaxConfig;
58
- hooks: LoadedHooksConfig;
59
- pluginRegistry: PluginRegistry;
60
- prd: PRD;
61
- eventEmitter?: PipelineEventEmitter;
62
- }
63
-
64
- /**
65
- * Actual implementation of rectifyConflictedStory.
66
- *
67
- * Steps:
68
- * 1. Remove the old worktree
69
- * 2. Create a fresh worktree from current HEAD (post-merge state)
70
- * 3. Re-run the full story pipeline
71
- * 4. Attempt merge on the updated base
72
- * 5. Return success/finalConflict
73
- */
74
- async function rectifyConflictedStory(options: RectifyConflictedStoryOptions): Promise<RectificationResult> {
75
- const { storyId, workdir, config, hooks, pluginRegistry, prd, eventEmitter } = options;
76
- const logger = getSafeLogger();
77
-
78
- logger?.info("parallel", "Rectifying story on updated base", { storyId, attempt: "rectification" });
79
-
80
- try {
81
- const { WorktreeManager } = await import("../worktree/manager");
82
- const { MergeEngine } = await import("../worktree/merge");
83
- const { runPipeline } = await import("../pipeline/runner");
84
- const { defaultPipeline } = await import("../pipeline/stages");
85
- const { routeTask } = await import("../routing");
86
-
87
- const worktreeManager = new WorktreeManager();
88
- const mergeEngine = new MergeEngine(worktreeManager);
89
-
90
- // Step 1: Remove old worktree
91
- try {
92
- await worktreeManager.remove(workdir, storyId);
93
- } catch {
94
- // Ignore — worktree may have already been removed
95
- }
96
-
97
- // Step 2: Create fresh worktree from current HEAD
98
- await worktreeManager.create(workdir, storyId);
99
- const worktreePath = path.join(workdir, ".nax-wt", storyId);
100
-
101
- // Step 3: Re-run the story pipeline
102
- const story = prd.userStories.find((s) => s.id === storyId);
103
- if (!story) {
104
- return { success: false, storyId, cost: 0, finalConflict: false, pipelineFailure: true };
105
- }
106
-
107
- const routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, config);
108
-
109
- const pipelineContext = {
110
- config,
111
- prd,
112
- story,
113
- stories: [story],
114
- workdir: worktreePath,
115
- featureDir: undefined,
116
- hooks,
117
- plugins: pluginRegistry,
118
- storyStartTime: new Date().toISOString(),
119
- routing: routing as import("../pipeline/types").RoutingResult,
120
- };
121
-
122
- const pipelineResult = await runPipeline(defaultPipeline, pipelineContext, eventEmitter);
123
- const cost = pipelineResult.context.agentResult?.estimatedCost ?? 0;
124
-
125
- if (!pipelineResult.success) {
126
- logger?.info("parallel", "Rectification failed - preserving worktree", { storyId });
127
- return { success: false, storyId, cost, finalConflict: false, pipelineFailure: true };
128
- }
129
-
130
- // Step 4: Attempt merge on updated base
131
- const mergeResults = await mergeEngine.mergeAll(workdir, [storyId], { [storyId]: [] });
132
- const mergeResult = mergeResults[0];
133
-
134
- if (!mergeResult || !mergeResult.success) {
135
- const conflictFiles = mergeResult?.conflictFiles ?? [];
136
- logger?.info("parallel", "Rectification failed - preserving worktree", { storyId });
137
- return { success: false, storyId, cost, finalConflict: true, conflictFiles };
138
- }
139
-
140
- logger?.info("parallel", "Rectification succeeded - story merged", {
141
- storyId,
142
- originalCost: options.originalCost,
143
- rectificationCost: cost,
144
- });
145
- return { success: true, storyId, cost };
146
- } catch (error) {
147
- logger?.error("parallel", "Rectification failed - preserving worktree", {
148
- storyId,
149
- error: error instanceof Error ? error.message : String(error),
150
- });
151
- return { success: false, storyId, cost: 0, finalConflict: false, pipelineFailure: true };
152
- }
153
- }
154
-
155
30
  /**
156
31
  * Injectable dependencies for testing (avoids mock.module() which leaks in Bun 1.x).
157
32
  * @internal - test use only.
@@ -201,86 +76,6 @@ export interface ParallelExecutorResult {
201
76
  rectificationStats: RectificationStats;
202
77
  }
203
78
 
204
- /**
205
- * Run the rectification pass: sequentially re-run each conflicted story on
206
- * the updated base (which already includes all clean merges from the first pass).
207
- */
208
- async function runRectificationPass(
209
- conflictedStories: ConflictedStoryInfo[],
210
- options: ParallelExecutorOptions,
211
- prd: PRD,
212
- ): Promise<{
213
- rectifiedCount: number;
214
- stillConflictingCount: number;
215
- additionalCost: number;
216
- updatedPrd: PRD;
217
- rectificationMetrics: ParallelStoryMetrics[];
218
- }> {
219
- const logger = getSafeLogger();
220
- const { workdir, config, hooks, pluginRegistry, eventEmitter } = options;
221
- const rectificationMetrics: ParallelStoryMetrics[] = [];
222
- let rectifiedCount = 0;
223
- let stillConflictingCount = 0;
224
- let additionalCost = 0;
225
-
226
- logger?.info("parallel", "Starting merge conflict rectification", {
227
- stories: conflictedStories.map((s) => s.storyId),
228
- totalConflicts: conflictedStories.length,
229
- });
230
-
231
- // Sequential — each story sees all previously rectified stories in the base
232
- for (const conflictInfo of conflictedStories) {
233
- const result = await _parallelExecutorDeps.rectifyConflictedStory({
234
- ...conflictInfo,
235
- workdir,
236
- config,
237
- hooks,
238
- pluginRegistry,
239
- prd,
240
- eventEmitter,
241
- });
242
-
243
- additionalCost += result.cost;
244
-
245
- if (result.success) {
246
- markStoryPassed(prd, result.storyId);
247
- rectifiedCount++;
248
-
249
- rectificationMetrics.push({
250
- storyId: result.storyId,
251
- complexity: "unknown",
252
- modelTier: "parallel",
253
- modelUsed: "parallel",
254
- attempts: 1,
255
- finalTier: "parallel",
256
- success: true,
257
- cost: result.cost,
258
- durationMs: 0,
259
- firstPassSuccess: false,
260
- startedAt: new Date().toISOString(),
261
- completedAt: new Date().toISOString(),
262
- source: "rectification" as const,
263
- rectifiedFromConflict: true,
264
- originalCost: conflictInfo.originalCost,
265
- rectificationCost: result.cost,
266
- });
267
- } else {
268
- const isFinalConflict = result.finalConflict === true;
269
- if (isFinalConflict) {
270
- stillConflictingCount++;
271
- }
272
- // pipelineFailure — not counted as structural conflict, story remains failed
273
- }
274
- }
275
-
276
- logger?.info("parallel", "Rectification complete", {
277
- rectified: rectifiedCount,
278
- stillConflicting: stillConflictingCount,
279
- });
280
-
281
- return { rectifiedCount, stillConflictingCount, additionalCost, updatedPrd: prd, rectificationMetrics };
282
- }
283
-
284
79
  /**
285
80
  * Execute parallel stories if --parallel is set
286
81
  */
@@ -415,7 +210,7 @@ export async function runParallelExecution(
415
210
  });
416
211
  } catch (error) {
417
212
  logger?.error("parallel", "Parallel execution failed", {
418
- error: error instanceof Error ? error.message : String(error),
213
+ error: errorMessage(error),
419
214
  });
420
215
 
421
216
  // Clear parallel status on error
@@ -430,7 +225,12 @@ export async function runParallelExecution(
430
225
  let rectificationStats: RectificationStats = { rectified: 0, stillConflicting: 0 };
431
226
 
432
227
  if (conflictedStories.length > 0) {
433
- const rectResult = await runRectificationPass(conflictedStories, options, prd);
228
+ const rectResult = await runRectificationPass(
229
+ conflictedStories,
230
+ options,
231
+ prd,
232
+ _parallelExecutorDeps.rectifyConflictedStory,
233
+ );
434
234
  prd = rectResult.updatedPrd;
435
235
  storiesCompleted += rectResult.rectifiedCount;
436
236
  totalCost += rectResult.additionalCost;
@@ -517,3 +317,11 @@ export async function runParallelExecution(
517
317
 
518
318
  return { prd, totalCost, storiesCompleted, completed: false, storyMetrics: batchStoryMetrics, rectificationStats };
519
319
  }
320
+
321
+ // Re-export types for backward compatibility
322
+ export type {
323
+ ConflictedStoryInfo,
324
+ RectificationResult,
325
+ RectifyConflictedStoryOptions,
326
+ } from "./parallel-executor-rectify";
327
+ export type { ParallelStoryMetrics } from "./parallel-executor-rectification-pass";
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Parallel worker — Story execution in worktrees
3
+ */
4
+
5
+ import { join } from "node:path";
6
+ import type { NaxConfig } from "../config";
7
+ import { getSafeLogger } from "../logger";
8
+ import type { PipelineEventEmitter } from "../pipeline/events";
9
+ import { runPipeline } from "../pipeline/runner";
10
+ import { defaultPipeline } from "../pipeline/stages";
11
+ import type { PipelineContext, RoutingResult } from "../pipeline/types";
12
+ import type { UserStory } from "../prd";
13
+ import { routeTask } from "../routing";
14
+ import { errorMessage } from "../utils/errors";
15
+
16
+ /**
17
+ * Execute a single story in its worktree
18
+ */
19
+ export async function executeStoryInWorktree(
20
+ story: UserStory,
21
+ worktreePath: string,
22
+ context: Omit<PipelineContext, "story" | "stories" | "workdir" | "routing">,
23
+ routing: RoutingResult,
24
+ eventEmitter?: PipelineEventEmitter,
25
+ ): Promise<{ success: boolean; cost: number; error?: string }> {
26
+ const logger = getSafeLogger();
27
+
28
+ try {
29
+ const pipelineContext: PipelineContext = {
30
+ ...context,
31
+ story,
32
+ stories: [story],
33
+ workdir: worktreePath,
34
+ routing,
35
+ };
36
+
37
+ logger?.debug("parallel", "Executing story in worktree", {
38
+ storyId: story.id,
39
+ worktreePath,
40
+ });
41
+
42
+ const result = await runPipeline(defaultPipeline, pipelineContext, eventEmitter);
43
+
44
+ return {
45
+ success: result.success,
46
+ cost: result.context.agentResult?.estimatedCost || 0,
47
+ error: result.success ? undefined : result.reason,
48
+ };
49
+ } catch (error) {
50
+ return {
51
+ success: false,
52
+ cost: 0,
53
+ error: errorMessage(error),
54
+ };
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Batch execution result
60
+ */
61
+ export interface ParallelBatchResult {
62
+ /** Stories that passed the TDD pipeline (pre-merge) */
63
+ pipelinePassed: UserStory[];
64
+ /** Stories that were actually merged to the base branch */
65
+ merged: UserStory[];
66
+ /** Stories that failed the pipeline */
67
+ failed: Array<{ story: UserStory; error: string }>;
68
+ /** Total cost accumulated */
69
+ totalCost: number;
70
+ /** Stories with merge conflicts (includes per-story original cost for rectification) */
71
+ mergeConflicts: Array<{ storyId: string; conflictFiles: string[]; originalCost: number }>;
72
+ /** Per-story execution costs for successful stories */
73
+ storyCosts: Map<string, number>;
74
+ }
75
+
76
+ /**
77
+ * Execute a batch of independent stories in parallel (worktree setup must be done separately)
78
+ */
79
+ export async function executeParallelBatch(
80
+ stories: UserStory[],
81
+ projectRoot: string,
82
+ config: NaxConfig,
83
+ context: Omit<PipelineContext, "story" | "stories" | "workdir" | "routing">,
84
+ worktreePaths: Map<string, string>,
85
+ maxConcurrency: number,
86
+ eventEmitter?: PipelineEventEmitter,
87
+ ): Promise<ParallelBatchResult> {
88
+ const logger = getSafeLogger();
89
+ const results: ParallelBatchResult = {
90
+ pipelinePassed: [],
91
+ merged: [],
92
+ failed: [],
93
+ totalCost: 0,
94
+ mergeConflicts: [],
95
+ storyCosts: new Map(),
96
+ };
97
+
98
+ // Execute stories in parallel with concurrency limit
99
+ const executing = new Set<Promise<void>>();
100
+
101
+ for (const story of stories) {
102
+ const worktreePath = worktreePaths.get(story.id);
103
+ if (!worktreePath) {
104
+ results.failed.push({
105
+ story,
106
+ error: "Worktree not created",
107
+ });
108
+ continue;
109
+ }
110
+
111
+ const routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, config);
112
+
113
+ const executePromise = executeStoryInWorktree(story, worktreePath, context, routing as RoutingResult, eventEmitter)
114
+ .then((result) => {
115
+ results.totalCost += result.cost;
116
+ results.storyCosts.set(story.id, result.cost);
117
+
118
+ if (result.success) {
119
+ results.pipelinePassed.push(story);
120
+ logger?.info("parallel", "Story execution succeeded", {
121
+ storyId: story.id,
122
+ cost: result.cost,
123
+ });
124
+ } else {
125
+ results.failed.push({ story, error: result.error || "Unknown error" });
126
+ logger?.error("parallel", "Story execution failed", {
127
+ storyId: story.id,
128
+ error: result.error,
129
+ });
130
+ }
131
+ })
132
+ .finally(() => {
133
+ executing.delete(executePromise);
134
+ });
135
+
136
+ executing.add(executePromise);
137
+
138
+ // Wait if we've hit the concurrency limit
139
+ if (executing.size >= maxConcurrency) {
140
+ await Promise.race(executing);
141
+ }
142
+ }
143
+
144
+ // Wait for all remaining executions
145
+ await Promise.all(executing);
146
+
147
+ return results;
148
+ }