@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 +89 -6
- package/package.json +1 -1
- package/src/config/defaults.ts +1 -0
- package/src/config/runtime-types.ts +2 -0
- package/src/config/schemas.ts +1 -0
- package/src/execution/deferred-review.ts +105 -0
- package/src/execution/executor-types.ts +2 -0
- package/src/execution/sequential-executor.ts +7 -0
- package/src/review/orchestrator.ts +5 -0
- package/src/review/types.ts +2 -0
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.
|
|
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("
|
|
20864
|
-
return "
|
|
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
package/src/config/defaults.ts
CHANGED
|
@@ -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 */
|
package/src/config/schemas.ts
CHANGED
|
@@ -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) {
|