@nathapp/nax 0.46.0 → 0.46.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,20 @@ 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.46.1] - 2026-03-17
9
+
10
+ ### Fixed
11
+ - **BUG-074:** `working-tree-clean` precheck now allows 12 nax runtime files to be dirty without blocking. Includes fix for `--porcelain` trim bug that corrupted leading status chars.
12
+ - **BUG-074:** `nax init` now adds complete gitignore entries for all nax runtime files (was missing: status.json, plan/, acp-sessions.json, interactions/, progress.txt, acceptance-refined.json, .nax-pids, .nax-wt/, ~/).
13
+ - **BUG-074:** `checkGitignoreCoversNax` warning now checks 6 critical patterns (was only 3).
14
+ - **BUG-075:** `acceptance-refined.json` now written to featureDir instead of workdir root.
15
+ - **BUG-076:** HOME env is now validated before passing to spawned agents — if not an absolute path (e.g. unexpanded "~"), falls back to `os.homedir()` with a warning log. Prevents literal "~/" directory creation in repo.
16
+ - **BUG-076:** New `checkHomeEnvValid()` precheck warning fires when HOME is unset or not absolute.
17
+
18
+ ### Tests
19
+ - New tests in `test/unit/precheck/checks-git.test.ts` (188 lines) for working-tree-clean allowlist.
20
+ - New tests in `test/unit/agents/claude/execution.test.ts` (79 lines) for HOME sanitization.
21
+
8
22
  ## [0.46.0] - 2026-03-16
9
23
 
10
24
  ### Fixed
package/dist/nax.js CHANGED
@@ -3458,7 +3458,46 @@ var init_complete = __esm(() => {
3458
3458
  };
3459
3459
  });
3460
3460
 
3461
- // src/agents/claude/cost.ts
3461
+ // src/agents/cost/pricing.ts
3462
+ var COST_RATES, MODEL_PRICING;
3463
+ var init_pricing = __esm(() => {
3464
+ COST_RATES = {
3465
+ fast: {
3466
+ inputPer1M: 0.8,
3467
+ outputPer1M: 4
3468
+ },
3469
+ balanced: {
3470
+ inputPer1M: 3,
3471
+ outputPer1M: 15
3472
+ },
3473
+ powerful: {
3474
+ inputPer1M: 15,
3475
+ outputPer1M: 75
3476
+ }
3477
+ };
3478
+ MODEL_PRICING = {
3479
+ sonnet: { input: 3, output: 15 },
3480
+ haiku: { input: 0.8, output: 4, cacheRead: 0.1, cacheCreation: 1 },
3481
+ opus: { input: 15, output: 75 },
3482
+ "claude-sonnet-4": { input: 3, output: 15 },
3483
+ "claude-sonnet-4-5": { input: 3, output: 15 },
3484
+ "claude-sonnet-4-6": { input: 3, output: 15 },
3485
+ "claude-haiku": { input: 0.8, output: 4, cacheRead: 0.1, cacheCreation: 1 },
3486
+ "claude-haiku-4-5": { input: 0.8, output: 4, cacheRead: 0.1, cacheCreation: 1 },
3487
+ "claude-opus": { input: 15, output: 75 },
3488
+ "claude-opus-4": { input: 15, output: 75 },
3489
+ "claude-opus-4-6": { input: 15, output: 75 },
3490
+ "gpt-4.1": { input: 10, output: 30 },
3491
+ "gpt-4": { input: 30, output: 60 },
3492
+ "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
3493
+ "gemini-2.5-pro": { input: 0.075, output: 0.3 },
3494
+ "gemini-2-pro": { input: 0.075, output: 0.3 },
3495
+ codex: { input: 0.02, output: 0.06 },
3496
+ "code-davinci-002": { input: 0.02, output: 0.06 }
3497
+ };
3498
+ });
3499
+
3500
+ // src/agents/cost/parse.ts
3462
3501
  function parseTokenUsage(output) {
3463
3502
  try {
3464
3503
  const jsonMatch = output.match(/\{[^}]*"usage"\s*:\s*\{[^}]*"input_tokens"\s*:\s*(\d+)[^}]*"output_tokens"\s*:\s*(\d+)[^}]*\}[^}]*\}/);
@@ -3502,6 +3541,8 @@ function parseTokenUsage(output) {
3502
3541
  }
3503
3542
  return null;
3504
3543
  }
3544
+
3545
+ // src/agents/cost/calculate.ts
3505
3546
  function estimateCost(modelTier, inputTokens, outputTokens, customRates) {
3506
3547
  const rates = customRates ?? COST_RATES[modelTier];
3507
3548
  const inputCost = inputTokens / 1e6 * rates.inputPer1M;
@@ -3543,25 +3584,45 @@ function formatCostWithConfidence(estimate) {
3543
3584
  return `~${formattedCost} (duration-based)`;
3544
3585
  }
3545
3586
  }
