@nathapp/nax 0.39.0 → 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 +98 -27
- 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/escalation/tier-escalation.ts +2 -20
- package/src/execution/executor-types.ts +2 -0
- package/src/execution/sequential-executor.ts +7 -0
- package/src/pipeline/stages/review.ts +7 -1
- package/src/review/orchestrator.ts +7 -1
- package/src/review/types.ts +2 -0
- package/src/routing/batch-route.ts +4 -1
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",
|
|
@@ -20159,7 +20161,9 @@ async function tryLlmBatchRoute(config2, stories, label = "routing") {
|
|
|
20159
20161
|
const logger = getSafeLogger();
|
|
20160
20162
|
try {
|
|
20161
20163
|
logger?.debug("routing", `LLM batch routing: ${label}`, { storyCount: stories.length, mode });
|
|
20162
|
-
|
|
20164
|
+
const agentName = config2.execution?.agent ?? "claude";
|
|
20165
|
+
const adapter = getAgent(agentName);
|
|
20166
|
+
await routeBatch(stories, { config: config2, adapter });
|
|
20163
20167
|
logger?.debug("routing", "LLM batch routing complete", { label });
|
|
20164
20168
|
} catch (err) {
|
|
20165
20169
|
logger?.warn("routing", "LLM batch routing failed, falling back to individual routing", {
|
|
@@ -20169,6 +20173,7 @@ async function tryLlmBatchRoute(config2, stories, label = "routing") {
|
|
|
20169
20173
|
}
|
|
20170
20174
|
}
|
|
20171
20175
|
var init_batch_route = __esm(() => {
|
|
20176
|
+
init_registry();
|
|
20172
20177
|
init_logger2();
|
|
20173
20178
|
init_llm();
|
|
20174
20179
|
});
|
|
@@ -20793,7 +20798,7 @@ var package_default;
|
|
|
20793
20798
|
var init_package = __esm(() => {
|
|
20794
20799
|
package_default = {
|
|
20795
20800
|
name: "@nathapp/nax",
|
|
20796
|
-
version: "0.39.
|
|
20801
|
+
version: "0.39.2",
|
|
20797
20802
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
20798
20803
|
type: "module",
|
|
20799
20804
|
bin: {
|
|
@@ -20857,8 +20862,8 @@ var init_version = __esm(() => {
|
|
|
20857
20862
|
NAX_VERSION = package_default.version;
|
|
20858
20863
|
NAX_COMMIT = (() => {
|
|
20859
20864
|
try {
|
|
20860
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
20861
|
-
return "
|
|
20865
|
+
if (/^[0-9a-f]{6,10}$/.test("d6c0898"))
|
|
20866
|
+
return "d6c0898";
|
|
20862
20867
|
} catch {}
|
|
20863
20868
|
try {
|
|
20864
20869
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -22813,16 +22818,20 @@ async function getChangedFiles(workdir, baseRef) {
|
|
|
22813
22818
|
}
|
|
22814
22819
|
|
|
22815
22820
|
class ReviewOrchestrator {
|
|
22816
|
-
async review(reviewConfig, workdir, executionConfig, plugins) {
|
|
22821
|
+
async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef) {
|
|
22817
22822
|
const logger = getSafeLogger();
|
|
22818
22823
|
const builtIn = await runReview(reviewConfig, workdir, executionConfig);
|
|
22819
22824
|
if (!builtIn.success) {
|
|
22820
22825
|
return { builtIn, success: false, failureReason: builtIn.failureReason, pluginFailed: false };
|
|
22821
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
|
+
}
|
|
22822
22831
|
if (plugins) {
|
|
22823
22832
|
const reviewers = plugins.getReviewers();
|
|
22824
22833
|
if (reviewers.length > 0) {
|
|
22825
|
-
const baseRef = executionConfig?.storyGitRef;
|
|
22834
|
+
const baseRef = storyGitRef ?? executionConfig?.storyGitRef;
|
|
22826
22835
|
const changedFiles = await getChangedFiles(workdir, baseRef);
|
|
22827
22836
|
const pluginResults = [];
|
|
22828
22837
|
for (const reviewer of reviewers) {
|
|
@@ -22897,7 +22906,7 @@ var init_review = __esm(() => {
|
|
|
22897
22906
|
async execute(ctx) {
|
|
22898
22907
|
const logger = getLogger();
|
|
22899
22908
|
logger.info("review", "Running review phase", { storyId: ctx.story.id });
|
|
22900
|
-
const result = await reviewOrchestrator.review(ctx.config.review, ctx.workdir, ctx.config.execution, ctx.plugins);
|
|
22909
|
+
const result = await reviewOrchestrator.review(ctx.config.review, ctx.workdir, ctx.config.execution, ctx.plugins, ctx.storyGitRef);
|
|
22901
22910
|
ctx.reviewResult = result.builtIn;
|
|
22902
22911
|
if (!result.success) {
|
|
22903
22912
|
const allFindings = result.builtIn.pluginReviewers?.flatMap((pr) => pr.findings ?? []) ?? [];
|
|
@@ -31461,6 +31470,78 @@ var init_reporters = __esm(() => {
|
|
|
31461
31470
|
init_logger2();
|
|
31462
31471
|
});
|
|
31463
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
|
+
|
|
31464
31545
|
// src/execution/dry-run.ts
|
|
31465
31546
|
async function handleDryRun(ctx) {
|
|
31466
31547
|
const logger = getSafeLogger();
|
|
@@ -31644,22 +31725,6 @@ function resolveMaxAttemptsOutcome(failureCategory) {
|
|
|
31644
31725
|
return "fail";
|
|
31645
31726
|
}
|
|
31646
31727
|
}
|
|
31647
|
-
async function tryLlmBatchRoute2(config2, stories, label = "routing") {
|
|
31648
|
-
const mode = config2.routing.llm?.mode ?? "hybrid";
|
|
31649
|
-
if (config2.routing.strategy !== "llm" || mode === "per-story" || stories.length === 0)
|
|
31650
|
-
return;
|
|
31651
|
-
const logger = getSafeLogger();
|
|
31652
|
-
try {
|
|
31653
|
-
logger?.debug("routing", `LLM batch routing: ${label}`, { storyCount: stories.length, mode });
|
|
31654
|
-
await routeBatch(stories, { config: config2 });
|
|
31655
|
-
logger?.debug("routing", "LLM batch routing complete", { label });
|
|
31656
|
-
} catch (err) {
|
|
31657
|
-
logger?.warn("routing", "LLM batch routing failed, falling back to individual routing", {
|
|
31658
|
-
error: err.message,
|
|
31659
|
-
label
|
|
31660
|
-
});
|
|
31661
|
-
}
|
|
31662
|
-
}
|
|
31663
31728
|
function shouldRetrySameTier(verifyResult) {
|
|
31664
31729
|
return verifyResult?.status === "RUNTIME_CRASH";
|
|
31665
31730
|
}
|
|
@@ -31739,7 +31804,7 @@ async function handleTierEscalation(ctx) {
|
|
|
31739
31804
|
clearCacheForStory(story.id);
|
|
31740
31805
|
}
|
|
31741
31806
|
if (routingMode === "hybrid") {
|
|
31742
|
-
await
|
|
31807
|
+
await tryLlmBatchRoute(ctx.config, storiesToEscalate, "hybrid-re-route-pipeline");
|
|
31743
31808
|
}
|
|
31744
31809
|
return {
|
|
31745
31810
|
outcome: "escalated",
|
|
@@ -31752,6 +31817,7 @@ var init_tier_escalation = __esm(() => {
|
|
|
31752
31817
|
init_hooks();
|
|
31753
31818
|
init_logger2();
|
|
31754
31819
|
init_prd();
|
|
31820
|
+
init_batch_route();
|
|
31755
31821
|
init_llm();
|
|
31756
31822
|
init_escalation();
|
|
31757
31823
|
init_helpers();
|
|
@@ -32062,6 +32128,8 @@ async function executeSequential(ctx, initialPrd) {
|
|
|
32062
32128
|
];
|
|
32063
32129
|
const allStoryMetrics = [];
|
|
32064
32130
|
let warningSent = false;
|
|
32131
|
+
let deferredReview;
|
|
32132
|
+
const runStartRef = await captureRunStartRef(ctx.workdir);
|
|
32065
32133
|
pipelineEventBus.clear();
|
|
32066
32134
|
wireHooks(pipelineEventBus, ctx.hooks, ctx.workdir, ctx.feature);
|
|
32067
32135
|
wireReporters(pipelineEventBus, ctx.pluginRegistry, ctx.runId, ctx.startTime);
|
|
@@ -32074,7 +32142,8 @@ async function executeSequential(ctx, initialPrd) {
|
|
|
32074
32142
|
storiesCompleted,
|
|
32075
32143
|
totalCost,
|
|
32076
32144
|
allStoryMetrics,
|
|
32077
|
-
exitReason
|
|
32145
|
+
exitReason,
|
|
32146
|
+
deferredReview
|
|
32078
32147
|
});
|
|
32079
32148
|
startHeartbeat(ctx.statusWriter, () => totalCost, () => iterations, ctx.logFilePath);
|
|
32080
32149
|
try {
|
|
@@ -32093,6 +32162,7 @@ async function executeSequential(ctx, initialPrd) {
|
|
|
32093
32162
|
return buildResult2("pre-merge-aborted");
|
|
32094
32163
|
}
|
|
32095
32164
|
}
|
|
32165
|
+
deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef);
|
|
32096
32166
|
return buildResult2("completed");
|
|
32097
32167
|
}
|
|
32098
32168
|
const selected = selectNextStories(prd, ctx.config, ctx.batchPlan, currentBatchIndex, lastStoryId, ctx.useBatch);
|
|
@@ -32176,6 +32246,7 @@ var init_sequential_executor = __esm(() => {
|
|
|
32176
32246
|
init_reporters();
|
|
32177
32247
|
init_prd();
|
|
32178
32248
|
init_crash_recovery();
|
|
32249
|
+
init_deferred_review();
|
|
32179
32250
|
init_iteration_runner();
|
|
32180
32251
|
init_story_selector();
|
|
32181
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
|
+
}
|
|
@@ -12,7 +12,8 @@ import { type LoadedHooksConfig, fireHook } from "../../hooks";
|
|
|
12
12
|
import { getSafeLogger } from "../../logger";
|
|
13
13
|
import type { PRD, StructuredFailure, UserStory } from "../../prd";
|
|
14
14
|
import { markStoryFailed, savePRD } from "../../prd";
|
|
15
|
-
import {
|
|
15
|
+
import { tryLlmBatchRoute } from "../../routing/batch-route";
|
|
16
|
+
import { clearCacheForStory } from "../../routing/strategies/llm";
|
|
16
17
|
import type { FailureCategory } from "../../tdd/types";
|
|
17
18
|
import { calculateMaxIterations, escalateTier, getTierConfig } from "../escalation";
|
|
18
19
|
import { hookCtx } from "../helpers";
|
|
@@ -172,25 +173,6 @@ export async function preIterationTierCheck(
|
|
|
172
173
|
return { shouldSkipIteration: true, prdDirty: true, prd: failedPrd };
|
|
173
174
|
}
|
|
174
175
|
|
|
175
|
-
/**
|
|
176
|
-
* Try LLM batch routing for ready stories. Logs and swallows errors (falls back to per-story routing).
|
|
177
|
-
*/
|
|
178
|
-
async function tryLlmBatchRoute(config: NaxConfig, stories: UserStory[], label = "routing"): Promise<void> {
|
|
179
|
-
const mode = config.routing.llm?.mode ?? "hybrid";
|
|
180
|
-
if (config.routing.strategy !== "llm" || mode === "per-story" || stories.length === 0) return;
|
|
181
|
-
const logger = getSafeLogger();
|
|
182
|
-
try {
|
|
183
|
-
logger?.debug("routing", `LLM batch routing: ${label}`, { storyCount: stories.length, mode });
|
|
184
|
-
await llmRouteBatch(stories, { config });
|
|
185
|
-
logger?.debug("routing", "LLM batch routing complete", { label });
|
|
186
|
-
} catch (err) {
|
|
187
|
-
logger?.warn("routing", "LLM batch routing failed, falling back to individual routing", {
|
|
188
|
-
error: (err as Error).message,
|
|
189
|
-
label,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
176
|
export interface EscalationHandlerContext {
|
|
195
177
|
story: UserStory;
|
|
196
178
|
storiesToExecute: UserStory[];
|
|
@@ -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
|
|
|
@@ -25,7 +25,13 @@ export const reviewStage: PipelineStage = {
|
|
|
25
25
|
|
|
26
26
|
logger.info("review", "Running review phase", { storyId: ctx.story.id });
|
|
27
27
|
|
|
28
|
-
const result = await reviewOrchestrator.review(
|
|
28
|
+
const result = await reviewOrchestrator.review(
|
|
29
|
+
ctx.config.review,
|
|
30
|
+
ctx.workdir,
|
|
31
|
+
ctx.config.execution,
|
|
32
|
+
ctx.plugins,
|
|
33
|
+
ctx.storyGitRef,
|
|
34
|
+
);
|
|
29
35
|
|
|
30
36
|
ctx.reviewResult = result.builtIn;
|
|
31
37
|
|
|
@@ -74,6 +74,7 @@ export class ReviewOrchestrator {
|
|
|
74
74
|
workdir: string,
|
|
75
75
|
executionConfig: NaxConfig["execution"],
|
|
76
76
|
plugins?: PluginRegistry,
|
|
77
|
+
storyGitRef?: string,
|
|
77
78
|
): Promise<OrchestratorReviewResult> {
|
|
78
79
|
const logger = getSafeLogger();
|
|
79
80
|
|
|
@@ -83,11 +84,16 @@ export class ReviewOrchestrator {
|
|
|
83
84
|
return { builtIn, success: false, failureReason: builtIn.failureReason, pluginFailed: false };
|
|
84
85
|
}
|
|
85
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
|
+
|
|
86
92
|
if (plugins) {
|
|
87
93
|
const reviewers = plugins.getReviewers();
|
|
88
94
|
if (reviewers.length > 0) {
|
|
89
95
|
// Use the story's start ref if available to capture auto-committed changes
|
|
90
|
-
const baseRef = executionConfig?.storyGitRef;
|
|
96
|
+
const baseRef = storyGitRef ?? executionConfig?.storyGitRef;
|
|
91
97
|
const changedFiles = await getChangedFiles(workdir, baseRef);
|
|
92
98
|
const pluginResults: ReviewResult["pluginReviewers"] = [];
|
|
93
99
|
|
package/src/review/types.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* LLM Batch Routing Helper
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { getAgent } from "../agents/registry";
|
|
5
6
|
import type { NaxConfig } from "../config";
|
|
6
7
|
import { getSafeLogger } from "../logger";
|
|
7
8
|
import type { UserStory } from "../prd";
|
|
@@ -21,7 +22,9 @@ export async function tryLlmBatchRoute(config: NaxConfig, stories: UserStory[],
|
|
|
21
22
|
const logger = getSafeLogger();
|
|
22
23
|
try {
|
|
23
24
|
logger?.debug("routing", `LLM batch routing: ${label}`, { storyCount: stories.length, mode });
|
|
24
|
-
|
|
25
|
+
const agentName = config.execution?.agent ?? "claude";
|
|
26
|
+
const adapter = getAgent(agentName);
|
|
27
|
+
await llmRouteBatch(stories, { config, adapter });
|
|
25
28
|
logger?.debug("routing", "LLM batch routing complete", { label });
|
|
26
29
|
} catch (err) {
|
|
27
30
|
logger?.warn("routing", "LLM batch routing failed, falling back to individual routing", {
|