@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 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, { config: options.config });
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({ agentName: this.name, permissionMode });
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.0",
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("6a5bc7a"))
22318
- return "6a5bc7a";
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
- if (realWorkdir !== realGitRoot)
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 addProc = _gitDeps.spawn(["git", "add", "-A"], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
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, { jsonMode: true, config: config2 });
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
- rawResponse = await cliAdapter.complete(prompt, { jsonMode: true, workdir, config: config2 });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.49.0",
3
+ "version": "0.49.1",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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, { config: options.config });
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 session name, no sidecar
698
- session = await client.createSession({ agentName: this.name, permissionMode });
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;
@@ -70,7 +70,7 @@ export function parseAcpxJsonOutput(rawOutput: string): {
70
70
  }
71
71
  }
72
72
 
73
- // Final result with token breakdown (camelCase from acpx)
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
 
@@ -135,6 +135,8 @@ class SpawnAcpSession implements AcpSession {
135
135
  "acpx",
136
136
  "--cwd",
137
137
  this.cwd,
138
+ "--format",
139
+ "json",
138
140
  ...(this.permissionMode === "approve-all" ? ["--approve-all"] : []),
139
141
  "--model",
140
142
  this.model,
@@ -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
- rawResponse = await cliAdapter.complete(prompt, { jsonMode: true, workdir, config });
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, { jsonMode: true, config });
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
- if (realWorkdir !== realGitRoot) return;
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
- const addProc = _gitDeps.spawn(["git", "add", "-A"], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
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`], {