3546
- var COST_RATES;
3587
+ function estimateCostFromTokenUsage(usage, model) {
3588
+ const pricing = MODEL_PRICING[model];
3589
+ if (!pricing) {
3590
+ const fallbackInputRate = 3 / 1e6;
3591
+ const fallbackOutputRate = 15 / 1e6;
3592
+ const inputCost2 = (usage.input_tokens ?? 0) * fallbackInputRate;
3593
+ const outputCost2 = (usage.output_tokens ?? 0) * fallbackOutputRate;
3594
+ const cacheReadCost2 = (usage.cache_read_input_tokens ?? 0) * (0.5 / 1e6);
3595
+ const cacheCreationCost2 = (usage.cache_creation_input_tokens ?? 0) * (2 / 1e6);
3596
+ return inputCost2 + outputCost2 + cacheReadCost2 + cacheCreationCost2;
3597
+ }
3598
+ const inputRate = pricing.input / 1e6;
3599
+ const outputRate = pricing.output / 1e6;
3600
+ const cacheReadRate = (pricing.cacheRead ?? pricing.input * 0.1) / 1e6;
3601
+ const cacheCreationRate = (pricing.cacheCreation ?? pricing.input * 0.33) / 1e6;
3602
+ const inputCost = (usage.input_tokens ?? 0) * inputRate;
3603
+ const outputCost = (usage.output_tokens ?? 0) * outputRate;
3604
+ const cacheReadCost = (usage.cache_read_input_tokens ?? 0) * cacheReadRate;
3605
+ const cacheCreationCost = (usage.cache_creation_input_tokens ?? 0) * cacheCreationRate;
3606
+ return inputCost + outputCost + cacheReadCost + cacheCreationCost;
3607
+ }
3608
+ var init_calculate = __esm(() => {
3609
+ init_pricing();
3610
+ });
3611
+
3612
+ // src/agents/cost/index.ts
3547
3613
  var init_cost = __esm(() => {
3548
- COST_RATES = {
3549
- fast: {
3550
- inputPer1M: 0.8,
3551
- outputPer1M: 4
3552
- },
3553
- balanced: {
3554
- inputPer1M: 3,
3555
- outputPer1M: 15
3556
- },
3557
- powerful: {
3558
- inputPer1M: 15,
3559
- outputPer1M: 75
3560
- }
3561
- };
3614
+ init_pricing();
3615
+ init_calculate();
3616
+ });
3617
+
3618
+ // src/agents/claude/cost.ts
3619
+ var init_cost2 = __esm(() => {
3620
+ init_cost();
3562
3621
  });
3563
3622
 
3564
3623
  // src/agents/claude/execution.ts
