@nathapp/nax 0.38.2 → 0.39.1
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 +15 -24
- package/package.json +2 -2
- package/src/cli/init-context.ts +348 -0
- package/src/cli/init-detect.ts +169 -0
- package/src/cli/init.ts +96 -31
- package/src/execution/escalation/tier-escalation.ts +2 -20
- package/src/pipeline/stages/review.ts +7 -1
- package/src/review/orchestrator.ts +2 -1
- package/src/routing/batch-route.ts +4 -1
package/dist/nax.js
CHANGED
|
@@ -20159,7 +20159,9 @@ async function tryLlmBatchRoute(config2, stories, label = "routing") {
|
|
|
20159
20159
|
const logger = getSafeLogger();
|
|
20160
20160
|
try {
|
|
20161
20161
|
logger?.debug("routing", `LLM batch routing: ${label}`, { storyCount: stories.length, mode });
|
|
20162
|
-
|
|
20162
|
+
const agentName = config2.execution?.agent ?? "claude";
|
|
20163
|
+
const adapter = getAgent(agentName);
|
|
20164
|
+
await routeBatch(stories, { config: config2, adapter });
|
|
20163
20165
|
logger?.debug("routing", "LLM batch routing complete", { label });
|
|
20164
20166
|
} catch (err) {
|
|
20165
20167
|
logger?.warn("routing", "LLM batch routing failed, falling back to individual routing", {
|
|
@@ -20169,6 +20171,7 @@ async function tryLlmBatchRoute(config2, stories, label = "routing") {
|
|
|
20169
20171
|
}
|
|
20170
20172
|
}
|
|
20171
20173
|
var init_batch_route = __esm(() => {
|
|
20174
|
+
init_registry();
|
|
20172
20175
|
init_logger2();
|
|
20173
20176
|
init_llm();
|
|
20174
20177
|
});
|
|
@@ -20793,7 +20796,7 @@ var package_default;
|
|
|
20793
20796
|
var init_package = __esm(() => {
|
|
20794
20797
|
package_default = {
|
|
20795
20798
|
name: "@nathapp/nax",
|
|
20796
|
-
version: "0.
|
|
20799
|
+
version: "0.39.1",
|
|
20797
20800
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
20798
20801
|
type: "module",
|
|
20799
20802
|
bin: {
|
|
@@ -20857,8 +20860,8 @@ var init_version = __esm(() => {
|
|
|
20857
20860
|
NAX_VERSION = package_default.version;
|
|
20858
20861
|
NAX_COMMIT = (() => {
|
|
20859
20862
|
try {
|
|
20860
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
20861
|
-
return "
|
|
20863
|
+
if (/^[0-9a-f]{6,10}$/.test("91b2a1c"))
|
|
20864
|
+
return "91b2a1c";
|
|
20862
20865
|
} catch {}
|
|
20863
20866
|
try {
|
|
20864
20867
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -22813,7 +22816,7 @@ async function getChangedFiles(workdir, baseRef) {
|
|
|
22813
22816
|
}
|
|
22814
22817
|
|
|
22815
22818
|
class ReviewOrchestrator {
|
|
22816
|
-
async review(reviewConfig, workdir, executionConfig, plugins) {
|
|
22819
|
+
async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef) {
|
|
22817
22820
|
const logger = getSafeLogger();
|
|
22818
22821
|
const builtIn = await runReview(reviewConfig, workdir, executionConfig);
|
|
22819
22822
|
if (!builtIn.success) {
|
|
@@ -22822,7 +22825,7 @@ class ReviewOrchestrator {
|
|
|
22822
22825
|
if (plugins) {
|
|
22823
22826
|
const reviewers = plugins.getReviewers();
|
|
22824
22827
|
if (reviewers.length > 0) {
|
|
22825
|
-
const baseRef = executionConfig?.storyGitRef;
|
|
22828
|
+
const baseRef = storyGitRef ?? executionConfig?.storyGitRef;
|
|
22826
22829
|
const changedFiles = await getChangedFiles(workdir, baseRef);
|
|
22827
22830
|
const pluginResults = [];
|
|
22828
22831
|
for (const reviewer of reviewers) {
|
|
@@ -22897,7 +22900,7 @@ var init_review = __esm(() => {
|
|
|
22897
22900
|
async execute(ctx) {
|
|
22898
22901
|
const logger = getLogger();
|
|
22899
22902
|
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);
|
|
22903
|
+
const result = await reviewOrchestrator.review(ctx.config.review, ctx.workdir, ctx.config.execution, ctx.plugins, ctx.storyGitRef);
|
|
22901
22904
|
ctx.reviewResult = result.builtIn;
|
|
22902
22905
|
if (!result.success) {
|
|
22903
22906
|
const allFindings = result.builtIn.pluginReviewers?.flatMap((pr) => pr.findings ?? []) ?? [];
|
|
@@ -31644,22 +31647,6 @@ function resolveMaxAttemptsOutcome(failureCategory) {
|
|
|
31644
31647
|
return "fail";
|
|
31645
31648
|
}
|
|
31646
31649
|
}
|
|
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
31650
|
function shouldRetrySameTier(verifyResult) {
|
|
31664
31651
|
return verifyResult?.status === "RUNTIME_CRASH";
|
|
31665
31652
|
}
|
|
@@ -31739,7 +31726,7 @@ async function handleTierEscalation(ctx) {
|
|
|
31739
31726
|
clearCacheForStory(story.id);
|
|
31740
31727
|
}
|
|
31741
31728
|
if (routingMode === "hybrid") {
|
|
31742
|
-
await
|
|
31729
|
+
await tryLlmBatchRoute(ctx.config, storiesToEscalate, "hybrid-re-route-pipeline");
|
|
31743
31730
|
}
|
|
31744
31731
|
return {
|
|
31745
31732
|
outcome: "escalated",
|
|
@@ -31752,6 +31739,7 @@ var init_tier_escalation = __esm(() => {
|
|
|
31752
31739
|
init_hooks();
|
|
31753
31740
|
init_logger2();
|
|
31754
31741
|
init_prd();
|
|
31742
|
+
init_batch_route();
|
|
31755
31743
|
init_llm();
|
|
31756
31744
|
init_escalation();
|
|
31757
31745
|
init_helpers();
|
|
@@ -65180,6 +65168,9 @@ async function exportPromptCommand(options) {
|
|
|
65180
65168
|
// src/cli/init.ts
|
|
65181
65169
|
init_paths();
|
|
65182
65170
|
init_logger2();
|
|
65171
|
+
|
|
65172
|
+
// src/cli/init-context.ts
|
|
65173
|
+
init_logger2();
|
|
65183
65174
|
// src/cli/plugins.ts
|
|
65184
65175
|
init_loader5();
|
|
65185
65176
|
import * as os2 from "os";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nathapp/nax",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "AI Coding Agent Orchestrator
|
|
3
|
+
"version": "0.39.1",
|
|
4
|
+
"description": "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"nax": "./dist/nax.js"
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context.md Generation (INIT-002)
|
|
3
|
+
*
|
|
4
|
+
* Generates context.md from filesystem scan with optional LLM enhancement.
|
|
5
|
+
* Default mode: template from scan (zero LLM cost)
|
|
6
|
+
* AI mode (--ai flag): LLM-powered narrative context
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import { mkdir } from "node:fs/promises";
|
|
11
|
+
import { basename, join } from "node:path";
|
|
12
|
+
import { getLogger } from "../logger";
|
|
13
|
+
|
|
14
|
+
/** Project scan results */
|
|
15
|
+
export interface ProjectScan {
|
|
16
|
+
projectName: string;
|
|
17
|
+
fileTree: string[];
|
|
18
|
+
packageManifest: {
|
|
19
|
+
name?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
scripts?: Record<string, string>;
|
|
22
|
+
dependencies?: Record<string, string>;
|
|
23
|
+
} | null;
|
|
24
|
+
readmeSnippet: string | null;
|
|
25
|
+
entryPoints: string[];
|
|
26
|
+
configFiles: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Package manifest structure */
|
|
30
|
+
interface PackageManifest {
|
|
31
|
+
name?: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
scripts?: Record<string, string>;
|
|
34
|
+
dependencies?: Record<string, string>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** initContext options */
|
|
38
|
+
export interface InitContextOptions {
|
|
39
|
+
ai?: boolean;
|
|
40
|
+
force?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Dependency injection for testing */
|
|
44
|
+
export const _deps = {
|
|
45
|
+
callLLM: async (_prompt: string): Promise<string> => {
|
|
46
|
+
// Placeholder implementation
|
|
47
|
+
// In production, this would call the nax LLM infrastructure
|
|
48
|
+
throw new Error("callLLM not implemented");
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Recursively find all files in a directory, excluding certain paths.
|
|
54
|
+
* Returns relative paths, limited to maxFiles entries.
|
|
55
|
+
*/
|
|
56
|
+
async function findFiles(dir: string, maxFiles = 200): Promise<string[]> {
|
|
57
|
+
// Use find command to locate files, excluding common directories
|
|
58
|
+
try {
|
|
59
|
+
const proc = Bun.spawnSync(
|
|
60
|
+
[
|
|
61
|
+
"find",
|
|
62
|
+
dir,
|
|
63
|
+
"-type",
|
|
64
|
+
"f",
|
|
65
|
+
"-not",
|
|
66
|
+
"-path",
|
|
67
|
+
"*/node_modules/*",
|
|
68
|
+
"-not",
|
|
69
|
+
"-path",
|
|
70
|
+
"*/.git/*",
|
|
71
|
+
"-not",
|
|
72
|
+
"-path",
|
|
73
|
+
"*/dist/*",
|
|
74
|
+
],
|
|
75
|
+
{ stdio: ["pipe", "pipe", "pipe"] },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (proc.success) {
|
|
79
|
+
const output = new TextDecoder().decode(proc.stdout);
|
|
80
|
+
const files = output
|
|
81
|
+
.trim()
|
|
82
|
+
.split("\n")
|
|
83
|
+
.filter((f) => f.length > 0)
|
|
84
|
+
.map((f) => f.replace(`${dir}/`, ""))
|
|
85
|
+
.slice(0, maxFiles);
|
|
86
|
+
return files;
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// find command failed, use fallback
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Read and parse package.json if it exists
|
|
97
|
+
*/
|
|
98
|
+
async function readPackageManifest(projectRoot: string): Promise<PackageManifest | null> {
|
|
99
|
+
const packageJsonPath = join(projectRoot, "package.json");
|
|
100
|
+
|
|
101
|
+
if (!existsSync(packageJsonPath)) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const content = await Bun.file(packageJsonPath).text();
|
|
107
|
+
const manifest = JSON.parse(content) as PackageManifest;
|
|
108
|
+
return {
|
|
109
|
+
name: manifest.name,
|
|
110
|
+
description: manifest.description,
|
|
111
|
+
scripts: manifest.scripts,
|
|
112
|
+
dependencies: manifest.dependencies,
|
|
113
|
+
};
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Read first 100 lines of README.md if it exists
|
|
121
|
+
*/
|
|
122
|
+
async function readReadmeSnippet(projectRoot: string): Promise<string | null> {
|
|
123
|
+
const readmePath = join(projectRoot, "README.md");
|
|
124
|
+
|
|
125
|
+
if (!existsSync(readmePath)) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const content = await Bun.file(readmePath).text();
|
|
131
|
+
const lines = content.split("\n");
|
|
132
|
+
return lines.slice(0, 100).join("\n");
|
|
133
|
+
} catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Detect entry points in the project
|
|
140
|
+
*/
|
|
141
|
+
async function detectEntryPoints(projectRoot: string): Promise<string[]> {
|
|
142
|
+
const candidates = ["src/index.ts", "src/main.ts", "main.go", "src/lib.rs"];
|
|
143
|
+
const found: string[] = [];
|
|
144
|
+
|
|
145
|
+
for (const candidate of candidates) {
|
|
146
|
+
const path = join(projectRoot, candidate);
|
|
147
|
+
if (existsSync(path)) {
|
|
148
|
+
found.push(candidate);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return found;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Detect config files in the project
|
|
157
|
+
*/
|
|
158
|
+
async function detectConfigFiles(projectRoot: string): Promise<string[]> {
|
|
159
|
+
const candidates = ["tsconfig.json", "biome.json", "turbo.json", ".env.example"];
|
|
160
|
+
const found: string[] = [];
|
|
161
|
+
|
|
162
|
+
for (const candidate of candidates) {
|
|
163
|
+
const path = join(projectRoot, candidate);
|
|
164
|
+
if (existsSync(path)) {
|
|
165
|
+
found.push(candidate);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return found;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Scan a project for context information
|
|
174
|
+
*/
|
|
175
|
+
export async function scanProject(projectRoot: string): Promise<ProjectScan> {
|
|
176
|
+
const fileTree = await findFiles(projectRoot, 200);
|
|
177
|
+
const packageManifest = await readPackageManifest(projectRoot);
|
|
178
|
+
const readmeSnippet = await readReadmeSnippet(projectRoot);
|
|
179
|
+
const entryPoints = await detectEntryPoints(projectRoot);
|
|
180
|
+
const configFiles = await detectConfigFiles(projectRoot);
|
|
181
|
+
|
|
182
|
+
// Determine project name from package.json or directory basename
|
|
183
|
+
const projectName = packageManifest?.name || basename(projectRoot);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
projectName,
|
|
187
|
+
fileTree,
|
|
188
|
+
packageManifest,
|
|
189
|
+
readmeSnippet,
|
|
190
|
+
entryPoints,
|
|
191
|
+
configFiles,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Generate a markdown template for context.md from scan results
|
|
197
|
+
*/
|
|
198
|
+
export function generateContextTemplate(scan: ProjectScan): string {
|
|
199
|
+
const lines: string[] = [];
|
|
200
|
+
|
|
201
|
+
lines.push(`# ${scan.projectName}\n`);
|
|
202
|
+
|
|
203
|
+
if (scan.packageManifest?.description) {
|
|
204
|
+
lines.push(`${scan.packageManifest.description}\n`);
|
|
205
|
+
} else {
|
|
206
|
+
lines.push("<!-- TODO: Add project description -->\n");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (scan.entryPoints.length > 0) {
|
|
210
|
+
lines.push("## Entry Points\n");
|
|
211
|
+
for (const ep of scan.entryPoints) {
|
|
212
|
+
lines.push(`- ${ep}`);
|
|
213
|
+
}
|
|
214
|
+
lines.push("");
|
|
215
|
+
} else {
|
|
216
|
+
lines.push("## Entry Points\n");
|
|
217
|
+
lines.push("<!-- TODO: Document entry points -->\n");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (scan.fileTree.length > 0) {
|
|
221
|
+
lines.push("## Project Structure\n");
|
|
222
|
+
lines.push("```");
|
|
223
|
+
for (const file of scan.fileTree.slice(0, 20)) {
|
|
224
|
+
lines.push(file);
|
|
225
|
+
}
|
|
226
|
+
if (scan.fileTree.length > 20) {
|
|
227
|
+
lines.push(`... and ${scan.fileTree.length - 20} more files`);
|
|
228
|
+
}
|
|
229
|
+
lines.push("```\n");
|
|
230
|
+
} else {
|
|
231
|
+
lines.push("## Project Structure\n");
|
|
232
|
+
lines.push("<!-- TODO: Document project structure -->\n");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (scan.configFiles.length > 0) {
|
|
236
|
+
lines.push("## Configuration Files\n");
|
|
237
|
+
for (const cf of scan.configFiles) {
|
|
238
|
+
lines.push(`- ${cf}`);
|
|
239
|
+
}
|
|
240
|
+
lines.push("");
|
|
241
|
+
} else {
|
|
242
|
+
lines.push("## Configuration Files\n");
|
|
243
|
+
lines.push("<!-- TODO: Document configuration files -->\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (scan.packageManifest?.scripts) {
|
|
247
|
+
const hasScripts = Object.keys(scan.packageManifest.scripts).length > 0;
|
|
248
|
+
if (hasScripts) {
|
|
249
|
+
lines.push("## Scripts\n");
|
|
250
|
+
for (const [name, command] of Object.entries(scan.packageManifest.scripts)) {
|
|
251
|
+
lines.push(`- **${name}**: \`${command}\``);
|
|
252
|
+
}
|
|
253
|
+
lines.push("");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (scan.packageManifest?.dependencies) {
|
|
258
|
+
const deps = Object.keys(scan.packageManifest.dependencies);
|
|
259
|
+
if (deps.length > 0) {
|
|
260
|
+
lines.push("## Dependencies\n");
|
|
261
|
+
lines.push("<!-- TODO: Document key dependencies and their purpose -->\n");
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
lines.push("## Development Guidelines\n");
|
|
266
|
+
lines.push("<!-- TODO: Document development guidelines and conventions -->\n");
|
|
267
|
+
|
|
268
|
+
return `${lines.join("\n").trim()}\n`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Generate context.md with LLM enhancement
|
|
273
|
+
*/
|
|
274
|
+
async function generateContextWithLLM(scan: ProjectScan): Promise<string> {
|
|
275
|
+
const logger = getLogger();
|
|
276
|
+
|
|
277
|
+
// Build LLM prompt from scan results
|
|
278
|
+
const scanSummary = `
|
|
279
|
+
Project: ${scan.projectName}
|
|
280
|
+
Entry Points: ${scan.entryPoints.join(", ") || "None detected"}
|
|
281
|
+
Config Files: ${scan.configFiles.join(", ") || "None detected"}
|
|
282
|
+
Total Files: ${scan.fileTree.length}
|
|
283
|
+
Description: ${scan.packageManifest?.description || "Not provided"}
|
|
284
|
+
`;
|
|
285
|
+
|
|
286
|
+
const prompt = `
|
|
287
|
+
You are a technical documentation expert. Generate a concise, well-structured context.md file for a software project based on this scan:
|
|
288
|
+
|
|
289
|
+
${scanSummary}
|
|
290
|
+
|
|
291
|
+
The context.md should include:
|
|
292
|
+
1. Project overview (name, purpose, key technologies)
|
|
293
|
+
2. Entry points and main modules
|
|
294
|
+
3. Key dependencies and why they're used
|
|
295
|
+
4. Development setup and common commands
|
|
296
|
+
5. Architecture overview (brief)
|
|
297
|
+
6. Development guidelines
|
|
298
|
+
|
|
299
|
+
Keep it under 2000 tokens. Use markdown formatting. Be specific to the detected stack and structure.
|
|
300
|
+
`;
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
const result = await _deps.callLLM(prompt);
|
|
304
|
+
logger.info("init", "Generated context.md with LLM");
|
|
305
|
+
return result;
|
|
306
|
+
} catch (err) {
|
|
307
|
+
logger.warn(
|
|
308
|
+
"init",
|
|
309
|
+
`LLM context generation failed, falling back to template: ${err instanceof Error ? err.message : String(err)}`,
|
|
310
|
+
);
|
|
311
|
+
return generateContextTemplate(scan);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Initialize context.md for a project
|
|
317
|
+
*/
|
|
318
|
+
export async function initContext(projectRoot: string, options: InitContextOptions = {}): Promise<void> {
|
|
319
|
+
const logger = getLogger();
|
|
320
|
+
const naxDir = join(projectRoot, "nax");
|
|
321
|
+
const contextPath = join(naxDir, "context.md");
|
|
322
|
+
|
|
323
|
+
// Check if context.md already exists
|
|
324
|
+
if (existsSync(contextPath) && !options.force) {
|
|
325
|
+
logger.info("init", "context.md already exists, skipping (use --force to overwrite)", { path: contextPath });
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Create nax directory if needed
|
|
330
|
+
if (!existsSync(naxDir)) {
|
|
331
|
+
await mkdir(naxDir, { recursive: true });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Scan the project
|
|
335
|
+
const scan = await scanProject(projectRoot);
|
|
336
|
+
|
|
337
|
+
// Generate content (template or LLM-enhanced)
|
|
338
|
+
let content: string;
|
|
339
|
+
if (options.ai) {
|
|
340
|
+
content = await generateContextWithLLM(scan);
|
|
341
|
+
} else {
|
|
342
|
+
content = generateContextTemplate(scan);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Write context.md
|
|
346
|
+
await Bun.write(contextPath, content);
|
|
347
|
+
logger.info("init", "Generated nax/context.md template from project scan", { path: contextPath });
|
|
348
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project Stack Detection for nax init
|
|
3
|
+
*
|
|
4
|
+
* Scans the project root for stack indicators and builds quality.commands
|
|
5
|
+
* for nax/config.json.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync } from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
/** Detected project runtime */
|
|
12
|
+
export type Runtime = "bun" | "node" | "unknown";
|
|
13
|
+
|
|
14
|
+
/** Detected project language */
|
|
15
|
+
export type Language = "typescript" | "python" | "rust" | "go" | "unknown";
|
|
16
|
+
|
|
17
|
+
/** Detected linter */
|
|
18
|
+
export type Linter = "biome" | "eslint" | "ruff" | "clippy" | "golangci-lint" | "unknown";
|
|
19
|
+
|
|
20
|
+
/** Detected monorepo tooling */
|
|
21
|
+
export type Monorepo = "turborepo" | "none";
|
|
22
|
+
|
|
23
|
+
/** Full detected project stack */
|
|
24
|
+
export interface ProjectStack {
|
|
25
|
+
runtime: Runtime;
|
|
26
|
+
language: Language;
|
|
27
|
+
linter: Linter;
|
|
28
|
+
monorepo: Monorepo;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Quality commands derived from stack detection */
|
|
32
|
+
export interface QualityCommands {
|
|
33
|
+
typecheck?: string;
|
|
34
|
+
lint?: string;
|
|
35
|
+
test?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function detectRuntime(projectRoot: string): Runtime {
|
|
39
|
+
if (existsSync(join(projectRoot, "bun.lockb")) || existsSync(join(projectRoot, "bunfig.toml"))) {
|
|
40
|
+
return "bun";
|
|
41
|
+
}
|
|
42
|
+
if (
|
|
43
|
+
existsSync(join(projectRoot, "package-lock.json")) ||
|
|
44
|
+
existsSync(join(projectRoot, "yarn.lock")) ||
|
|
45
|
+
existsSync(join(projectRoot, "pnpm-lock.yaml"))
|
|
46
|
+
) {
|
|
47
|
+
return "node";
|
|
48
|
+
}
|
|
49
|
+
return "unknown";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function detectLanguage(projectRoot: string): Language {
|
|
53
|
+
if (existsSync(join(projectRoot, "tsconfig.json"))) return "typescript";
|
|
54
|
+
if (existsSync(join(projectRoot, "pyproject.toml")) || existsSync(join(projectRoot, "setup.py"))) {
|
|
55
|
+
return "python";
|
|
56
|
+
}
|
|
57
|
+
if (existsSync(join(projectRoot, "Cargo.toml"))) return "rust";
|
|
58
|
+
if (existsSync(join(projectRoot, "go.mod"))) return "go";
|
|
59
|
+
return "unknown";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function detectLinter(projectRoot: string): Linter {
|
|
63
|
+
if (existsSync(join(projectRoot, "biome.json")) || existsSync(join(projectRoot, "biome.jsonc"))) {
|
|
64
|
+
return "biome";
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
existsSync(join(projectRoot, ".eslintrc.json")) ||
|
|
68
|
+
existsSync(join(projectRoot, ".eslintrc.js")) ||
|
|
69
|
+
existsSync(join(projectRoot, "eslint.config.js"))
|
|
70
|
+
) {
|
|
71
|
+
return "eslint";
|
|
72
|
+
}
|
|
73
|
+
return "unknown";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function detectMonorepo(projectRoot: string): Monorepo {
|
|
77
|
+
if (existsSync(join(projectRoot, "turbo.json"))) return "turborepo";
|
|
78
|
+
return "none";
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Detect the project stack by scanning for indicator files.
|
|
83
|
+
*/
|
|
84
|
+
export function detectProjectStack(projectRoot: string): ProjectStack {
|
|
85
|
+
return {
|
|
86
|
+
runtime: detectRuntime(projectRoot),
|
|
87
|
+
language: detectLanguage(projectRoot),
|
|
88
|
+
linter: detectLinter(projectRoot),
|
|
89
|
+
monorepo: detectMonorepo(projectRoot),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveLintCommand(stack: ProjectStack, fallback: string): string {
|
|
94
|
+
if (stack.linter === "biome") return "biome check .";
|
|
95
|
+
if (stack.linter === "eslint") return "eslint .";
|
|
96
|
+
return fallback;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build quality.commands from a detected project stack.
|
|
101
|
+
*/
|
|
102
|
+
export function buildQualityCommands(stack: ProjectStack): QualityCommands {
|
|
103
|
+
if (stack.runtime === "bun" && stack.language === "typescript") {
|
|
104
|
+
return {
|
|
105
|
+
typecheck: "bun run tsc --noEmit",
|
|
106
|
+
lint: resolveLintCommand(stack, "bun run lint"),
|
|
107
|
+
test: "bun test",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (stack.runtime === "node" && stack.language === "typescript") {
|
|
112
|
+
return {
|
|
113
|
+
typecheck: "npx tsc --noEmit",
|
|
114
|
+
lint: resolveLintCommand(stack, "npm run lint"),
|
|
115
|
+
test: "npm test",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (stack.language === "python") {
|
|
120
|
+
return {
|
|
121
|
+
lint: "ruff check .",
|
|
122
|
+
test: "pytest",
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (stack.language === "rust") {
|
|
127
|
+
return {
|
|
128
|
+
typecheck: "cargo check",
|
|
129
|
+
lint: "cargo clippy",
|
|
130
|
+
test: "cargo test",
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (stack.language === "go") {
|
|
135
|
+
return {
|
|
136
|
+
typecheck: "go vet ./...",
|
|
137
|
+
lint: "golangci-lint run",
|
|
138
|
+
test: "go test ./...",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isStackDetected(stack: ProjectStack): boolean {
|
|
146
|
+
return stack.runtime !== "unknown" || stack.language !== "unknown";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Build the full init config object from a detected project stack.
|
|
151
|
+
* Falls back to minimal config when stack is undetected.
|
|
152
|
+
*/
|
|
153
|
+
export function buildInitConfig(stack: ProjectStack): object {
|
|
154
|
+
if (!isStackDetected(stack)) {
|
|
155
|
+
return { version: 1 };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const commands = buildQualityCommands(stack);
|
|
159
|
+
const hasCommands = Object.keys(commands).length > 0;
|
|
160
|
+
|
|
161
|
+
if (!hasCommands) {
|
|
162
|
+
return { version: 1 };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
version: 1,
|
|
167
|
+
quality: { commands },
|
|
168
|
+
};
|
|
169
|
+
}
|
package/src/cli/init.ts
CHANGED
|
@@ -8,8 +8,10 @@ import { existsSync } from "node:fs";
|
|
|
8
8
|
import { mkdir } from "node:fs/promises";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { globalConfigDir, projectConfigDir } from "../config/paths";
|
|
11
|
-
import { DEFAULT_CONFIG } from "../config/schema";
|
|
12
11
|
import { getLogger } from "../logger";
|
|
12
|
+
import { initContext } from "./init-context";
|
|
13
|
+
import { buildInitConfig, detectProjectStack } from "./init-detect";
|
|
14
|
+
import type { ProjectStack } from "./init-detect";
|
|
13
15
|
import { promptsInitCommand } from "./prompts";
|
|
14
16
|
|
|
15
17
|
/** Init command options */
|
|
@@ -20,10 +22,18 @@ export interface InitOptions {
|
|
|
20
22
|
projectRoot?: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
25
|
+
/** Options for initProject */
|
|
26
|
+
export interface InitProjectOptions {
|
|
27
|
+
/** Use LLM to generate context.md (--ai flag) */
|
|
28
|
+
ai?: boolean;
|
|
29
|
+
/** Force overwrite of existing files */
|
|
30
|
+
force?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
/**
|
|
24
34
|
* Gitignore entries added by nax init
|
|
25
35
|
*/
|
|
26
|
-
const NAX_GITIGNORE_ENTRIES = [".nax-verifier-verdict.json"];
|
|
36
|
+
const NAX_GITIGNORE_ENTRIES = [".nax-verifier-verdict.json", "nax.lock", "nax/**/runs/", "nax/metrics.json"];
|
|
27
37
|
|
|
28
38
|
/**
|
|
29
39
|
* Add nax-specific entries to .gitignore if not already present.
|
|
@@ -55,33 +65,58 @@ async function updateGitignore(projectRoot: string): Promise<void> {
|
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
/**
|
|
58
|
-
*
|
|
68
|
+
* Build a stack-aware constitution.md from the detected project stack.
|
|
59
69
|
*/
|
|
60
|
-
|
|
70
|
+
function buildConstitution(stack: ProjectStack): string {
|
|
71
|
+
const sections: string[] = [];
|
|
61
72
|
|
|
62
|
-
|
|
63
|
-
- Deliver high-quality, maintainable code
|
|
64
|
-
- Follow project conventions and best practices
|
|
65
|
-
- Maintain comprehensive test coverage
|
|
73
|
+
sections.push("# Project Constitution\n");
|
|
66
74
|
|
|
67
|
-
##
|
|
68
|
-
-
|
|
69
|
-
- Follow
|
|
70
|
-
-
|
|
75
|
+
sections.push("## Goals");
|
|
76
|
+
sections.push("- Deliver high-quality, maintainable code");
|
|
77
|
+
sections.push("- Follow project conventions and best practices");
|
|
78
|
+
sections.push("- Maintain comprehensive test coverage\n");
|
|
71
79
|
|
|
72
|
-
##
|
|
73
|
-
-
|
|
74
|
-
-
|
|
75
|
-
- Clear, descriptive naming
|
|
76
|
-
`;
|
|
80
|
+
sections.push("## Constraints");
|
|
81
|
+
sections.push("- Follow functional style for pure logic");
|
|
82
|
+
sections.push("- Keep files focused and under 400 lines\n");
|
|
77
83
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
84
|
+
if (stack.runtime === "bun") {
|
|
85
|
+
sections.push("## Bun-Native APIs");
|
|
86
|
+
sections.push("- Use `Bun.file()` for file reads, `Bun.write()` for file writes");
|
|
87
|
+
sections.push("- Use `Bun.spawn()` for subprocesses (never `child_process`)");
|
|
88
|
+
sections.push("- Use `Bun.sleep()` for delays");
|
|
89
|
+
sections.push("- Use `bun test` for running tests\n");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (stack.language === "typescript") {
|
|
93
|
+
sections.push("## strict TypeScript");
|
|
94
|
+
sections.push("- Enable strict mode in tsconfig.json");
|
|
95
|
+
sections.push("- No `any` in public APIs — use `unknown` + type guards");
|
|
96
|
+
sections.push("- Explicit return types on all exported functions\n");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (stack.language === "python") {
|
|
100
|
+
sections.push("## Python Standards");
|
|
101
|
+
sections.push("- Follow PEP 8 style guide for formatting and naming");
|
|
102
|
+
sections.push("- Add type hints to all function signatures");
|
|
103
|
+
sections.push("- Use type annotations for variables where non-obvious\n");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (stack.monorepo === "turborepo") {
|
|
107
|
+
sections.push("## Monorepo Conventions");
|
|
108
|
+
sections.push("- Respect package boundaries — do not import across packages without explicit dependency");
|
|
109
|
+
sections.push("- Each package should be independently buildable and testable");
|
|
110
|
+
sections.push("- Shared utilities go in a dedicated `packages/shared` (or equivalent) package\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
sections.push("## Preferences");
|
|
114
|
+
sections.push("- Prefer immutability over mutation");
|
|
115
|
+
sections.push("- Write tests first (TDD approach)");
|
|
116
|
+
sections.push("- Clear, descriptive naming");
|
|
117
|
+
|
|
118
|
+
return `${sections.join("\n")}\n`;
|
|
119
|
+
}
|
|
85
120
|
|
|
86
121
|
const MINIMAL_GLOBAL_CONFIG = {
|
|
87
122
|
version: 1,
|
|
@@ -113,7 +148,10 @@ async function initGlobal(): Promise<void> {
|
|
|
113
148
|
// Create ~/.nax/constitution.md if it doesn't exist
|
|
114
149
|
const constitutionPath = join(globalDir, "constitution.md");
|
|
115
150
|
if (!existsSync(constitutionPath)) {
|
|
116
|
-
await Bun.write(
|
|
151
|
+
await Bun.write(
|
|
152
|
+
constitutionPath,
|
|
153
|
+
buildConstitution({ runtime: "unknown", language: "unknown", linter: "unknown", monorepo: "none" }),
|
|
154
|
+
);
|
|
117
155
|
logger.info("init", "Created global constitution", { path: constitutionPath });
|
|
118
156
|
} else {
|
|
119
157
|
logger.info("init", "Global constitution already exists", { path: constitutionPath });
|
|
@@ -134,7 +172,7 @@ async function initGlobal(): Promise<void> {
|
|
|
134
172
|
/**
|
|
135
173
|
* Initialize project nax directory (nax/)
|
|
136
174
|
*/
|
|
137
|
-
export async function initProject(projectRoot: string): Promise<void> {
|
|
175
|
+
export async function initProject(projectRoot: string, options?: InitProjectOptions): Promise<void> {
|
|
138
176
|
const logger = getLogger();
|
|
139
177
|
const projectDir = projectConfigDir(projectRoot);
|
|
140
178
|
|
|
@@ -144,19 +182,32 @@ export async function initProject(projectRoot: string): Promise<void> {
|
|
|
144
182
|
logger.info("init", "Created project config directory", { path: projectDir });
|
|
145
183
|
}
|
|
146
184
|
|
|
185
|
+
// Detect project stack and build config
|
|
186
|
+
const stack = detectProjectStack(projectRoot);
|
|
187
|
+
const projectConfig = buildInitConfig(stack);
|
|
188
|
+
logger.info("init", "Detected project stack", {
|
|
189
|
+
runtime: stack.runtime,
|
|
190
|
+
language: stack.language,
|
|
191
|
+
linter: stack.linter,
|
|
192
|
+
monorepo: stack.monorepo,
|
|
193
|
+
});
|
|
194
|
+
|
|
147
195
|
// Create nax/config.json if it doesn't exist
|
|
148
196
|
const configPath = join(projectDir, "config.json");
|
|
149
197
|
if (!existsSync(configPath)) {
|
|
150
|
-
await Bun.write(configPath, `${JSON.stringify(
|
|
198
|
+
await Bun.write(configPath, `${JSON.stringify(projectConfig, null, 2)}\n`);
|
|
151
199
|
logger.info("init", "Created project config", { path: configPath });
|
|
152
200
|
} else {
|
|
153
201
|
logger.info("init", "Project config already exists", { path: configPath });
|
|
154
202
|
}
|
|
155
203
|
|
|
156
|
-
//
|
|
204
|
+
// Generate context.md (template or LLM-enhanced with --ai flag)
|
|
205
|
+
await initContext(projectRoot, { ai: options?.ai, force: options?.force });
|
|
206
|
+
|
|
207
|
+
// Create nax/constitution.md with stack-aware content
|
|
157
208
|
const constitutionPath = join(projectDir, "constitution.md");
|
|
158
|
-
if (!existsSync(constitutionPath)) {
|
|
159
|
-
await Bun.write(constitutionPath,
|
|
209
|
+
if (!existsSync(constitutionPath) || options?.force) {
|
|
210
|
+
await Bun.write(constitutionPath, buildConstitution(stack));
|
|
160
211
|
logger.info("init", "Created project constitution", { path: constitutionPath });
|
|
161
212
|
} else {
|
|
162
213
|
logger.info("init", "Project constitution already exists", { path: constitutionPath });
|
|
@@ -174,11 +225,25 @@ export async function initProject(projectRoot: string): Promise<void> {
|
|
|
174
225
|
// Update .gitignore to include nax-specific entries
|
|
175
226
|
await updateGitignore(projectRoot);
|
|
176
227
|
|
|
177
|
-
// Create prompt templates
|
|
228
|
+
// Create prompt templates
|
|
178
229
|
// Pass autoWireConfig: false to prevent auto-wiring prompts.overrides
|
|
179
230
|
// Templates are created but not activated until user explicitly configures them
|
|
180
231
|
await promptsInitCommand({ workdir: projectRoot, force: false, autoWireConfig: false });
|
|
181
232
|
|
|
233
|
+
// Print summary
|
|
234
|
+
console.log("\n[OK] nax init complete. Created files:");
|
|
235
|
+
console.log(" - nax/config.json");
|
|
236
|
+
console.log(" - nax/context.md");
|
|
237
|
+
console.log(" - nax/constitution.md");
|
|
238
|
+
console.log(" - nax/hooks/");
|
|
239
|
+
console.log(" - nax/templates/");
|
|
240
|
+
console.log("\nNext steps:");
|
|
241
|
+
console.log(" 1. Review nax/context.md and fill in TODOs");
|
|
242
|
+
console.log(" 2. Review nax/config.json and adjust quality commands");
|
|
243
|
+
console.log(" 3. Run: nax generate");
|
|
244
|
+
console.log(" 4. Run: nax plan");
|
|
245
|
+
console.log(" 5. Run: nax run");
|
|
246
|
+
|
|
182
247
|
logger.info("init", "Project config initialized successfully", { path: projectDir });
|
|
183
248
|
}
|
|
184
249
|
|
|
@@ -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[];
|
|
@@ -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
|
|
|
@@ -87,7 +88,7 @@ export class ReviewOrchestrator {
|
|
|
87
88
|
const reviewers = plugins.getReviewers();
|
|
88
89
|
if (reviewers.length > 0) {
|
|
89
90
|
// Use the story's start ref if available to capture auto-committed changes
|
|
90
|
-
const baseRef = executionConfig?.storyGitRef;
|
|
91
|
+
const baseRef = storyGitRef ?? executionConfig?.storyGitRef;
|
|
91
92
|
const changedFiles = await getChangedFiles(workdir, baseRef);
|
|
92
93
|
const pluginResults: ReviewResult["pluginReviewers"] = [];
|
|
93
94
|
|
|
@@ -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", {
|