@nathapp/nax 0.39.1 → 0.39.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.
package/dist/nax.js CHANGED
@@ -18281,7 +18281,8 @@ var init_schemas3 = __esm(() => {
18281
18281
  typecheck: exports_external.string().optional(),
18282
18282
  lint: exports_external.string().optional(),
18283
18283
  test: exports_external.string().optional()
18284
- })
18284
+ }),
18285
+ pluginMode: exports_external.enum(["per-story", "deferred"]).default("per-story")
18285
18286
  });
18286
18287
  PlanConfigSchema = exports_external.object({
18287
18288
  model: ModelTierSchema,
@@ -18529,7 +18530,8 @@ var init_defaults = __esm(() => {
18529
18530
  review: {
18530
18531
  enabled: true,
18531
18532
  checks: ["typecheck", "lint"],
18532
- commands: {}
18533
+ commands: {},
18534
+ pluginMode: "per-story"
18533
18535
  },
18534
18536
  plan: {
18535
18537
  model: "balanced",
@@ -20796,7 +20798,7 @@ var package_default;
20796
20798
  var init_package = __esm(() => {
20797
20799
  package_default = {
20798
20800
  name: "@nathapp/nax",
20799
- version: "0.39.1",
20801
+ version: "0.39.2",
20800
20802
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
20801
20803
  type: "module",
20802
20804
  bin: {
@@ -20860,8 +20862,8 @@ var init_version = __esm(() => {
20860
20862
  NAX_VERSION = package_default.version;
20861
20863
  NAX_COMMIT = (() => {
20862
20864
  try {
20863
- if (/^[0-9a-f]{6,10}$/.test("91b2a1c"))
20864
- return "91b2a1c";
20865
+ if (/^[0-9a-f]{6,10}$/.test("d6c0898"))
20866
+ return "d6c0898";
20865
20867
  } catch {}
20866
20868
  try {
20867
20869
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -22822,6 +22824,10 @@ class ReviewOrchestrator {
22822
22824
  if (!builtIn.success) {
22823
22825
  return { builtIn, success: false, failureReason: builtIn.failureReason, pluginFailed: false };
22824
22826
  }
22827
+ if (reviewConfig.pluginMode === "deferred") {
22828
+ logger?.debug("review", "Plugin reviewers deferred \u2014 skipping per-story execution");
22829
+ return { builtIn, success: true, pluginFailed: false };
22830
+ }
22825
22831
  if (plugins) {
22826
22832
  const reviewers = plugins.getReviewers();
22827
22833
  if (reviewers.length > 0) {
@@ -31464,6 +31470,78 @@ var init_reporters = __esm(() => {
31464
31470
  init_logger2();
31465
31471
  });
31466
31472
 
31473
+ // src/execution/deferred-review.ts
31474
+ var {spawn: spawn3 } = globalThis.Bun;
31475
+ async function captureRunStartRef(workdir) {
31476
+ try {
31477
+ const proc = _deferredReviewDeps.spawn({
31478
+ cmd: ["git", "rev-parse", "HEAD"],
31479
+ cwd: workdir,
31480
+ stdout: "pipe",
31481
+ stderr: "pipe"
31482
+ });
31483
+ const [, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
31484
+ return stdout.trim();
31485
+ } catch {
31486
+ return "";
31487
+ }
31488
+ }
31489
+ async function getChangedFilesForDeferred(workdir, baseRef) {
31490
+ try {
31491
+ const proc = _deferredReviewDeps.spawn({
31492
+ cmd: ["git", "diff", "--name-only", `${baseRef}...HEAD`],
31493
+ cwd: workdir,
31494
+ stdout: "pipe",
31495
+ stderr: "pipe"
31496
+ });
31497
+ const [, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
31498
+ return stdout.trim().split(`
31499
+ `).filter(Boolean);
31500
+ } catch {
31501
+ return [];
31502
+ }
31503
+ }
31504
+ async function runDeferredReview(workdir, reviewConfig, plugins, runStartRef) {
31505
+ if (!reviewConfig || reviewConfig.pluginMode !== "deferred") {
31506
+ return;
31507
+ }
31508
+ const reviewers = plugins.getReviewers();
31509
+ if (reviewers.length === 0) {
31510
+ return;
31511
+ }
31512
+ const changedFiles = await getChangedFilesForDeferred(workdir, runStartRef);
31513
+ const reviewerResults = [];
31514
+ let anyFailed = false;
31515
+ for (const reviewer of reviewers) {
31516
+ try {
31517
+ const result = await reviewer.check(workdir, changedFiles);
31518
+ reviewerResults.push({
31519
+ name: reviewer.name,
31520
+ passed: result.passed,
31521
+ output: result.output,
31522
+ exitCode: result.exitCode
31523
+ });
31524
+ if (!result.passed) {
31525
+ anyFailed = true;
31526
+ }
31527
+ } catch (error48) {
31528
+ const errorMsg = error48 instanceof Error ? error48.message : String(error48);
31529
+ reviewerResults.push({
31530
+ name: reviewer.name,
31531
+ passed: false,
31532
+ output: "",
31533
+ error: errorMsg
31534
+ });
31535
+ anyFailed = true;
31536
+ }
31537
+ }
31538
+ return { runStartRef, changedFiles, reviewerResults, anyFailed };
31539
+ }
31540
+ var _deferredReviewDeps;
31541
+ var init_deferred_review = __esm(() => {
31542
+ _deferredReviewDeps = { spawn: spawn3 };
31543
+ });
31544
+
31467
31545
  // src/execution/dry-run.ts
31468
31546
  async function handleDryRun(ctx) {
31469
31547
  const logger = getSafeLogger();
@@ -32050,6 +32128,8 @@ async function executeSequential(ctx, initialPrd) {
32050
32128
  ];
32051
32129
  const allStoryMetrics = [];
32052
32130
  let warningSent = false;
32131
+ let deferredReview;
32132
+ const runStartRef = await captureRunStartRef(ctx.workdir);
32053
32133
  pipelineEventBus.clear();
32054
32134
  wireHooks(pipelineEventBus, ctx.hooks, ctx.workdir, ctx.feature);
32055
32135
  wireReporters(pipelineEventBus, ctx.pluginRegistry, ctx.runId, ctx.startTime);
@@ -32062,7 +32142,8 @@ async function executeSequential(ctx, initialPrd) {
32062
32142
  storiesCompleted,
32063
32143
  totalCost,
32064
32144
  allStoryMetrics,
32065
- exitReason
32145
+ exitReason,
32146
+ deferredReview
32066
32147
  });
32067
32148
  startHeartbeat(ctx.statusWriter, () => totalCost, () => iterations, ctx.logFilePath);
32068
32149
  try {
@@ -32081,6 +32162,7 @@ async function executeSequential(ctx, initialPrd) {
32081
32162
  return buildResult2("pre-merge-aborted");
32082
32163
  }
32083
32164
  }
32165
+ deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef);
32084
32166
  return buildResult2("completed");
32085
32167
  }
32086
32168
  const selected = selectNextStories(prd, ctx.config, ctx.batchPlan, currentBatchIndex, lastStoryId, ctx.useBatch);
@@ -32164,6 +32246,7 @@ var init_sequential_executor = __esm(() => {
32164
32246
  init_reporters();
32165
32247
  init_prd();
32166
32248
  init_crash_recovery();
32249
+ init_deferred_review();
32167
32250
  init_iteration_runner();
32168
32251
  init_story_selector();
32169
32252
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.39.1",
3
+ "version": "0.39.2",
4
4
  "description": "AI Coding Agent Orchestrator \u2014 loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -116,6 +116,7 @@ export const DEFAULT_CONFIG: NaxConfig = {
116
116
  enabled: true,
117
117
  checks: ["typecheck", "lint"],
118
118
  commands: {},
119
+ pluginMode: "per-story",
119
120
  },
120
121
  plan: {
121
122
  model: "balanced",
@@ -216,6 +216,8 @@ export interface ReviewConfig {
216
216
  lint?: string;
217
217
  test?: string;
218
218
  };
219
+ /** Plugin reviewer mode: "per-story" (run after each story) or "deferred" (run once at end of run, default: "per-story") */
220
+ pluginMode?: "per-story" | "deferred";
219
221
  }
220
222
 
221
223
  /** Plan config */
@@ -170,6 +170,7 @@ const ReviewConfigSchema = z.object({
170
170
  lint: z.string().optional(),
171
171
  test: z.string().optional(),
172
172
  }),
173
+ pluginMode: z.enum(["per-story", "deferred"]).default("per-story"),
173
174
  });
174
175
 
175
176
  const PlanConfigSchema = z.object({
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Deferred Plugin Review (DR-003)
3
+ *
4
+ * Captures the run-start git ref and runs all plugin reviewers once after
5
+ * all stories complete, using the full diff from run-start to HEAD.
6
+ */
7
+
8
+ import { spawn } from "bun";
9
+ import type { PluginRegistry } from "../plugins";
10
+ import type { ReviewConfig } from "../review/types";
11
+
12
+ /** Injectable deps for testing */
13
+ export const _deferredReviewDeps = { spawn };
14
+
15
+ export interface DeferredReviewResult {
16
+ runStartRef: string;
17
+ changedFiles: string[];
18
+ reviewerResults: Array<{
19
+ name: string;
20
+ passed: boolean;
21
+ output: string;
22
+ exitCode?: number;
23
+ error?: string;
24
+ }>;
25
+ anyFailed: boolean;
26
+ }
27
+
28
+ /** Capture the current HEAD git ref. Returns "" on failure. */
29
+ export async function captureRunStartRef(workdir: string): Promise<string> {
30
+ try {
31
+ const proc = _deferredReviewDeps.spawn({
32
+ cmd: ["git", "rev-parse", "HEAD"],
33
+ cwd: workdir,
34
+ stdout: "pipe",
35
+ stderr: "pipe",
36
+ });
37
+ const [, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
38
+ return stdout.trim();
39
+ } catch {
40
+ return "";
41
+ }
42
+ }
43
+
44
+ async function getChangedFilesForDeferred(workdir: string, baseRef: string): Promise<string[]> {
45
+ try {
46
+ const proc = _deferredReviewDeps.spawn({
47
+ cmd: ["git", "diff", "--name-only", `${baseRef}...HEAD`],
48
+ cwd: workdir,
49
+ stdout: "pipe",
50
+ stderr: "pipe",
51
+ });
52
+ const [, stdout] = await Promise.all([proc.exited, new Response(proc.stdout).text()]);
53
+ return stdout.trim().split("\n").filter(Boolean);
54
+ } catch {
55
+ return [];
56
+ }
57
+ }
58
+
59
+ /** Run all plugin reviewers once with the full diff since runStartRef. */
60
+ export async function runDeferredReview(
61
+ workdir: string,
62
+ reviewConfig: ReviewConfig,
63
+ plugins: PluginRegistry,
64
+ runStartRef: string,
65
+ ): Promise<DeferredReviewResult | undefined> {
66
+ if (!reviewConfig || reviewConfig.pluginMode !== "deferred") {
67
+ return undefined;
68
+ }
69
+
70
+ const reviewers = plugins.getReviewers();
71
+ if (reviewers.length === 0) {
72
+ return undefined;
73
+ }
74
+
75
+ const changedFiles = await getChangedFilesForDeferred(workdir, runStartRef);
76
+
77
+ const reviewerResults: DeferredReviewResult["reviewerResults"] = [];
78
+ let anyFailed = false;
79
+
80
+ for (const reviewer of reviewers) {
81
+ try {
82
+ const result = await reviewer.check(workdir, changedFiles);
83
+ reviewerResults.push({
84
+ name: reviewer.name,
85
+ passed: result.passed,
86
+ output: result.output,
87
+ exitCode: result.exitCode,
88
+ });
89
+ if (!result.passed) {
90
+ anyFailed = true;
91
+ }
92
+ } catch (error) {
93
+ const errorMsg = error instanceof Error ? error.message : String(error);
94
+ reviewerResults.push({
95
+ name: reviewer.name,
96
+ passed: false,
97
+ output: "",
98
+ error: errorMsg,
99
+ });
100
+ anyFailed = true;
101
+ }
102
+ }
103
+
104
+ return { runStartRef, changedFiles, reviewerResults, anyFailed };
105
+ }
@@ -13,6 +13,7 @@ import type { RoutingResult } from "../pipeline/types";
13
13
  import type { PluginRegistry } from "../plugins";
14
14
  import type { PRD, UserStory } from "../prd/types";
15
15
  import type { StoryBatch } from "./batching";
16
+ import type { DeferredReviewResult } from "./deferred-review";
16
17
  import type { StatusWriter } from "./status-writer";
17
18
 
18
19
  export interface SequentialExecutionContext {
@@ -41,6 +42,7 @@ export interface SequentialExecutionResult {
41
42
  totalCost: number;
42
43
  allStoryMetrics: StoryMetrics[];
43
44
  exitReason: "completed" | "cost-limit" | "max-iterations" | "stalled" | "no-stories" | "pre-merge-aborted";
45
+ deferredReview?: DeferredReviewResult;
44
46
  }
45
47
 
46
48
  /**
@@ -15,6 +15,8 @@ import type { PipelineContext } from "../pipeline/types";
15
15
  import { generateHumanHaltSummary, isComplete, isStalled, loadPRD } from "../prd";
16
16
  import type { PRD } from "../prd/types";
17
17
  import { startHeartbeat } from "./crash-recovery";
18
+ import { captureRunStartRef, runDeferredReview } from "./deferred-review";
19
+ import type { DeferredReviewResult } from "./deferred-review";
18
20
  import type { SequentialExecutionContext, SequentialExecutionResult } from "./executor-types";
19
21
  import { runIteration } from "./iteration-runner";
20
22
  import { selectNextStories } from "./story-selector";
@@ -37,6 +39,9 @@ export async function executeSequential(
37
39
  ];
38
40
  const allStoryMetrics: StoryMetrics[] = [];
39
41
  let warningSent = false;
42
+ let deferredReview: DeferredReviewResult | undefined;
43
+
44
+ const runStartRef = await captureRunStartRef(ctx.workdir);
40
45
 
41
46
  pipelineEventBus.clear();
42
47
  wireHooks(pipelineEventBus, ctx.hooks, ctx.workdir, ctx.feature);
@@ -52,6 +57,7 @@ export async function executeSequential(
52
57
  totalCost,
53
58
  allStoryMetrics,
54
59
  exitReason,
60
+ deferredReview,
55
61
  });
56
62
 
57
63
  startHeartbeat(
@@ -82,6 +88,7 @@ export async function executeSequential(
82
88
  return buildResult("pre-merge-aborted");
83
89
  }
84
90
  }
91
+ deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef);
85
92
  return buildResult("completed");
86
93
  }
87
94
 
@@ -84,6 +84,11 @@ export class ReviewOrchestrator {
84
84
  return { builtIn, success: false, failureReason: builtIn.failureReason, pluginFailed: false };
85
85
  }
86
86
 
87
+ if (reviewConfig.pluginMode === "deferred") {
88
+ logger?.debug("review", "Plugin reviewers deferred — skipping per-story execution");
89
+ return { builtIn, success: true, pluginFailed: false };
90
+ }
91
+
87
92
  if (plugins) {
88
93
  const reviewers = plugins.getReviewers();
89
94
  if (reviewers.length > 0) {
@@ -65,4 +65,6 @@ export interface ReviewConfig {
65
65
  lint?: string;
66
66
  test?: string;
67
67
  };
68
+ /** When to run plugin reviewers: per-story (default) or deferred (skip per-story, run once at end) */
69
+ pluginMode?: "per-story" | "deferred";
68
70
  }