@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 +14 -0
- package/dist/nax.js +163 -82
- package/package.json +1 -1
- package/src/acceptance/generator.ts +1 -1
- package/src/acceptance/types.ts +2 -0
- package/src/agents/acp/cost.ts +5 -75
- package/src/agents/acp/spawn-client.ts +11 -1
- package/src/agents/claude/cost.ts +12 -264
- package/src/agents/claude/execution.ts +12 -1
- package/src/agents/cost/calculate.ts +154 -0
- package/src/agents/cost/index.ts +10 -0
- package/src/agents/cost/parse.ts +97 -0
- package/src/agents/cost/pricing.ts +59 -0
- package/src/agents/cost/types.ts +45 -0
- package/src/agents/index.ts +4 -2
- package/src/agents/types.ts +3 -0
- package/src/cli/init.ts +15 -1
- package/src/pipeline/stages/acceptance-setup.ts +1 -0
- package/src/precheck/checks-git.ts +28 -2
- package/src/precheck/checks-warnings.ts +30 -2
- package/src/precheck/checks.ts +1 -0
- package/src/precheck/index.ts +2 -0
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/
|
|
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
|
-
|
|
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
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
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", "
|
|
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
|
-
|
|
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.
|
|
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", "
|
|
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
|
-
|
|
19274
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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 (!
|
|
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(
|
|
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.
|
|
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("
|
|
22215
|
-
return "
|
|
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
|
|
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" :
|
|
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
|
|
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 = [
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
@@ -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.
|
|
127
|
+
await _generatorPRDDeps.writeFile(join(options.featureDir, "acceptance-refined.json"), refinedJsonContent);
|
|
128
128
|
|
|
129
129
|
return { testCode, criteria };
|
|
130
130
|
}
|
package/src/acceptance/types.ts
CHANGED
|
@@ -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 */
|
package/src/agents/acp/cost.ts
CHANGED
|
@@ -1,79 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ACP cost estimation from
|
|
2
|
+
* ACP cost estimation — re-exports from the shared src/agents/cost/ module.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Kept for zero-breakage backward compatibility.
|
|
5
|
+
* Import directly from src/agents/cost for new code.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
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", "
|
|
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];
|