@nathapp/nax 0.49.0 → 0.49.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/CHANGELOG.md +8 -0
- package/dist/nax.js +45 -9
- package/package.json +1 -1
- package/src/acceptance/generator.ts +4 -1
- package/src/agents/acp/adapter.ts +7 -2
- package/src/agents/acp/parser.ts +1 -1
- package/src/agents/acp/spawn-client.ts +2 -0
- package/src/agents/types.ts +6 -0
- package/src/cli/plan.ts +11 -1
- package/src/pipeline/stages/routing.ts +20 -1
- package/src/utils/git.ts +10 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.49.1] - 2026-03-18
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **ACP zero cost:** `acpx prompt` was called without `--format json`, causing it to output plain text instead of JSON-RPC NDJSON. Cost and token usage were always 0. Fix: pass `--format json` as a global flag so the parser receives `usage_update` (exact cost in USD) and `result.usage` (token breakdown).
|
|
12
|
+
- **Decompose session name / model:** Decompose one-shots used an auto-generated timestamp session name and passed the tier string (`"balanced"`) as the model instead of the resolved model ID. Fix: session name is now `nax-decompose-<story-id>` and model tier is resolved via `resolveModel()` before the `complete()` call.
|
|
13
|
+
- **`autoCommitIfDirty` skipping monorepo subdirs:** The working-directory guard rejected any workdir that wasn't exactly the git root, silently skipping commits for monorepo package subdirs. Fix: allow subdirs (`startsWith(gitRoot + '/')`); use `git add .` for subdirs vs `git add -A` at root.
|
|
14
|
+
- **`complete()` missing model in `generateFromPRD()` and `plan` auto mode:** `generator.ts` ignored `options.modelDef.model`; `plan.ts` auto path didn't call `resolveModel()`. Both now pass the correct resolved model to `adapter.complete()`.
|
|
15
|
+
|
|
8
16
|
## [0.46.2] - 2026-03-17
|
|
9
17
|
|
|
10
18
|
### Fixed
|
package/dist/nax.js
CHANGED
|
@@ -18729,7 +18729,10 @@ describe("${options.featureName} - Acceptance Tests", () => {
|
|
|
18729
18729
|
|
|
18730
18730
|
IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\`\`typescript or \`\`\`). Start directly with the import statement.`;
|
|
18731
18731
|
logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
|
|
18732
|
-
const rawOutput = await _generatorPRDDeps.adapter.complete(prompt, {
|
|
18732
|
+
const rawOutput = await _generatorPRDDeps.adapter.complete(prompt, {
|
|
18733
|
+
model: options.modelDef.model,
|
|
18734
|
+
config: options.config
|
|
18735
|
+
});
|
|
18733
18736
|
const testCode = extractTestCode(rawOutput);
|
|
18734
18737
|
const refinedJsonContent = JSON.stringify(refinedCriteria.map((c, i) => ({
|
|
18735
18738
|
acId: `AC-${i + 1}`,
|
|
@@ -19170,6 +19173,8 @@ class SpawnAcpSession {
|
|
|
19170
19173
|
"acpx",
|
|
19171
19174
|
"--cwd",
|
|
19172
19175
|
this.cwd,
|
|
19176
|
+
"--format",
|
|
19177
|
+
"json",
|
|
19173
19178
|
...this.permissionMode === "approve-all" ? ["--approve-all"] : [],
|
|
19174
19179
|
"--model",
|
|
19175
19180
|
this.model,
|
|
@@ -19742,7 +19747,11 @@ class AcpAgentAdapter {
|
|
|
19742
19747
|
await client.start();
|
|
19743
19748
|
let session = null;
|
|
19744
19749
|
try {
|
|
19745
|
-
session = await client.createSession({
|
|
19750
|
+
session = await client.createSession({
|
|
19751
|
+
agentName: this.name,
|
|
19752
|
+
permissionMode,
|
|
19753
|
+
sessionName: _options?.sessionName
|
|
19754
|
+
});
|
|
19746
19755
|
let timeoutId;
|
|
19747
19756
|
const timeoutPromise = new Promise((_, reject) => {
|
|
19748
19757
|
timeoutId = setTimeout(() => reject(new Error(`complete() timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
@@ -22241,7 +22250,7 @@ var package_default;
|
|
|
22241
22250
|
var init_package = __esm(() => {
|
|
22242
22251
|
package_default = {
|
|
22243
22252
|
name: "@nathapp/nax",
|
|
22244
|
-
version: "0.49.
|
|
22253
|
+
version: "0.49.1",
|
|
22245
22254
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
22246
22255
|
type: "module",
|
|
22247
22256
|
bin: {
|
|
@@ -22314,8 +22323,8 @@ var init_version = __esm(() => {
|
|
|
22314
22323
|
NAX_VERSION = package_default.version;
|
|
22315
22324
|
NAX_COMMIT = (() => {
|
|
22316
22325
|
try {
|
|
22317
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
22318
|
-
return "
|
|
22326
|
+
if (/^[0-9a-f]{6,10}$/.test("635a552"))
|
|
22327
|
+
return "635a552";
|
|
22319
22328
|
} catch {}
|
|
22320
22329
|
try {
|
|
22321
22330
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -26178,7 +26187,9 @@ async function autoCommitIfDirty(workdir, stage, role, storyId) {
|
|
|
26178
26187
|
return gitRoot;
|
|
26179
26188
|
}
|
|
26180
26189
|
})();
|
|
26181
|
-
|
|
26190
|
+
const isAtRoot = realWorkdir === realGitRoot;
|
|
26191
|
+
const isSubdir = realGitRoot && realWorkdir.startsWith(`${realGitRoot}/`);
|
|
26192
|
+
if (!isAtRoot && !isSubdir)
|
|
26182
26193
|
return;
|
|
26183
26194
|
const statusProc = _gitDeps.spawn(["git", "status", "--porcelain"], {
|
|
26184
26195
|
cwd: workdir,
|
|
@@ -26195,7 +26206,8 @@ async function autoCommitIfDirty(workdir, stage, role, storyId) {
|
|
|
26195
26206
|
dirtyFiles: statusOutput.trim().split(`
|
|
26196
26207
|
`).length
|
|
26197
26208
|
});
|
|
26198
|
-
const
|
|
26209
|
+
const addArgs = isSubdir ? ["git", "add", "."] : ["git", "add", "-A"];
|
|
26210
|
+
const addProc = _gitDeps.spawn(addArgs, { cwd: workdir, stdout: "pipe", stderr: "pipe" });
|
|
26199
26211
|
await addProc.exited;
|
|
26200
26212
|
const commitProc = _gitDeps.spawn(["git", "commit", "-m", `chore(${storyId}): auto-commit after ${role} session`], {
|
|
26201
26213
|
cwd: workdir,
|
|
@@ -29440,9 +29452,24 @@ async function runDecompose(story, prd, config2, _workdir, agentGetFn) {
|
|
|
29440
29452
|
if (!agent) {
|
|
29441
29453
|
throw new Error(`[decompose] Agent "${config2.autoMode.defaultAgent}" not found \u2014 cannot decompose`);
|
|
29442
29454
|
}
|
|
29455
|
+
const decomposeTier = naxDecompose?.model ?? "balanced";
|
|
29456
|
+
let decomposeModel;
|
|
29457
|
+
try {
|
|
29458
|
+
const { resolveModel: resolveModel2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
|
|
29459
|
+
const models = config2.models;
|
|
29460
|
+
const entry = models[decomposeTier] ?? models.balanced;
|
|
29461
|
+
if (entry)
|
|
29462
|
+
decomposeModel = resolveModel2(entry).model;
|
|
29463
|
+
} catch {}
|
|
29464
|
+
const storySessionName = `nax-decompose-${story.id.toLowerCase()}`;
|
|
29443
29465
|
const adapter = {
|
|
29444
29466
|
async decompose(prompt) {
|
|
29445
|
-
return agent.complete(prompt, {
|
|
29467
|
+
return agent.complete(prompt, {
|
|
29468
|
+
model: decomposeModel,
|
|
29469
|
+
jsonMode: true,
|
|
29470
|
+
config: config2,
|
|
29471
|
+
sessionName: storySessionName
|
|
29472
|
+
});
|
|
29446
29473
|
}
|
|
29447
29474
|
};
|
|
29448
29475
|
return DecomposeBuilder.for(story).prd(prd).config(builderConfig).decompose(adapter);
|
|
@@ -67305,7 +67332,16 @@ async function planCommand(workdir, config2, options) {
|
|
|
67305
67332
|
const cliAdapter = _deps2.getAgent(agentName);
|
|
67306
67333
|
if (!cliAdapter)
|
|
67307
67334
|
throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
67308
|
-
|
|
67335
|
+
let autoModel;
|
|
67336
|
+
try {
|
|
67337
|
+
const planTier = config2?.plan?.model ?? "balanced";
|
|
67338
|
+
const { resolveModel: resolveModel2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
|
|
67339
|
+
const models = config2?.models;
|
|
67340
|
+
const entry = models?.[planTier] ?? models?.balanced;
|
|
67341
|
+
if (entry)
|
|
67342
|
+
autoModel = resolveModel2(entry).model;
|
|
67343
|
+
} catch {}
|
|
67344
|
+
rawResponse = await cliAdapter.complete(prompt, { model: autoModel, jsonMode: true, workdir, config: config2 });
|
|
67309
67345
|
try {
|
|
67310
67346
|
const envelope = JSON.parse(rawResponse);
|
|
67311
67347
|
if (envelope?.type === "result" && typeof envelope?.result === "string") {
|
package/package.json
CHANGED
|
@@ -108,7 +108,10 @@ IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\
|
|
|
108
108
|
|
|
109
109
|
logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
|
|
110
110
|
|
|
111
|
-
const rawOutput = await _generatorPRDDeps.adapter.complete(prompt, {
|
|
111
|
+
const rawOutput = await _generatorPRDDeps.adapter.complete(prompt, {
|
|
112
|
+
model: options.modelDef.model,
|
|
113
|
+
config: options.config,
|
|
114
|
+
});
|
|
112
115
|
const testCode = extractTestCode(rawOutput);
|
|
113
116
|
|
|
114
117
|
const refinedJsonContent = JSON.stringify(
|
|
@@ -694,8 +694,13 @@ export class AcpAgentAdapter implements AgentAdapter {
|
|
|
694
694
|
|
|
695
695
|
let session: AcpSession | null = null;
|
|
696
696
|
try {
|
|
697
|
-
// complete() is one-shot — ephemeral session, no
|
|
698
|
-
|
|
697
|
+
// complete() is one-shot — ephemeral session, no sidecar
|
|
698
|
+
// Use caller-provided sessionName if available (aids tracing), otherwise timestamp-based
|
|
699
|
+
session = await client.createSession({
|
|
700
|
+
agentName: this.name,
|
|
701
|
+
permissionMode,
|
|
702
|
+
sessionName: _options?.sessionName,
|
|
703
|
+
});
|
|
699
704
|
|
|
700
705
|
// Enforce timeout via Promise.race — session.prompt() can hang indefinitely
|
|
701
706
|
let timeoutId: ReturnType<typeof setTimeout> | undefined;
|
package/src/agents/acp/parser.ts
CHANGED
|
@@ -70,7 +70,7 @@ export function parseAcpxJsonOutput(rawOutput: string): {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
// Final result with token breakdown
|
|
73
|
+
// Final result with token breakdown
|
|
74
74
|
if (event.id !== undefined && event.result && typeof event.result === "object") {
|
|
75
75
|
const result = event.result as Record<string, unknown>;
|
|
76
76
|
|
package/src/agents/types.ts
CHANGED
|
@@ -127,6 +127,12 @@ export interface CompleteOptions {
|
|
|
127
127
|
* Pass when available so complete() honours permissionProfile / dangerouslySkipPermissions.
|
|
128
128
|
*/
|
|
129
129
|
config?: NaxConfig;
|
|
130
|
+
/**
|
|
131
|
+
* Named session to use for this completion call.
|
|
132
|
+
* If omitted, a timestamp-based ephemeral session name is generated.
|
|
133
|
+
* Pass a meaningful name (e.g. "nax-decompose-us-001") to aid debugging.
|
|
134
|
+
*/
|
|
135
|
+
sessionName?: string;
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
/**
|
package/src/cli/plan.ts
CHANGED
|
@@ -138,7 +138,17 @@ export async function planCommand(workdir: string, config: NaxConfig, options: P
|
|
|
138
138
|
const prompt = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails);
|
|
139
139
|
const cliAdapter = _deps.getAgent(agentName);
|
|
140
140
|
if (!cliAdapter) throw new Error(`[plan] No agent adapter found for '${agentName}'`);
|
|
141
|
-
|
|
141
|
+
let autoModel: string | undefined;
|
|
142
|
+
try {
|
|
143
|
+
const planTier = config?.plan?.model ?? "balanced";
|
|
144
|
+
const { resolveModel } = await import("../config/schema");
|
|
145
|
+
const models = config?.models as Record<string, unknown> | undefined;
|
|
146
|
+
const entry = models?.[planTier] ?? models?.balanced;
|
|
147
|
+
if (entry) autoModel = resolveModel(entry as Parameters<typeof resolveModel>[0]).model;
|
|
148
|
+
} catch {
|
|
149
|
+
// fall through — complete() will use its own fallback
|
|
150
|
+
}
|
|
151
|
+
rawResponse = await cliAdapter.complete(prompt, { model: autoModel, jsonMode: true, workdir, config });
|
|
142
152
|
// CLI adapter returns {"type":"result","result":"..."} envelope — unwrap it
|
|
143
153
|
try {
|
|
144
154
|
const envelope = JSON.parse(rawResponse) as Record<string, unknown>;
|
|
@@ -65,9 +65,28 @@ async function runDecompose(
|
|
|
65
65
|
if (!agent) {
|
|
66
66
|
throw new Error(`[decompose] Agent "${config.autoMode.defaultAgent}" not found — cannot decompose`);
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
// Resolve decompose model: config.decompose.model tier → actual model string
|
|
70
|
+
const decomposeTier = naxDecompose?.model ?? "balanced";
|
|
71
|
+
let decomposeModel: string | undefined;
|
|
72
|
+
try {
|
|
73
|
+
const { resolveModel } = await import("../../config/schema");
|
|
74
|
+
const models = config.models as Record<string, unknown>;
|
|
75
|
+
const entry = models[decomposeTier] ?? models.balanced;
|
|
76
|
+
if (entry) decomposeModel = resolveModel(entry as Parameters<typeof resolveModel>[0]).model;
|
|
77
|
+
} catch {
|
|
78
|
+
// resolveModel can throw on malformed entries — fall through to let complete() handle it
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const storySessionName = `nax-decompose-${story.id.toLowerCase()}`;
|
|
68
82
|
const adapter = {
|
|
69
83
|
async decompose(prompt: string): Promise<string> {
|
|
70
|
-
return agent.complete(prompt, {
|
|
84
|
+
return agent.complete(prompt, {
|
|
85
|
+
model: decomposeModel,
|
|
86
|
+
jsonMode: true,
|
|
87
|
+
config,
|
|
88
|
+
sessionName: storySessionName,
|
|
89
|
+
});
|
|
71
90
|
},
|
|
72
91
|
};
|
|
73
92
|
|
package/src/utils/git.ts
CHANGED
|
@@ -181,7 +181,11 @@ export async function autoCommitIfDirty(workdir: string, stage: string, role: st
|
|
|
181
181
|
return gitRoot;
|
|
182
182
|
}
|
|
183
183
|
})();
|
|
184
|
-
|
|
184
|
+
// Allow: workdir IS the git root, or workdir is a subdirectory (monorepo package)
|
|
185
|
+
// Reject: workdir has no git repo at all (realGitRoot would be empty/error)
|
|
186
|
+
const isAtRoot = realWorkdir === realGitRoot;
|
|
187
|
+
const isSubdir = realGitRoot && realWorkdir.startsWith(`${realGitRoot}/`);
|
|
188
|
+
if (!isAtRoot && !isSubdir) return;
|
|
185
189
|
|
|
186
190
|
const statusProc = _gitDeps.spawn(["git", "status", "--porcelain"], {
|
|
187
191
|
cwd: workdir,
|
|
@@ -199,7 +203,11 @@ export async function autoCommitIfDirty(workdir: string, stage: string, role: st
|
|
|
199
203
|
dirtyFiles: statusOutput.trim().split("\n").length,
|
|
200
204
|
});
|
|
201
205
|
|
|
202
|
-
|
|
206
|
+
// Use "git add ." when workdir is a monorepo package subdir — only stages files under
|
|
207
|
+
// that package, preventing accidental cross-package commits.
|
|
208
|
+
// Use "git add -A" at repo root to capture renames/deletions across the full tree.
|
|
209
|
+
const addArgs = isSubdir ? ["git", "add", "."] : ["git", "add", "-A"];
|
|
210
|
+
const addProc = _gitDeps.spawn(addArgs, { cwd: workdir, stdout: "pipe", stderr: "pipe" });
|
|
203
211
|
await addProc.exited;
|
|
204
212
|
|
|
205
213
|
const commitProc = _gitDeps.spawn(["git", "commit", "-m", `chore(${storyId}): auto-commit after ${role} session`], {
|