3624
+ import { homedir } from "os";
3625
+ import { isAbsolute } from "path";
3565
3626
  function buildCommand(binary, options) {
3566
3627
  const model = options.modelDef.model;
3567
3628
  const { skipPermissions } = resolvePermissions(options.config, options.pipelineStage ?? "run");
@@ -3570,12 +3631,19 @@ function buildCommand(binary, options) {
3570
3631
  }
3571
3632
  function buildAllowedEnv(options) {
3572
3633
  const allowed = {};
3573
- const essentialVars = ["PATH", "HOME", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
3634
+ const essentialVars = ["PATH", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
3574
3635
  for (const varName of essentialVars) {
3575
3636
  if (process.env[varName]) {
3576
3637
  allowed[varName] = process.env[varName];
3577
3638
  }
3578
3639
  }
3640
+ const rawHome = process.env.HOME ?? "";
3641
+ const safeHome = rawHome && isAbsolute(rawHome) ? rawHome : homedir();
3642
+ if (rawHome !== safeHome) {
3643
+ const logger = getLogger();
3644
+ logger.warn("env", `HOME env is not absolute ("${rawHome}"), falling back to os.homedir(): ${safeHome}`);
3645
+ }
3646
+ allowed.HOME = safeHome;
3579
3647
  const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"];
3580
3648
  for (const varName of apiKeyVars) {
3581
3649
  if (process.env[varName]) {
@@ -3665,7 +3733,7 @@ async function executeOnce(binary, options, pidRegistry) {
3665
3733
  var MAX_AGENT_OUTPUT_CHARS = 5000, MAX_AGENT_STDERR_CHARS = 1000, SIGKILL_GRACE_PERIOD_MS = 5000, _runOnceDeps;
3666
3734
  var init_execution = __esm(() => {
3667
3735
  init_logger2();
3668
- init_cost();
3736
+ init_cost2();
3669
3737
  _runOnceDeps = {
3670
3738
  killProc(proc, signal) {
3671
3739
  proc.kill(signal);
@@ -18671,7 +18739,7 @@ Respond with ONLY the TypeScript test code (no markdown code fences, no explanat
18671
18739
  testable: c.testable,
18672
18740
  storyId: c.storyId
18673
18741
  })), null, 2);
18674
- await _generatorPRDDeps.writeFile(join2(options.workdir, "acceptance-refined.json"), refinedJsonContent);
18742
+ await _generatorPRDDeps.writeFile(join2(options.featureDir, "acceptance-refined.json"), refinedJsonContent);
18675
18743
  return { testCode, criteria };
18676
18744
  }
18677
18745
  function buildStrategyInstructions(strategy, framework) {
@@ -19047,13 +19115,21 @@ function parseAcpxJsonOutput(rawOutput) {
19047
19115
  }
19048
19116
 
19049
19117
  // src/agents/acp/spawn-client.ts
19118
+ import { homedir as homedir2 } from "os";
19119
+ import { isAbsolute as isAbsolute2 } from "path";
19050
19120
  function buildAllowedEnv2(extraEnv) {
19051
19121
  const allowed = {};
19052
- const essentialVars = ["PATH", "HOME", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
19122
+ const essentialVars = ["PATH", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
19053
19123
  for (const varName of essentialVars) {
19054
19124
  if (process.env[varName])
19055
19125
  allowed[varName] = process.env[varName];
19056
19126
  }
19127
+ const rawHome = process.env.HOME ?? "";
19128
+ const safeHome = rawHome && isAbsolute2(rawHome) ? rawHome : homedir2();
19129
+ if (rawHome !== safeHome) {
19130
+ getSafeLogger()?.warn("env", `HOME env is not absolute ("${rawHome}"), falling back to os.homedir(): ${safeHome}`);
19131
+ }
19132
+ allowed.HOME = safeHome;
19057
19133
  const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "CLAUDE_API_KEY"];
19058
19134
  for (const varName of apiKeyVars) {
19059
19135
  if (process.env[varName])
@@ -19270,44 +19346,8 @@ var init_spawn_client = __esm(() => {
19270
19346
  });
19271
19347
 
19272
19348
  // src/agents/acp/cost.ts
19273
- function estimateCostFromTokenUsage(usage, model) {
19274
- const pricing = MODEL_PRICING[model];
19275
- if (!pricing) {
19276
- const fallbackInputRate = 3 / 1e6;
19277
- const fallbackOutputRate = 15 / 1e6;
19278
- const inputCost2 = (usage.input_tokens ?? 0) * fallbackInputRate;
19279
- const outputCost2 = (usage.output_tokens ?? 0) * fallbackOutputRate;
19280
- const cacheReadCost2 = (usage.cache_read_input_tokens ?? 0) * (0.5 / 1e6);
19281
- const cacheCreationCost2 = (usage.cache_creation_input_tokens ?? 0) * (2 / 1e6);
19282
- return inputCost2 + outputCost2 + cacheReadCost2 + cacheCreationCost2;
19283
- }
19284
- const inputRate = pricing.input / 1e6;
19285
- const outputRate = pricing.output / 1e6;
19286
- const cacheReadRate = (pricing.cacheRead ?? pricing.input * 0.1) / 1e6;
19287
- const cacheCreationRate = (pricing.cacheCreation ?? pricing.input * 0.33) / 1e6;
19288
- const inputCost = (usage.input_tokens ?? 0) * inputRate;
19289
- const outputCost = (usage.output_tokens ?? 0) * outputRate;
19290
- const cacheReadCost = (usage.cache_read_input_tokens ?? 0) * cacheReadRate;
19291
- const cacheCreationCost = (usage.cache_creation_input_tokens ?? 0) * cacheCreationRate;
19292
- return inputCost + outputCost + cacheReadCost + cacheCreationCost;
19293
- }
19294
- var MODEL_PRICING;
19295
- var init_cost2 = __esm(() => {
19296
- MODEL_PRICING = {
19297
- "claude-sonnet-4": { input: 3, output: 15 },
19298
- "claude-sonnet-4-5": { input: 3, output: 15 },
19299
- "claude-haiku": { input: 0.8, output: 4, cacheRead: 0.1, cacheCreation: 1 },
19300
- "claude-haiku-4-5": { input: 0.8, output: 4, cacheRead: 0.1, cacheCreation: 1 },
19301
- "claude-opus": { input: 15, output: 75 },
19302
- "claude-opus-4": { input: 15, output: 75 },
19303
- "gpt-4.1": { input: 10, output: 30 },
19304
- "gpt-4": { input: 30, output: 60 },
19305
- "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
19306
- "gemini-2.5-pro": { input: 0.075, output: 0.3 },
19307
- "gemini-2-pro": { input: 0.075, output: 0.3 },
19308
- codex: { input: 0.02, output: 0.06 },
19309
- "code-davinci-002": { input: 0.02, output: 0.06 }
19310
- };
19349
+ var init_cost3 = __esm(() => {
19350
+ init_cost();
19311
19351
  });
19312
19352
 
19313
19353
  // src/agents/acp/adapter.ts
@@ -19828,7 +19868,7 @@ var init_adapter2 = __esm(() => {
19828
19868
  init_decompose();
19829
19869
  init_spawn_client();
19830
19870
  init_types2();
19831
- init_cost2();
19871
+ init_cost3();
19832
19872
  INTERACTION_TIMEOUT_MS = 5 * 60 * 1000;
19833
19873
  AGENT_REGISTRY = {
19834
19874
  claude: {
@@ -20427,7 +20467,7 @@ var init_chain = __esm(() => {
20427
20467
 
20428
20468
  // src/utils/path-security.ts
20429
20469
  import { realpathSync } from "fs";
20430
- import { dirname, isAbsolute, join as join5, normalize, resolve } from "path";
20470
+ import { dirname, isAbsolute as isAbsolute3, join as join5, normalize, resolve } from "path";
20431
20471
  function safeRealpath(p) {
20432
20472
  try {
20433
20473
  return realpathSync(p);
@@ -20445,7 +20485,7 @@ function validateModulePath(modulePath, allowedRoots) {
20445
20485
  return { valid: false, error: "Module path is empty" };
20446
20486
  }
20447
20487
  const normalizedRoots = allowedRoots.map((r) => safeRealpath(resolve(r)));
20448
- if (isAbsolute(modulePath)) {
20488
+ if (isAbsolute3(modulePath)) {
20449
20489
  const absoluteTarget = safeRealpath(normalize(modulePath));
20450
20490
  const isWithin = normalizedRoots.some((root) => {
20451
20491
  return absoluteTarget.startsWith(`${root}/`) || absoluteTarget === root;
@@ -20736,7 +20776,7 @@ function isPlainObject2(value) {
20736
20776
 
20737
20777
  // src/config/path-security.ts
20738
20778
  import { existsSync as existsSync4, lstatSync, realpathSync as realpathSync2 } from "fs";
20739
- import { isAbsolute as isAbsolute2, normalize as normalize2, resolve as resolve3 } from "path";
20779
+ import { isAbsolute as isAbsolute4, normalize as normalize2, resolve as resolve3 } from "path";
20740
20780
  function validateDirectory(dirPath, baseDir) {
20741
20781
  const resolved = resolve3(dirPath);
20742
20782
  if (!existsSync4(resolved)) {
@@ -20768,7 +20808,7 @@ function validateDirectory(dirPath, baseDir) {
20768
20808
  function isWithinDirectory(targetPath, basePath) {
20769
20809
  const normalizedTarget = normalize2(targetPath);
20770
20810
  const normalizedBase = normalize2(basePath);
20771
- if (!isAbsolute2(normalizedTarget) || !isAbsolute2(normalizedBase)) {
20811
+ if (!isAbsolute4(normalizedTarget) || !isAbsolute4(normalizedBase)) {
20772
20812
  return false;
20773
20813
  }
20774
20814
  const baseWithSlash = normalizedBase.endsWith("/") ? normalizedBase : `${normalizedBase}/`;
@@ -20804,10 +20844,10 @@ var MAX_DIRECTORY_DEPTH = 10;
20804
20844
  var init_path_security2 = () => {};
20805
20845
 
20806
20846
  // src/config/paths.ts
20807
- import { homedir } from "os";
20847
+ import { homedir as homedir3 } from "os";
20808
20848
  import { join as join6, resolve as resolve4 } from "path";
20809
20849
  function globalConfigDir() {
20810
- return join6(homedir(), ".nax");
20850
+ return join6(homedir3(), ".nax");
20811
20851
  }
20812
20852
  var init_paths = () => {};
20813
20853
 
@@ -22138,7 +22178,7 @@ var package_default;
22138
22178
  var init_package = __esm(() => {
22139
22179
  package_default = {
22140
22180
  name: "@nathapp/nax",
22141
- version: "0.46.0",
22181
+ version: "0.46.1",
22142
22182
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22143
22183
  type: "module",
22144
22184
  bin: {
@@ -22211,8 +22251,8 @@ var init_version = __esm(() => {
22211
22251
  NAX_VERSION = package_default.version;
22212
22252
  NAX_COMMIT = (() => {
22213
22253
  try {
22214
- if (/^[0-9a-f]{6,10}$/.test("6a485b9"))
22215
- return "6a485b9";
22254
+ if (/^[0-9a-f]{6,10}$/.test("405c88a"))
22255
+ return "405c88a";
22216
22256
  } catch {}
22217
22257
  try {
22218
22258
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -24018,6 +24058,7 @@ ${stderr}` };
24018
24058
  const result = await _acceptanceSetupDeps.generate(ctx.prd.userStories, refinedCriteria, {
24019
24059
  featureName: ctx.prd.feature,
24020
24060
  workdir: ctx.workdir,
24061
+ featureDir: ctx.featureDir,
24021
24062
  codebaseContext: "",
24022
24063
  modelTier: ctx.config.acceptance.model ?? "fast",
24023
24064
  modelDef: resolveModel(ctx.config.models[ctx.config.acceptance.model ?? "fast"]),
@@ -25752,11 +25793,13 @@ __export(exports_agents, {
25752
25793
  getAgentVersion: () => getAgentVersion,
25753
25794
  getAgent: () => getAgent,
25754
25795
  formatCostWithConfidence: () => formatCostWithConfidence,
25796
+ estimateCostFromTokenUsage: () => estimateCostFromTokenUsage,
25755
25797
  estimateCostFromOutput: () => estimateCostFromOutput,
25756
25798
  estimateCostByDuration: () => estimateCostByDuration,
25757
25799
  estimateCost: () => estimateCost,
25758
25800
  describeAgentCapabilities: () => describeAgentCapabilities,
25759
25801
  checkAgentHealth: () => checkAgentHealth,
25802
+ MODEL_PRICING: () => MODEL_PRICING,
25760
25803
  CompleteError: () => CompleteError,
25761
25804
  ClaudeCodeAdapter: () => ClaudeCodeAdapter,
25762
25805
  COST_RATES: () => COST_RATES
@@ -30069,12 +30112,15 @@ async function checkWorkingTreeClean(workdir) {
30069
30112
  });
30070
30113
  const output = await new Response(proc.stdout).text();
30071
30114
  const exitCode = await proc.exited;
30072
- const passed = exitCode === 0 && output.trim() === "";
30115
+ const lines = output.trim() === "" ? [] : output.split(`
30116
+ `).filter(Boolean);
30117
+ const nonNaxDirtyFiles = lines.filter((line) => !NAX_RUNTIME_PATTERNS.some((pattern) => pattern.test(line)));
30118
+ const passed = exitCode === 0 && nonNaxDirtyFiles.length === 0;
30073
30119
  return {
30074
30120
  name: "working-tree-clean",
30075
30121
  tier: "blocker",
30076
30122
  passed,
30077
- message: passed ? "Working tree is clean" : "Uncommitted changes detected"
30123
+ message: passed ? "Working tree is clean" : `Uncommitted changes detected: ${nonNaxDirtyFiles.map((l) => l.slice(3)).join(", ")}`
30078
30124
  };
30079
30125
  }
30080
30126
  async function checkGitUserConfigured(workdir) {
@@ -30099,7 +30145,23 @@ async function checkGitUserConfigured(workdir) {
30099
30145
  message: passed ? "Git user is configured" : !hasName && !hasEmail ? "Git user.name and user.email not configured" : !hasName ? "Git user.name not configured" : "Git user.email not configured"
30100
30146
  };
30101
30147
  }
30102
- var init_checks_git = () => {};
30148
+ var NAX_RUNTIME_PATTERNS;
30149
+ var init_checks_git = __esm(() => {
30150
+ NAX_RUNTIME_PATTERNS = [
30151
+ /^.{2} nax\.lock$/,
30152
+ /^.{2} nax\/metrics\.json$/,
30153
+ /^.{2} nax\/features\/[^/]+\/status\.json$/,
30154
+ /^.{2} nax\/features\/[^/]+\/runs\//,
30155
+ /^.{2} nax\/features\/[^/]+\/plan\//,
30156
+ /^.{2} nax\/features\/[^/]+\/acp-sessions\.json$/,
30157
+ /^.{2} nax\/features\/[^/]+\/interactions\//,
30158
+ /^.{2} nax\/features\/[^/]+\/progress\.txt$/,
30159
+ /^.{2} nax\/features\/[^/]+\/acceptance-refined\.json$/,
30160
+ /^.{2} \.nax-verifier-verdict\.json$/,
30161
+ /^.{2} \.nax-pids$/,
30162
+ /^.{2} \.nax-wt\//
30163
+ ];
30164
+ });
30103
30165
 
30104
30166
  // src/precheck/checks-config.ts
30105
30167
  import { existsSync as existsSync26, statSync as statSync3 } from "fs";
@@ -30360,6 +30422,7 @@ var init_checks_blockers = __esm(() => {
30360
30422
 
30361
30423
  // src/precheck/checks-warnings.ts
30362
30424
  import { existsSync as existsSync28 } from "fs";
30425
+ import { isAbsolute as isAbsolute6 } from "path";
30363
30426
  async function checkClaudeMdExists(workdir) {
30364
30427
  const claudeMdPath = `${workdir}/CLAUDE.md`;
30365
30428
  const passed = existsSync28(claudeMdPath);
@@ -30453,7 +30516,14 @@ async function checkGitignoreCoversNax(workdir) {
30453
30516
  }
30454
30517
  const file2 = Bun.file(gitignorePath);
30455
30518
  const content = await file2.text();
30456
- const patterns = ["nax.lock", "runs/", "test/tmp/"];
30519
+ const patterns = [
30520
+ "nax.lock",
30521
+ "nax/**/runs/",
30522
+ "nax/metrics.json",
30523
+ "nax/features/*/status.json",
30524
+ ".nax-pids",
30525
+ ".nax-wt/"
30526
+ ];
30457
30527
  const missing = patterns.filter((pattern) => !content.includes(pattern));
30458
30528
  const passed = missing.length === 0;
30459
30529
  return {
@@ -30482,6 +30552,16 @@ async function checkPromptOverrideFiles(config2, workdir) {
30482
30552
  }
30483
30553
  return checks3;
30484
30554
  }
30555
+ async function checkHomeEnvValid() {
30556
+ const home = process.env.HOME ?? "";
30557
+ const passed = home !== "" && isAbsolute6(home);
30558
+ return {
30559
+ name: "home-env-valid",
30560
+ tier: "warning",
30561
+ passed,
30562
+ message: passed ? `HOME env is valid: ${home}` : home === "" ? "HOME env is not set \u2014 agent may write files to unexpected locations" : `HOME env is not an absolute path ("${home}") \u2014 may cause literal "~" directories in repo`
30563
+ };
30564
+ }
30485
30565
  var init_checks_warnings = () => {};
30486
30566
 
30487
30567
  // src/precheck/checks-agents.ts
@@ -30662,6 +30742,7 @@ function getEnvironmentWarnings(config2, workdir) {
30662
30742
  () => checkDiskSpace(),
30663
30743
  () => checkOptionalCommands(config2, workdir),
30664
30744
  () => checkGitignoreCoversNax(workdir),
30745
+ () => checkHomeEnvValid(),
30665
30746
  () => checkPromptOverrideFiles(config2, workdir),
30666
30747
  () => checkMultiAgentHealth()
30667
30748
  ];
@@ -32902,12 +32983,12 @@ var init_parallel_executor = __esm(() => {
32902
32983
 
32903
32984
  // src/pipeline/subscribers/events-writer.ts
32904
32985
  import { appendFile as appendFile2, mkdir } from "fs/promises";
32905
- import { homedir as homedir5 } from "os";
32986
+ import { homedir as homedir7 } from "os";
32906
32987
  import { basename as basename3, join as join40 } from "path";
32907
32988
  function wireEventsWriter(bus, feature, runId, workdir) {
32908
32989
  const logger = getSafeLogger();
32909
32990
  const project = basename3(workdir);
32910
- const eventsDir = join40(homedir5(), ".nax", "events", project);
32991
+ const eventsDir = join40(homedir7(), ".nax", "events", project);
32911
32992
  const eventsFile = join40(eventsDir, "events.jsonl");
32912
32993
  let dirReady = false;
32913
32994
  const write = (line) => {
@@ -33067,12 +33148,12 @@ var init_interaction2 = __esm(() => {
33067
33148
 
33068
33149
  // src/pipeline/subscribers/registry.ts
33069
33150
  import { mkdir as mkdir2, writeFile } from "fs/promises";
33070
- import { homedir as homedir6 } from "os";
33151
+ import { homedir as homedir8 } from "os";
33071
33152
  import { basename as basename4, join as join41 } from "path";
33072
33153
  function wireRegistry(bus, feature, runId, workdir) {
33073
33154
  const logger = getSafeLogger();
33074
33155
  const project = basename4(workdir);
33075
- const runDir = join41(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
33156
+ const runDir = join41(homedir8(), ".nax", "runs", `${project}-${feature}-${runId}`);
33076
33157
  const metaFile = join41(runDir, "meta.json");
33077
33158
  const unsub = bus.on("run:started", (_ev) => {
33078
33159
  (async () => {
@@ -65472,7 +65553,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
65472
65553
  // bin/nax.ts
65473
65554
  init_source();
65474
65555
  import { existsSync as existsSync32, mkdirSync as mkdirSync6 } from "fs";
65475
- import { homedir as homedir8 } from "os";
65556
+ import { homedir as homedir10 } from "os";
65476
65557
  import { join as join43 } from "path";
65477
65558
 
65478
65559
  // node_modules/commander/esm.mjs
@@ -68628,10 +68709,10 @@ import { join as join32 } from "path";
68628
68709
  // src/commands/logs-reader.ts
68629
68710
  import { existsSync as existsSync23, readdirSync as readdirSync6 } from "fs";
68630
68711
  import { readdir as readdir3 } from "fs/promises";
68631
- import { homedir as homedir3 } from "os";
68712
+ import { homedir as homedir5 } from "os";
68632
68713
  import { join as join31 } from "path";
68633
68714
  var _deps6 = {
68634
- getRunsDir: () => process.env.NAX_RUNS_DIR ?? join31(homedir3(), ".nax", "runs")
68715
+ getRunsDir: () => process.env.NAX_RUNS_DIR ?? join31(homedir5(), ".nax", "runs")
68635
68716
  };
68636
68717
  async function resolveRunFileFromRegistry(runId) {
68637
68718
  const runsDir = _deps6.getRunsDir();
@@ -68957,11 +69038,11 @@ async function precheckCommand(options) {
68957
69038
  // src/commands/runs.ts
68958
69039
  init_source();
68959
69040
  import { readdir as readdir4 } from "fs/promises";
68960
- import { homedir as homedir4 } from "os";
69041
+ import { homedir as homedir6 } from "os";
68961
69042
  import { join as join35 } from "path";
68962
69043
  var DEFAULT_LIMIT = 20;
68963
69044
  var _deps8 = {
68964
- getRunsDir: () => join35(homedir4(), ".nax", "runs")
69045
+ getRunsDir: () => join35(homedir6(), ".nax", "runs")
68965
69046
  };
68966
69047
  function formatDuration3(ms) {
68967
69048
  if (ms <= 0)
@@ -77155,7 +77236,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
77155
77236
  config2.autoMode.defaultAgent = options.agent;
77156
77237
  }
77157
77238
  config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
77158
- const globalNaxDir = join43(homedir8(), ".nax");
77239
+ const globalNaxDir = join43(homedir10(), ".nax");
77159
77240
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
77160
77241
  const eventEmitter = new PipelineEventEmitter;
77161
77242
  let tuiInstance;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.46.0",
3
+ "version": "0.46.1",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -124,7 +124,7 @@ Respond with ONLY the TypeScript test code (no markdown code fences, no explanat
124
124
  2,
125
125
  );
126
126
 
127
- await _generatorPRDDeps.writeFile(join(options.workdir, "acceptance-refined.json"), refinedJsonContent);
127
+ await _generatorPRDDeps.writeFile(join(options.featureDir, "acceptance-refined.json"), refinedJsonContent);
128
128
 
129
129
  return { testCode, criteria };
130
130
  }
@@ -80,6 +80,8 @@ export interface GenerateFromPRDOptions {
80
80
  featureName: string;
81
81
  /** Working directory for context scanning */
82
82
  workdir: string;
83
+ /** Feature directory where acceptance-refined.json is written */
84
+ featureDir: string;
83
85
  /** Codebase context (file tree, dependencies, test patterns) */
84
86
  codebaseContext: string;
85
87
  /** Model tier to use for test generation */
@@ -1,79 +1,9 @@
1
1
  /**
2
- * ACP cost estimation from token usage.
2
+ * ACP cost estimation — re-exports from the shared src/agents/cost/ module.
3
3
  *
4
- * Stub implementation in ACP-006.
4
+ * Kept for zero-breakage backward compatibility.
5
+ * Import directly from src/agents/cost for new code.
5
6
  */
6
7
 
7
- /**
8
- * Token usage data from an ACP session's cumulative_token_usage field.
9
- */
10
- export interface SessionTokenUsage {
11
- input_tokens: number;
12
- output_tokens: number;
13
- /** Cache read tokens — billed at a reduced rate */
14
- cache_read_input_tokens?: number;
15
- /** Cache creation tokens — billed at a higher creation rate */
16
- cache_creation_input_tokens?: number;
17
- }
18
-
19
- /**
20
- * Per-model pricing in $/1M tokens: { input, output }
21
- */
22
- const MODEL_PRICING: Record<string, { input: number; output: number; cacheRead?: number; cacheCreation?: number }> = {
23
- // Anthropic Claude models
24
- "claude-sonnet-4": { input: 3, output: 15 },
25
- "claude-sonnet-4-5": { input: 3, output: 15 },
26
- "claude-haiku": { input: 0.8, output: 4.0, cacheRead: 0.1, cacheCreation: 1.0 },
27
- "claude-haiku-4-5": { input: 0.8, output: 4.0, cacheRead: 0.1, cacheCreation: 1.0 },
28
- "claude-opus": { input: 15, output: 75 },
29
- "claude-opus-4": { input: 15, output: 75 },
30
-
31
- // OpenAI models
32
- "gpt-4.1": { input: 10, output: 30 },
33
- "gpt-4": { input: 30, output: 60 },
34
- "gpt-3.5-turbo": { input: 0.5, output: 1.5 },
35
-
36
- // Google Gemini
37
- "gemini-2.5-pro": { input: 0.075, output: 0.3 },
38
- "gemini-2-pro": { input: 0.075, output: 0.3 },
39
-
40
- // OpenAI Codex
41
- codex: { input: 0.02, output: 0.06 },
42
- "code-davinci-002": { input: 0.02, output: 0.06 },
43
- };
44
-
45
- /**
46
- * Calculate USD cost from ACP session token counts using per-model pricing.
47
- *
48
- * @param usage - Token counts from cumulative_token_usage
49
- * @param model - Model identifier (e.g., 'claude-sonnet-4', 'claude-haiku-4-5')
50
- * @returns Estimated cost in USD
51
- */
52
- export function estimateCostFromTokenUsage(usage: SessionTokenUsage, model: string): number {
53
- const pricing = MODEL_PRICING[model];
54
-
55
- if (!pricing) {
56
- // Fallback: use average rate for unknown models
57
- // Average of known rates: ~$5/1M tokens combined
58
- const fallbackInputRate = 3 / 1_000_000;
59
- const fallbackOutputRate = 15 / 1_000_000;
60
- const inputCost = (usage.input_tokens ?? 0) * fallbackInputRate;
61
- const outputCost = (usage.output_tokens ?? 0) * fallbackOutputRate;
62
- const cacheReadCost = (usage.cache_read_input_tokens ?? 0) * (0.5 / 1_000_000);
63
- const cacheCreationCost = (usage.cache_creation_input_tokens ?? 0) * (2 / 1_000_000);
64
- return inputCost + outputCost + cacheReadCost + cacheCreationCost;
65
- }
66
-
67
- // Convert $/1M rates to $/token
68
- const inputRate = pricing.input / 1_000_000;
69
- const outputRate = pricing.output / 1_000_000;
70
- const cacheReadRate = (pricing.cacheRead ?? pricing.input * 0.1) / 1_000_000;
71
- const cacheCreationRate = (pricing.cacheCreation ?? pricing.input * 0.33) / 1_000_000;
72
-
73
- const inputCost = (usage.input_tokens ?? 0) * inputRate;
74
- const outputCost = (usage.output_tokens ?? 0) * outputRate;
75
- const cacheReadCost = (usage.cache_read_input_tokens ?? 0) * cacheReadRate;
76
- const cacheCreationCost = (usage.cache_creation_input_tokens ?? 0) * cacheCreationRate;
77
-
78
- return inputCost + outputCost + cacheReadCost + cacheCreationCost;
79
- }
8
+ export type { SessionTokenUsage } from "../cost";
9
+ export { estimateCostFromTokenUsage } from "../cost";
@@ -12,6 +12,8 @@
12
12
  * acpx <agent> cancel → session.cancelActivePrompt()
13
13
  */
14
14
 
15
+ import { homedir } from "node:os";
16
+ import { isAbsolute } from "node:path";
15
17
  import type { PidRegistry } from "../../execution/pid-registry";
16
18
  import { getSafeLogger } from "../../logger";
17
19
  import type { AcpClient, AcpSession, AcpSessionResponse } from "./adapter";
@@ -60,11 +62,19 @@ export const _spawnClientDeps = {
60
62
  function buildAllowedEnv(extraEnv?: Record<string, string | undefined>): Record<string, string | undefined> {
61
63
  const allowed: Record<string, string | undefined> = {};
62
64
 
63
- const essentialVars = ["PATH", "HOME", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
65
+ const essentialVars = ["PATH", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
64
66
  for (const varName of essentialVars) {
65
67
  if (process.env[varName]) allowed[varName] = process.env[varName];
66
68
  }
67
69
 
70
+ // Sanitize HOME — must be absolute path. Unexpanded "~" causes literal ~/dir in cwd.
71
+ const rawHome = process.env.HOME ?? "";
72
+ const safeHome = rawHome && isAbsolute(rawHome) ? rawHome : homedir();
73
+ if (rawHome !== safeHome) {
74
+ getSafeLogger()?.warn("env", `HOME env is not absolute ("${rawHome}"), falling back to os.homedir(): ${safeHome}`);
75
+ }
76
+ allowed.HOME = safeHome;
77
+
68
78
  const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "CLAUDE_API_KEY"];
69
79
  for (const varName of apiKeyVars) {
70
80
  if (process.env[varName]) allowed[varName] = process.env[varName];