@nathapp/nax 0.54.0 → 0.54.2
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/README.md +4 -2
- package/dist/nax.js +335 -212
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,7 +12,9 @@ nax is an **orchestrator, not an agent** — it doesn't write code itself. It dr
|
|
|
12
12
|
- **TDD-enforced** — acceptance tests must fail before implementation starts
|
|
13
13
|
- **Loop until done** — verify, retry, escalate, and regression-check automatically
|
|
14
14
|
- **Monorepo-ready** — per-package config and per-story working directories
|
|
15
|
-
- **Extensible** — plugin system for routing, review, and
|
|
15
|
+
- **Extensible** — plugin system for routing, review, reporting, and post-run actions
|
|
16
|
+
- **Language-aware** — auto-detects Go, Rust, Python, TypeScript from manifest files; adapts commands, test structure, and mocking patterns per language
|
|
17
|
+
- **Semantic review** — LLM-based behavioral review against story acceptance criteria; catches stubs, placeholders, and out-of-scope changes
|
|
16
18
|
|
|
17
19
|
## Install
|
|
18
20
|
|
|
@@ -160,7 +162,7 @@ See [Hooks Guide](docs/guides/hooks.md).
|
|
|
160
162
|
|
|
161
163
|
### Plugins
|
|
162
164
|
|
|
163
|
-
Extensible plugin architecture for prompt optimization, custom routing, code review, and reporting. Plugins live in `.nax/plugins/` (project) or `~/.nax/plugins/` (global).
|
|
165
|
+
Extensible plugin architecture for prompt optimization, custom routing, code review, and reporting. Plugins live in `.nax/plugins/` (project) or `~/.nax/plugins/` (global). Post-run action plugins (e.g. auto-PR creation) can implement `IPostRunAction` for results-aware post-completion workflows.
|
|
164
166
|
|
|
165
167
|
See [Plugins Guide](docs/guides/agents.md#plugins).
|
|
166
168
|
|
package/dist/nax.js
CHANGED
|
@@ -3580,6 +3580,44 @@ var init_complete = __esm(() => {
|
|
|
3580
3580
|
};
|
|
3581
3581
|
});
|
|
3582
3582
|
|
|
3583
|
+
// src/agents/shared/env.ts
|
|
3584
|
+
import { homedir } from "os";
|
|
3585
|
+
import { isAbsolute } from "path";
|
|
3586
|
+
function buildAllowedEnv(options) {
|
|
3587
|
+
const allowed = {};
|
|
3588
|
+
for (const varName of ESSENTIAL_VARS) {
|
|
3589
|
+
if (process.env[varName])
|
|
3590
|
+
allowed[varName] = process.env[varName];
|
|
3591
|
+
}
|
|
3592
|
+
const rawHome = process.env.HOME ?? "";
|
|
3593
|
+
const safeHome = rawHome && isAbsolute(rawHome) ? rawHome : homedir();
|
|
3594
|
+
if (rawHome !== safeHome) {
|
|
3595
|
+
getSafeLogger()?.warn("env", `HOME env is not absolute ("${rawHome}"), falling back to os.homedir(): ${safeHome}`);
|
|
3596
|
+
}
|
|
3597
|
+
allowed.HOME = safeHome;
|
|
3598
|
+
for (const varName of API_KEY_VARS) {
|
|
3599
|
+
if (process.env[varName])
|
|
3600
|
+
allowed[varName] = process.env[varName];
|
|
3601
|
+
}
|
|
3602
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
3603
|
+
if (ALLOWED_PREFIXES.some((prefix) => key.startsWith(prefix))) {
|
|
3604
|
+
allowed[key] = value;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
if (options?.modelEnv)
|
|
3608
|
+
Object.assign(allowed, options.modelEnv);
|
|
3609
|
+
if (options?.env)
|
|
3610
|
+
Object.assign(allowed, options.env);
|
|
3611
|
+
return allowed;
|
|
3612
|
+
}
|
|
3613
|
+
var ESSENTIAL_VARS, API_KEY_VARS, ALLOWED_PREFIXES;
|
|
3614
|
+
var init_env = __esm(() => {
|
|
3615
|
+
init_logger2();
|
|
3616
|
+
ESSENTIAL_VARS = ["PATH", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
|
|
3617
|
+
API_KEY_VARS = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "CLAUDE_API_KEY"];
|
|
3618
|
+
ALLOWED_PREFIXES = ["CLAUDE_", "NAX_", "CLAW_", "TURBO_", "ACPX_", "CODEX_", "GEMINI_", "ANTHROPIC_"];
|
|
3619
|
+
});
|
|
3620
|
+
|
|
3583
3621
|
// src/agents/cost/pricing.ts
|
|
3584
3622
|
var COST_RATES, MODEL_PRICING;
|
|
3585
3623
|
var init_pricing = __esm(() => {
|
|
@@ -3743,49 +3781,12 @@ var init_cost2 = __esm(() => {
|
|
|
3743
3781
|
});
|
|
3744
3782
|
|
|
3745
3783
|
// src/agents/claude/execution.ts
|
|
3746
|
-
import { homedir } from "os";
|
|
3747
|
-
import { isAbsolute } from "path";
|
|
3748
3784
|
function buildCommand(binary, options) {
|
|
3749
3785
|
const model = options.modelDef.model;
|
|
3750
3786
|
const { skipPermissions } = resolvePermissions(options.config, options.pipelineStage ?? "run");
|
|
3751
3787
|
const permArgs = skipPermissions ? ["--dangerously-skip-permissions"] : [];
|
|
3752
3788
|
return [binary, "--model", model, ...permArgs, "-p", options.prompt];
|
|
3753
3789
|
}
|
|
3754
|
-
function buildAllowedEnv(options) {
|
|
3755
|
-
const allowed = {};
|
|
3756
|
-
const essentialVars = ["PATH", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
|
|
3757
|
-
for (const varName of essentialVars) {
|
|
3758
|
-
if (process.env[varName]) {
|
|
3759
|
-
allowed[varName] = process.env[varName];
|
|
3760
|
-
}
|
|
3761
|
-
}
|
|
3762
|
-
const rawHome = process.env.HOME ?? "";
|
|
3763
|
-
const safeHome = rawHome && isAbsolute(rawHome) ? rawHome : homedir();
|
|
3764
|
-
if (rawHome !== safeHome) {
|
|
3765
|
-
const logger = getLogger();
|
|
3766
|
-
logger.warn("env", `HOME env is not absolute ("${rawHome}"), falling back to os.homedir(): ${safeHome}`);
|
|
3767
|
-
}
|
|
3768
|
-
allowed.HOME = safeHome;
|
|
3769
|
-
const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"];
|
|
3770
|
-
for (const varName of apiKeyVars) {
|
|
3771
|
-
if (process.env[varName]) {
|
|
3772
|
-
allowed[varName] = process.env[varName];
|
|
3773
|
-
}
|
|
3774
|
-
}
|
|
3775
|
-
const allowedPrefixes = ["CLAUDE_", "NAX_", "CLAW_", "TURBO_"];
|
|
3776
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
3777
|
-
if (allowedPrefixes.some((prefix) => key.startsWith(prefix))) {
|
|
3778
|
-
allowed[key] = value;
|
|
3779
|
-
}
|
|
3780
|
-
}
|
|
3781
|
-
if (options.modelDef.env) {
|
|
3782
|
-
Object.assign(allowed, options.modelDef.env);
|
|
3783
|
-
}
|
|
3784
|
-
if (options.env) {
|
|
3785
|
-
Object.assign(allowed, options.env);
|
|
3786
|
-
}
|
|
3787
|
-
return allowed;
|
|
3788
|
-
}
|
|
3789
3790
|
async function executeOnce(binary, options, pidRegistry) {
|
|
3790
3791
|
const cmd = _runOnceDeps.buildCmd(binary, options);
|
|
3791
3792
|
const startTime = Date.now();
|
|
@@ -3803,7 +3804,7 @@ async function executeOnce(binary, options, pidRegistry) {
|
|
|
3803
3804
|
cwd: options.workdir,
|
|
3804
3805
|
stdout: "pipe",
|
|
3805
3806
|
stderr: "inherit",
|
|
3806
|
-
env: buildAllowedEnv(options)
|
|
3807
|
+
env: buildAllowedEnv({ env: options.env, modelEnv: options.modelDef.env })
|
|
3807
3808
|
});
|
|
3808
3809
|
const processPid = proc.pid;
|
|
3809
3810
|
await pidRegistry.register(processPid);
|
|
@@ -3866,7 +3867,9 @@ var MAX_AGENT_OUTPUT_CHARS = 5000, MAX_AGENT_STDERR_CHARS = 1000, SIGKILL_GRACE_
|
|
|
3866
3867
|
var init_execution = __esm(() => {
|
|
3867
3868
|
init_logger2();
|
|
3868
3869
|
init_bun_deps();
|
|
3870
|
+
init_env();
|
|
3869
3871
|
init_cost2();
|
|
3872
|
+
init_env();
|
|
3870
3873
|
_runOnceDeps = {
|
|
3871
3874
|
killProc(proc, signal) {
|
|
3872
3875
|
proc.kill(signal);
|
|
@@ -17978,7 +17981,8 @@ var init_schemas3 = __esm(() => {
|
|
|
17978
17981
|
});
|
|
17979
17982
|
SemanticReviewConfigSchema = exports_external.object({
|
|
17980
17983
|
modelTier: ModelTierSchema.default("balanced"),
|
|
17981
|
-
rules: exports_external.array(exports_external.string()).default([])
|
|
17984
|
+
rules: exports_external.array(exports_external.string()).default([]),
|
|
17985
|
+
timeoutMs: exports_external.number().int().positive().default(600000)
|
|
17982
17986
|
});
|
|
17983
17987
|
ReviewConfigSchema = exports_external.object({
|
|
17984
17988
|
enabled: exports_external.boolean(),
|
|
@@ -18273,7 +18277,8 @@ var init_defaults = __esm(() => {
|
|
|
18273
18277
|
pluginMode: "per-story",
|
|
18274
18278
|
semantic: {
|
|
18275
18279
|
modelTier: "balanced",
|
|
18276
|
-
rules: []
|
|
18280
|
+
rules: [],
|
|
18281
|
+
timeoutMs: 600000
|
|
18277
18282
|
}
|
|
18278
18283
|
},
|
|
18279
18284
|
plan: {
|
|
@@ -18839,6 +18844,7 @@ __export(exports_generator, {
|
|
|
18839
18844
|
generateAcceptanceTests: () => generateAcceptanceTests,
|
|
18840
18845
|
extractTestCode: () => extractTestCode,
|
|
18841
18846
|
buildAcceptanceTestPrompt: () => buildAcceptanceTestPrompt,
|
|
18847
|
+
buildAcceptanceRunCommand: () => buildAcceptanceRunCommand,
|
|
18842
18848
|
acceptanceTestFilename: () => acceptanceTestFilename,
|
|
18843
18849
|
_generatorPRDDeps: () => _generatorPRDDeps
|
|
18844
18850
|
});
|
|
@@ -18858,13 +18864,33 @@ function skeletonImportLine(testFramework) {
|
|
|
18858
18864
|
function acceptanceTestFilename(language) {
|
|
18859
18865
|
switch (language?.toLowerCase()) {
|
|
18860
18866
|
case "go":
|
|
18861
|
-
return "acceptance_test.go";
|
|
18867
|
+
return ".nax-acceptance_test.go";
|
|
18862
18868
|
case "python":
|
|
18863
|
-
return "
|
|
18869
|
+
return ".nax-acceptance.test.py";
|
|
18864
18870
|
case "rust":
|
|
18865
|
-
return "
|
|
18871
|
+
return ".nax-acceptance.rs";
|
|
18866
18872
|
default:
|
|
18867
|
-
return "acceptance.test.ts";
|
|
18873
|
+
return ".nax-acceptance.test.ts";
|
|
18874
|
+
}
|
|
18875
|
+
}
|
|
18876
|
+
function buildAcceptanceRunCommand(testPath, testFramework, commandOverride) {
|
|
18877
|
+
if (commandOverride) {
|
|
18878
|
+
const resolved = commandOverride.replace(/\{\{files\}\}/g, testPath).replace(/\{\{file\}\}/g, testPath).replace(/\{\{FILE\}\}/g, testPath);
|
|
18879
|
+
return resolved.trim().split(/\s+/);
|
|
18880
|
+
}
|
|
18881
|
+
switch (testFramework?.toLowerCase()) {
|
|
18882
|
+
case "vitest":
|
|
18883
|
+
return ["npx", "vitest", "run", testPath];
|
|
18884
|
+
case "jest":
|
|
18885
|
+
return ["npx", "jest", testPath];
|
|
18886
|
+
case "pytest":
|
|
18887
|
+
return ["pytest", testPath];
|
|
18888
|
+
case "go-test":
|
|
18889
|
+
return ["go", "test", testPath];
|
|
18890
|
+
case "cargo-test":
|
|
18891
|
+
return ["cargo", "test", "--test", "acceptance"];
|
|
18892
|
+
default:
|
|
18893
|
+
return ["bun", "test", testPath, "--timeout=60000"];
|
|
18868
18894
|
}
|
|
18869
18895
|
}
|
|
18870
18896
|
async function generateFromPRD(_stories, refinedCriteria, options) {
|
|
@@ -18915,7 +18941,7 @@ Rules:
|
|
|
18915
18941
|
- Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
|
|
18916
18942
|
- **Prefer behavioral tests** \u2014 import functions and call them rather than reading source files. For example, to verify "getPostRunActions() returns empty array", import PluginRegistry and call getPostRunActions(), don't grep the source file for the method name.
|
|
18917
18943
|
- Output raw code only \u2014 no markdown fences, start directly with the language's import or package declaration
|
|
18918
|
-
- **Path anchor (CRITICAL)**: This test file
|
|
18944
|
+
- **Path anchor (CRITICAL)**: This test file lives at \`<package-root>/${acceptanceTestFilename(options.language)}\` and runs from the package root. Import from package sources using relative paths like \`./src/...\`. No deep \`../../../../\` traversal needed.`;
|
|
18919
18945
|
const prompt = basePrompt;
|
|
18920
18946
|
logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
|
|
18921
18947
|
const rawOutput = await (options.adapter ?? _generatorPRDDeps.adapter).complete(prompt, {
|
|
@@ -18926,7 +18952,7 @@ Rules:
|
|
|
18926
18952
|
});
|
|
18927
18953
|
let testCode = extractTestCode(rawOutput);
|
|
18928
18954
|
if (!testCode) {
|
|
18929
|
-
const targetPath = join2(options.featureDir,
|
|
18955
|
+
const targetPath = join2(options.featureDir, acceptanceTestFilename(options.language));
|
|
18930
18956
|
try {
|
|
18931
18957
|
const existing = await Bun.file(targetPath).text();
|
|
18932
18958
|
const recovered = extractTestCode(existing);
|
|
@@ -19450,37 +19476,6 @@ function parseAcpxJsonOutput(rawOutput) {
|
|
|
19450
19476
|
}
|
|
19451
19477
|
|
|
19452
19478
|
// src/agents/acp/spawn-client.ts
|
|
19453
|
-
import { homedir as homedir2 } from "os";
|
|
19454
|
-
import { isAbsolute as isAbsolute2 } from "path";
|
|
19455
|
-
function buildAllowedEnv2(extraEnv) {
|
|
19456
|
-
const allowed = {};
|
|
19457
|
-
const essentialVars = ["PATH", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
|
|
19458
|
-
for (const varName of essentialVars) {
|
|
19459
|
-
if (process.env[varName])
|
|
19460
|
-
allowed[varName] = process.env[varName];
|
|
19461
|
-
}
|
|
19462
|
-
const rawHome = process.env.HOME ?? "";
|
|
19463
|
-
const safeHome = rawHome && isAbsolute2(rawHome) ? rawHome : homedir2();
|
|
19464
|
-
if (rawHome !== safeHome) {
|
|
19465
|
-
getSafeLogger()?.warn("env", `HOME env is not absolute ("${rawHome}"), falling back to os.homedir(): ${safeHome}`);
|
|
19466
|
-
}
|
|
19467
|
-
allowed.HOME = safeHome;
|
|
19468
|
-
const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "CLAUDE_API_KEY"];
|
|
19469
|
-
for (const varName of apiKeyVars) {
|
|
19470
|
-
if (process.env[varName])
|
|
19471
|
-
allowed[varName] = process.env[varName];
|
|
19472
|
-
}
|
|
19473
|
-
const allowedPrefixes = ["CLAUDE_", "NAX_", "CLAW_", "TURBO_", "ACPX_", "CODEX_", "GEMINI_"];
|
|
19474
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
19475
|
-
if (allowedPrefixes.some((prefix) => key.startsWith(prefix))) {
|
|
19476
|
-
allowed[key] = value;
|
|
19477
|
-
}
|
|
19478
|
-
}
|
|
19479
|
-
if (extraEnv)
|
|
19480
|
-
Object.assign(allowed, extraEnv);
|
|
19481
|
-
return allowed;
|
|
19482
|
-
}
|
|
19483
|
-
|
|
19484
19479
|
class SpawnAcpSession {
|
|
19485
19480
|
agentName;
|
|
19486
19481
|
sessionName;
|
|
@@ -19570,7 +19565,7 @@ class SpawnAcpSession {
|
|
|
19570
19565
|
await this.pidRegistry?.unregister(processPid);
|
|
19571
19566
|
}
|
|
19572
19567
|
}
|
|
19573
|
-
async close() {
|
|
19568
|
+
async close(options) {
|
|
19574
19569
|
if (this.activeProc) {
|
|
19575
19570
|
try {
|
|
19576
19571
|
this.activeProc.kill(15);
|
|
@@ -19589,6 +19584,14 @@ class SpawnAcpSession {
|
|
|
19589
19584
|
stderr: stderr.slice(0, 200)
|
|
19590
19585
|
});
|
|
19591
19586
|
}
|
|
19587
|
+
if (options?.forceTerminate) {
|
|
19588
|
+
try {
|
|
19589
|
+
const stopProc = _spawnClientDeps.spawn(["acpx", this.agentName, "stop"], { stdout: "pipe", stderr: "pipe" });
|
|
19590
|
+
await stopProc.exited;
|
|
19591
|
+
} catch (err) {
|
|
19592
|
+
getSafeLogger()?.debug("acp-adapter", "acpx stop failed (swallowed)", { cause: String(err) });
|
|
19593
|
+
}
|
|
19594
|
+
}
|
|
19592
19595
|
}
|
|
19593
19596
|
async cancelActivePrompt() {
|
|
19594
19597
|
if (this.activeProc) {
|
|
@@ -19622,7 +19625,7 @@ class SpawnAcpClient {
|
|
|
19622
19625
|
this.agentName = lastToken;
|
|
19623
19626
|
this.cwd = cwd || process.cwd();
|
|
19624
19627
|
this.timeoutSeconds = timeoutSeconds || 1800;
|
|
19625
|
-
this.env =
|
|
19628
|
+
this.env = buildAllowedEnv();
|
|
19626
19629
|
this.pidRegistry = pidRegistry;
|
|
19627
19630
|
}
|
|
19628
19631
|
async start() {}
|
|
@@ -19674,6 +19677,7 @@ var _spawnClientDeps;
|
|
|
19674
19677
|
var init_spawn_client = __esm(() => {
|
|
19675
19678
|
init_logger2();
|
|
19676
19679
|
init_bun_deps();
|
|
19680
|
+
init_env();
|
|
19677
19681
|
_spawnClientDeps = {
|
|
19678
19682
|
spawn: typedSpawn
|
|
19679
19683
|
};
|
|
@@ -20095,6 +20099,7 @@ class AcpAgentAdapter {
|
|
|
20095
20099
|
const client = _acpAdapterDeps.createClient(cmdStr, workdir);
|
|
20096
20100
|
await client.start();
|
|
20097
20101
|
let session = null;
|
|
20102
|
+
let hadError = false;
|
|
20098
20103
|
try {
|
|
20099
20104
|
session = await client.createSession({
|
|
20100
20105
|
agentName: this.name,
|
|
@@ -20136,6 +20141,7 @@ class AcpAgentAdapter {
|
|
|
20136
20141
|
}
|
|
20137
20142
|
return unwrapped;
|
|
20138
20143
|
} catch (err) {
|
|
20144
|
+
hadError = true;
|
|
20139
20145
|
const error48 = err instanceof Error ? err : new Error(String(err));
|
|
20140
20146
|
lastError = error48;
|
|
20141
20147
|
const shouldRetry = isRateLimitError(error48) && attempt < MAX_RATE_LIMIT_RETRIES - 1;
|
|
@@ -20149,7 +20155,7 @@ class AcpAgentAdapter {
|
|
|
20149
20155
|
await _acpAdapterDeps.sleep(backoffMs);
|
|
20150
20156
|
} finally {
|
|
20151
20157
|
if (session) {
|
|
20152
|
-
await session.close().catch(() => {});
|
|
20158
|
+
await session.close({ forceTerminate: hadError }).catch(() => {});
|
|
20153
20159
|
}
|
|
20154
20160
|
await client.close().catch(() => {});
|
|
20155
20161
|
}
|
|
@@ -20957,7 +20963,7 @@ function isPlainObject2(value) {
|
|
|
20957
20963
|
|
|
20958
20964
|
// src/config/path-security.ts
|
|
20959
20965
|
import { existsSync as existsSync4, lstatSync, realpathSync } from "fs";
|
|
20960
|
-
import { isAbsolute as
|
|
20966
|
+
import { isAbsolute as isAbsolute2, normalize, resolve } from "path";
|
|
20961
20967
|
function validateDirectory(dirPath, baseDir) {
|
|
20962
20968
|
const resolved = resolve(dirPath);
|
|
20963
20969
|
if (!existsSync4(resolved)) {
|
|
@@ -20989,7 +20995,7 @@ function validateDirectory(dirPath, baseDir) {
|
|
|
20989
20995
|
function isWithinDirectory(targetPath, basePath) {
|
|
20990
20996
|
const normalizedTarget = normalize(targetPath);
|
|
20991
20997
|
const normalizedBase = normalize(basePath);
|
|
20992
|
-
if (!
|
|
20998
|
+
if (!isAbsolute2(normalizedTarget) || !isAbsolute2(normalizedBase)) {
|
|
20993
20999
|
return false;
|
|
20994
21000
|
}
|
|
20995
21001
|
const baseWithSlash = normalizedBase.endsWith("/") ? normalizedBase : `${normalizedBase}/`;
|
|
@@ -21025,10 +21031,10 @@ var MAX_DIRECTORY_DEPTH = 10;
|
|
|
21025
21031
|
var init_path_security = () => {};
|
|
21026
21032
|
|
|
21027
21033
|
// src/config/paths.ts
|
|
21028
|
-
import { homedir as
|
|
21034
|
+
import { homedir as homedir2 } from "os";
|
|
21029
21035
|
import { join as join5, resolve as resolve2 } from "path";
|
|
21030
21036
|
function globalConfigDir() {
|
|
21031
|
-
return join5(
|
|
21037
|
+
return join5(homedir2(), ".nax");
|
|
21032
21038
|
}
|
|
21033
21039
|
var PROJECT_NAX_DIR = ".nax";
|
|
21034
21040
|
var init_paths = () => {};
|
|
@@ -22299,7 +22305,7 @@ var package_default;
|
|
|
22299
22305
|
var init_package = __esm(() => {
|
|
22300
22306
|
package_default = {
|
|
22301
22307
|
name: "@nathapp/nax",
|
|
22302
|
-
version: "0.54.
|
|
22308
|
+
version: "0.54.2",
|
|
22303
22309
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
22304
22310
|
type: "module",
|
|
22305
22311
|
bin: {
|
|
@@ -22376,8 +22382,8 @@ var init_version = __esm(() => {
|
|
|
22376
22382
|
NAX_VERSION = package_default.version;
|
|
22377
22383
|
NAX_COMMIT = (() => {
|
|
22378
22384
|
try {
|
|
22379
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
22380
|
-
return "
|
|
22385
|
+
if (/^[0-9a-f]{6,10}$/.test("18dd8fc"))
|
|
22386
|
+
return "18dd8fc";
|
|
22381
22387
|
} catch {}
|
|
22382
22388
|
try {
|
|
22383
22389
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -24041,6 +24047,7 @@ function areAllStoriesComplete(ctx) {
|
|
|
24041
24047
|
}
|
|
24042
24048
|
var acceptanceStage;
|
|
24043
24049
|
var init_acceptance2 = __esm(() => {
|
|
24050
|
+
init_generator();
|
|
24044
24051
|
init_logger2();
|
|
24045
24052
|
init_prd();
|
|
24046
24053
|
acceptanceStage = {
|
|
@@ -24063,59 +24070,44 @@ var init_acceptance2 = __esm(() => {
|
|
|
24063
24070
|
logger.warn("acceptance", "No feature directory \u2014 skipping acceptance tests");
|
|
24064
24071
|
return { action: "continue" };
|
|
24065
24072
|
}
|
|
24066
|
-
const
|
|
24067
|
-
|
|
24068
|
-
|
|
24069
|
-
|
|
24070
|
-
|
|
24071
|
-
|
|
24073
|
+
const testGroups = ctx.acceptanceTestPaths ?? [
|
|
24074
|
+
{
|
|
24075
|
+
testPath: path4.join(ctx.featureDir, effectiveConfig.acceptance.testPath),
|
|
24076
|
+
packageDir: ctx.workdir
|
|
24077
|
+
}
|
|
24078
|
+
];
|
|
24079
|
+
const allFailedACs = [];
|
|
24080
|
+
const allOutputParts = [];
|
|
24081
|
+
let anyError = false;
|
|
24082
|
+
let errorExitCode = 0;
|
|
24083
|
+
for (const { testPath, packageDir } of testGroups) {
|
|
24084
|
+
const testFile = Bun.file(testPath);
|
|
24085
|
+
const exists = await testFile.exists();
|
|
24086
|
+
if (!exists) {
|
|
24087
|
+
logger.warn("acceptance", "Acceptance test file not found \u2014 skipping", { testPath });
|
|
24088
|
+
continue;
|
|
24089
|
+
}
|
|
24090
|
+
const testCmdParts = buildAcceptanceRunCommand(testPath, effectiveConfig.project?.testFramework, effectiveConfig.acceptance.command);
|
|
24091
|
+
logger.info("acceptance", "Running acceptance command", {
|
|
24092
|
+
cmd: testCmdParts.join(" "),
|
|
24093
|
+
packageDir
|
|
24072
24094
|
});
|
|
24073
|
-
|
|
24074
|
-
|
|
24075
|
-
|
|
24076
|
-
|
|
24077
|
-
|
|
24078
|
-
const
|
|
24079
|
-
|
|
24080
|
-
|
|
24081
|
-
|
|
24082
|
-
|
|
24083
|
-
|
|
24084
|
-
cwd: ctx.workdir,
|
|
24085
|
-
stdout: "pipe",
|
|
24086
|
-
stderr: "pipe"
|
|
24087
|
-
});
|
|
24088
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
24089
|
-
proc.exited,
|
|
24090
|
-
new Response(proc.stdout).text(),
|
|
24091
|
-
new Response(proc.stderr).text()
|
|
24092
|
-
]);
|
|
24093
|
-
const output = `${stdout}
|
|
24095
|
+
const proc = Bun.spawn(testCmdParts, {
|
|
24096
|
+
cwd: packageDir,
|
|
24097
|
+
stdout: "pipe",
|
|
24098
|
+
stderr: "pipe"
|
|
24099
|
+
});
|
|
24100
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
24101
|
+
proc.exited,
|
|
24102
|
+
new Response(proc.stdout).text(),
|
|
24103
|
+
new Response(proc.stderr).text()
|
|
24104
|
+
]);
|
|
24105
|
+
const output = `${stdout}
|
|
24094
24106
|
${stderr}`;
|
|
24095
|
-
|
|
24096
|
-
|
|
24097
|
-
|
|
24098
|
-
|
|
24099
|
-
logger.info("acceptance", "All acceptance tests passed");
|
|
24100
|
-
return { action: "continue" };
|
|
24101
|
-
}
|
|
24102
|
-
if (failedACs.length > 0 && actualFailures.length === 0) {
|
|
24103
|
-
logger.info("acceptance", "All failed ACs are overridden \u2014 treating as pass");
|
|
24104
|
-
return { action: "continue" };
|
|
24105
|
-
}
|
|
24106
|
-
if (failedACs.length === 0 && exitCode !== 0) {
|
|
24107
|
-
logger.error("acceptance", "Tests errored with no AC failures parsed", { exitCode });
|
|
24108
|
-
logTestOutput(logger, "acceptance", output);
|
|
24109
|
-
ctx.acceptanceFailures = {
|
|
24110
|
-
failedACs: ["AC-ERROR"],
|
|
24111
|
-
testOutput: output
|
|
24112
|
-
};
|
|
24113
|
-
return {
|
|
24114
|
-
action: "fail",
|
|
24115
|
-
reason: `Acceptance tests errored (exit code ${exitCode}): syntax error, import failure, or unhandled exception`
|
|
24116
|
-
};
|
|
24117
|
-
}
|
|
24118
|
-
if (actualFailures.length > 0) {
|
|
24107
|
+
allOutputParts.push(output);
|
|
24108
|
+
const failedACs = parseTestFailures(output);
|
|
24109
|
+
const overrides = ctx.prd.acceptanceOverrides ?? {};
|
|
24110
|
+
const actualFailures = failedACs.filter((acId) => !overrides[acId]);
|
|
24119
24111
|
const overriddenFailures = failedACs.filter((acId) => overrides[acId]);
|
|
24120
24112
|
if (overriddenFailures.length > 0) {
|
|
24121
24113
|
logger.warn("acceptance", "Skipped failures (overridden)", {
|
|
@@ -24123,19 +24115,52 @@ ${stderr}`;
|
|
|
24123
24115
|
overrides: overriddenFailures.map((acId) => ({ acId, reason: overrides[acId] }))
|
|
24124
24116
|
});
|
|
24125
24117
|
}
|
|
24126
|
-
|
|
24127
|
-
|
|
24128
|
-
|
|
24129
|
-
|
|
24130
|
-
|
|
24131
|
-
|
|
24118
|
+
if (failedACs.length === 0 && exitCode !== 0) {
|
|
24119
|
+
logger.error("acceptance", "Tests errored with no AC failures parsed", {
|
|
24120
|
+
exitCode,
|
|
24121
|
+
packageDir
|
|
24122
|
+
});
|
|
24123
|
+
logTestOutput(logger, "acceptance", output);
|
|
24124
|
+
anyError = true;
|
|
24125
|
+
errorExitCode = exitCode;
|
|
24126
|
+
allFailedACs.push("AC-ERROR");
|
|
24127
|
+
continue;
|
|
24128
|
+
}
|
|
24129
|
+
for (const acId of actualFailures) {
|
|
24130
|
+
if (!allFailedACs.includes(acId)) {
|
|
24131
|
+
allFailedACs.push(acId);
|
|
24132
|
+
}
|
|
24133
|
+
}
|
|
24134
|
+
if (actualFailures.length > 0) {
|
|
24135
|
+
logger.error("acceptance", "Acceptance tests failed", {
|
|
24136
|
+
failedACs: actualFailures,
|
|
24137
|
+
packageDir
|
|
24138
|
+
});
|
|
24139
|
+
logTestOutput(logger, "acceptance", output);
|
|
24140
|
+
} else if (exitCode === 0) {
|
|
24141
|
+
logger.info("acceptance", "Package acceptance tests passed", { packageDir });
|
|
24142
|
+
}
|
|
24143
|
+
}
|
|
24144
|
+
const combinedOutput = allOutputParts.join(`
|
|
24145
|
+
`);
|
|
24146
|
+
if (allFailedACs.length === 0) {
|
|
24147
|
+
logger.info("acceptance", "All acceptance tests passed");
|
|
24148
|
+
return { action: "continue" };
|
|
24149
|
+
}
|
|
24150
|
+
ctx.acceptanceFailures = {
|
|
24151
|
+
failedACs: allFailedACs,
|
|
24152
|
+
testOutput: combinedOutput
|
|
24153
|
+
};
|
|
24154
|
+
if (anyError) {
|
|
24132
24155
|
return {
|
|
24133
24156
|
action: "fail",
|
|
24134
|
-
reason: `Acceptance tests
|
|
24157
|
+
reason: `Acceptance tests errored (exit code ${errorExitCode}): syntax error, import failure, or unhandled exception`
|
|
24135
24158
|
};
|
|
24136
24159
|
}
|
|
24137
|
-
|
|
24138
|
-
|
|
24160
|
+
return {
|
|
24161
|
+
action: "fail",
|
|
24162
|
+
reason: `Acceptance tests failed: ${allFailedACs.join(", ")}`
|
|
24163
|
+
};
|
|
24139
24164
|
}
|
|
24140
24165
|
};
|
|
24141
24166
|
});
|
|
@@ -24252,6 +24277,7 @@ var init_acceptance_setup = __esm(() => {
|
|
|
24252
24277
|
init_generator();
|
|
24253
24278
|
init_registry();
|
|
24254
24279
|
init_config();
|
|
24280
|
+
init_logger2();
|
|
24255
24281
|
_acceptanceSetupDeps = {
|
|
24256
24282
|
getAgent,
|
|
24257
24283
|
fileExists: async (_path) => {
|
|
@@ -24282,14 +24308,8 @@ var init_acceptance_setup = __esm(() => {
|
|
|
24282
24308
|
writeMeta: async (metaPath, meta3) => {
|
|
24283
24309
|
await Bun.write(metaPath, JSON.stringify(meta3, null, 2));
|
|
24284
24310
|
},
|
|
24285
|
-
runTest: async (_testPath, _workdir,
|
|
24286
|
-
|
|
24287
|
-
if (_testCmd) {
|
|
24288
|
-
const parts = _testCmd.trim().split(/\s+/);
|
|
24289
|
-
cmd = [...parts, _testPath];
|
|
24290
|
-
} else {
|
|
24291
|
-
cmd = ["bun", "test", _testPath];
|
|
24292
|
-
}
|
|
24311
|
+
runTest: async (_testPath, _workdir, _cmd) => {
|
|
24312
|
+
const cmd = _cmd;
|
|
24293
24313
|
const proc = Bun.spawn(cmd, {
|
|
24294
24314
|
cwd: _workdir,
|
|
24295
24315
|
stdout: "pipe",
|
|
@@ -24322,57 +24342,103 @@ ${stderr}` };
|
|
|
24322
24342
|
return { action: "fail", reason: "[acceptance-setup] featureDir is not set" };
|
|
24323
24343
|
}
|
|
24324
24344
|
const language = (ctx.effectiveConfig ?? ctx.config).project?.language;
|
|
24325
|
-
const testPath = path5.join(ctx.featureDir, acceptanceTestFilename(language));
|
|
24326
24345
|
const metaPath = path5.join(ctx.featureDir, "acceptance-meta.json");
|
|
24327
24346
|
const allCriteria = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-")).flatMap((s) => s.acceptanceCriteria);
|
|
24347
|
+
const nonFixStories = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-"));
|
|
24348
|
+
const workdirGroups = new Map;
|
|
24349
|
+
for (const story of nonFixStories) {
|
|
24350
|
+
const wd = story.workdir ?? "";
|
|
24351
|
+
if (!workdirGroups.has(wd)) {
|
|
24352
|
+
workdirGroups.set(wd, { stories: [], criteria: [] });
|
|
24353
|
+
}
|
|
24354
|
+
const group = workdirGroups.get(wd);
|
|
24355
|
+
if (group) {
|
|
24356
|
+
group.stories.push(story);
|
|
24357
|
+
group.criteria.push(...story.acceptanceCriteria);
|
|
24358
|
+
}
|
|
24359
|
+
}
|
|
24360
|
+
if (workdirGroups.size === 0) {
|
|
24361
|
+
workdirGroups.set("", { stories: [], criteria: [] });
|
|
24362
|
+
}
|
|
24363
|
+
const testPaths = [];
|
|
24364
|
+
for (const [workdir] of workdirGroups) {
|
|
24365
|
+
const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
|
|
24366
|
+
const testPath = path5.join(packageDir, acceptanceTestFilename(language));
|
|
24367
|
+
testPaths.push({ testPath, packageDir });
|
|
24368
|
+
}
|
|
24328
24369
|
let totalCriteria = 0;
|
|
24329
24370
|
let testableCount = 0;
|
|
24330
|
-
const
|
|
24331
|
-
|
|
24332
|
-
|
|
24371
|
+
const existsResults = await Promise.all(testPaths.map(({ testPath }) => _acceptanceSetupDeps.fileExists(testPath)));
|
|
24372
|
+
const anyFileMissing = existsResults.some((exists) => !exists);
|
|
24373
|
+
let shouldGenerate = anyFileMissing;
|
|
24374
|
+
if (!anyFileMissing) {
|
|
24333
24375
|
const fingerprint = computeACFingerprint(allCriteria);
|
|
24334
24376
|
const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
|
|
24377
|
+
getSafeLogger()?.debug("acceptance-setup", "Fingerprint check", {
|
|
24378
|
+
currentFingerprint: fingerprint,
|
|
24379
|
+
storedFingerprint: meta3?.acFingerprint ?? "none",
|
|
24380
|
+
match: meta3?.acFingerprint === fingerprint
|
|
24381
|
+
});
|
|
24335
24382
|
if (!meta3 || meta3.acFingerprint !== fingerprint) {
|
|
24336
|
-
|
|
24337
|
-
|
|
24383
|
+
getSafeLogger()?.info("acceptance-setup", "ACs changed \u2014 regenerating acceptance tests", {
|
|
24384
|
+
reason: !meta3 ? "no meta file" : "fingerprint mismatch"
|
|
24385
|
+
});
|
|
24386
|
+
for (const { testPath } of testPaths) {
|
|
24387
|
+
if (await _acceptanceSetupDeps.fileExists(testPath)) {
|
|
24388
|
+
await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
|
|
24389
|
+
await _acceptanceSetupDeps.deleteFile(testPath);
|
|
24390
|
+
}
|
|
24391
|
+
}
|
|
24338
24392
|
shouldGenerate = true;
|
|
24393
|
+
} else {
|
|
24394
|
+
getSafeLogger()?.info("acceptance-setup", "Reusing existing acceptance tests (fingerprint match)");
|
|
24339
24395
|
}
|
|
24340
24396
|
}
|
|
24341
24397
|
if (shouldGenerate) {
|
|
24342
24398
|
totalCriteria = allCriteria.length;
|
|
24343
24399
|
const { getAgent: getAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
24344
24400
|
const agent = (ctx.agentGetFn ?? _acceptanceSetupDeps.getAgent)(ctx.config.autoMode.defaultAgent);
|
|
24345
|
-
let
|
|
24401
|
+
let allRefinedCriteria;
|
|
24346
24402
|
if (ctx.config.acceptance.refinement) {
|
|
24347
|
-
|
|
24348
|
-
|
|
24349
|
-
|
|
24350
|
-
|
|
24351
|
-
|
|
24352
|
-
|
|
24353
|
-
|
|
24403
|
+
allRefinedCriteria = [];
|
|
24404
|
+
for (const story of nonFixStories) {
|
|
24405
|
+
const storyRefined = await _acceptanceSetupDeps.refine(story.acceptanceCriteria, {
|
|
24406
|
+
storyId: story.id,
|
|
24407
|
+
codebaseContext: "",
|
|
24408
|
+
config: ctx.config,
|
|
24409
|
+
testStrategy: ctx.config.acceptance.testStrategy,
|
|
24410
|
+
testFramework: ctx.config.acceptance.testFramework
|
|
24411
|
+
});
|
|
24412
|
+
allRefinedCriteria = allRefinedCriteria.concat(storyRefined);
|
|
24413
|
+
}
|
|
24354
24414
|
} else {
|
|
24355
|
-
|
|
24415
|
+
allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
|
|
24356
24416
|
original: c,
|
|
24357
24417
|
refined: c,
|
|
24358
24418
|
testable: true,
|
|
24359
|
-
storyId:
|
|
24360
|
-
}));
|
|
24419
|
+
storyId: story.id
|
|
24420
|
+
})));
|
|
24421
|
+
}
|
|
24422
|
+
testableCount = allRefinedCriteria.filter((r) => r.testable).length;
|
|
24423
|
+
for (const [workdir, group] of workdirGroups) {
|
|
24424
|
+
const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
|
|
24425
|
+
const testPath = path5.join(packageDir, acceptanceTestFilename(language));
|
|
24426
|
+
const groupStoryIds = new Set(group.stories.map((s) => s.id));
|
|
24427
|
+
const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
|
|
24428
|
+
const result = await _acceptanceSetupDeps.generate(group.stories, groupRefined, {
|
|
24429
|
+
featureName: ctx.prd.feature,
|
|
24430
|
+
workdir: packageDir,
|
|
24431
|
+
featureDir: ctx.featureDir,
|
|
24432
|
+
codebaseContext: "",
|
|
24433
|
+
modelTier: ctx.config.acceptance.model ?? "fast",
|
|
24434
|
+
modelDef: resolveModel(ctx.config.models[ctx.config.acceptance.model ?? "fast"]),
|
|
24435
|
+
config: ctx.config,
|
|
24436
|
+
testStrategy: ctx.config.acceptance.testStrategy,
|
|
24437
|
+
testFramework: ctx.config.acceptance.testFramework,
|
|
24438
|
+
adapter: agent ?? undefined
|
|
24439
|
+
});
|
|
24440
|
+
await _acceptanceSetupDeps.writeFile(testPath, result.testCode);
|
|
24361
24441
|
}
|
|
24362
|
-
testableCount = refinedCriteria.filter((r) => r.testable).length;
|
|
24363
|
-
const result = await _acceptanceSetupDeps.generate(ctx.prd.userStories, refinedCriteria, {
|
|
24364
|
-
featureName: ctx.prd.feature,
|
|
24365
|
-
workdir: ctx.workdir,
|
|
24366
|
-
featureDir: ctx.featureDir,
|
|
24367
|
-
codebaseContext: "",
|
|
24368
|
-
modelTier: ctx.config.acceptance.model ?? "fast",
|
|
24369
|
-
modelDef: resolveModel(ctx.config.models[ctx.config.acceptance.model ?? "fast"]),
|
|
24370
|
-
config: ctx.config,
|
|
24371
|
-
testStrategy: ctx.config.acceptance.testStrategy,
|
|
24372
|
-
testFramework: ctx.config.acceptance.testFramework,
|
|
24373
|
-
adapter: agent ?? undefined
|
|
24374
|
-
});
|
|
24375
|
-
await _acceptanceSetupDeps.writeFile(testPath, result.testCode);
|
|
24376
24442
|
const fingerprint = computeACFingerprint(allCriteria);
|
|
24377
24443
|
await _acceptanceSetupDeps.writeMeta(metaPath, {
|
|
24378
24444
|
generatedAt: new Date().toISOString(),
|
|
@@ -24382,20 +24448,32 @@ ${stderr}` };
|
|
|
24382
24448
|
generator: "nax"
|
|
24383
24449
|
});
|
|
24384
24450
|
}
|
|
24451
|
+
ctx.acceptanceTestPaths = testPaths;
|
|
24385
24452
|
if (ctx.config.acceptance.redGate === false) {
|
|
24386
24453
|
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
|
|
24387
24454
|
return { action: "continue" };
|
|
24388
24455
|
}
|
|
24389
|
-
const
|
|
24390
|
-
|
|
24391
|
-
|
|
24456
|
+
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
24457
|
+
let redFailCount = 0;
|
|
24458
|
+
for (const { testPath, packageDir } of testPaths) {
|
|
24459
|
+
const runCmd = buildAcceptanceRunCommand(testPath, effectiveConfig.project?.testFramework, effectiveConfig.acceptance.command);
|
|
24460
|
+
getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", {
|
|
24461
|
+
cmd: runCmd.join(" "),
|
|
24462
|
+
packageDir
|
|
24463
|
+
});
|
|
24464
|
+
const { exitCode } = await _acceptanceSetupDeps.runTest(testPath, packageDir, runCmd);
|
|
24465
|
+
if (exitCode !== 0) {
|
|
24466
|
+
redFailCount++;
|
|
24467
|
+
}
|
|
24468
|
+
}
|
|
24469
|
+
if (redFailCount === 0) {
|
|
24392
24470
|
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
|
|
24393
24471
|
return {
|
|
24394
24472
|
action: "skip",
|
|
24395
24473
|
reason: "[acceptance-setup] Acceptance tests already pass \u2014 they are not testing new behavior. Skipping acceptance gate."
|
|
24396
24474
|
};
|
|
24397
24475
|
}
|
|
24398
|
-
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount
|
|
24476
|
+
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount };
|
|
24399
24477
|
return { action: "continue" };
|
|
24400
24478
|
}
|
|
24401
24479
|
};
|
|
@@ -24723,7 +24801,11 @@ If the implementation looks correct, respond with { "passed": true, "findings":
|
|
|
24723
24801
|
}
|
|
24724
24802
|
function parseLLMResponse(raw) {
|
|
24725
24803
|
try {
|
|
24726
|
-
|
|
24804
|
+
let cleaned = raw.trim();
|
|
24805
|
+
const fenceMatch = cleaned.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?\s*```$/);
|
|
24806
|
+
if (fenceMatch)
|
|
24807
|
+
cleaned = fenceMatch[1].trim();
|
|
24808
|
+
const parsed = JSON.parse(cleaned);
|
|
24727
24809
|
if (typeof parsed !== "object" || parsed === null)
|
|
24728
24810
|
return null;
|
|
24729
24811
|
const obj = parsed;
|
|
@@ -24771,6 +24853,7 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
|
|
|
24771
24853
|
durationMs: Date.now() - startTime
|
|
24772
24854
|
};
|
|
24773
24855
|
}
|
|
24856
|
+
logger?.info("review", "Running semantic check", { storyId: story.id, modelTier: semanticConfig.modelTier });
|
|
24774
24857
|
const rawDiff = await collectDiff(workdir, storyGitRef);
|
|
24775
24858
|
const diff = truncateDiff(rawDiff);
|
|
24776
24859
|
const agent = modelResolver(semanticConfig.modelTier);
|
|
@@ -24790,7 +24873,11 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
|
|
|
24790
24873
|
const prompt = buildPrompt(story, semanticConfig, diff);
|
|
24791
24874
|
let rawResponse;
|
|
24792
24875
|
try {
|
|
24793
|
-
rawResponse = await agent.complete(prompt
|
|
24876
|
+
rawResponse = await agent.complete(prompt, {
|
|
24877
|
+
sessionName: `nax-semantic-${story.id}`,
|
|
24878
|
+
workdir,
|
|
24879
|
+
timeoutMs: semanticConfig.timeoutMs
|
|
24880
|
+
});
|
|
24794
24881
|
} catch (err) {
|
|
24795
24882
|
logger?.warn("semantic", "LLM call failed \u2014 fail-open", { cause: String(err) });
|
|
24796
24883
|
return {
|
|
@@ -24815,6 +24902,11 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
|
|
|
24815
24902
|
};
|
|
24816
24903
|
}
|
|
24817
24904
|
if (!parsed.passed && parsed.findings.length > 0) {
|
|
24905
|
+
const durationMs2 = Date.now() - startTime;
|
|
24906
|
+
logger?.warn("review", `Semantic review failed: ${parsed.findings.length} findings`, {
|
|
24907
|
+
storyId: story.id,
|
|
24908
|
+
durationMs: durationMs2
|
|
24909
|
+
});
|
|
24818
24910
|
const output = `Semantic review failed:
|
|
24819
24911
|
|
|
24820
24912
|
${formatFindings(parsed.findings)}`;
|
|
@@ -24824,17 +24916,21 @@ ${formatFindings(parsed.findings)}`;
|
|
|
24824
24916
|
command: "",
|
|
24825
24917
|
exitCode: 1,
|
|
24826
24918
|
output,
|
|
24827
|
-
durationMs:
|
|
24919
|
+
durationMs: durationMs2,
|
|
24828
24920
|
findings: toReviewFindings(parsed.findings)
|
|
24829
24921
|
};
|
|
24830
24922
|
}
|
|
24923
|
+
const durationMs = Date.now() - startTime;
|
|
24924
|
+
if (parsed.passed) {
|
|
24925
|
+
logger?.info("review", "Semantic review passed", { storyId: story.id, durationMs });
|
|
24926
|
+
}
|
|
24831
24927
|
return {
|
|
24832
24928
|
check: "semantic",
|
|
24833
24929
|
success: parsed.passed,
|
|
24834
24930
|
command: "",
|
|
24835
24931
|
exitCode: parsed.passed ? 0 : 1,
|
|
24836
24932
|
output: parsed.passed ? "Semantic review passed" : "Semantic review failed (no findings)",
|
|
24837
|
-
durationMs
|
|
24933
|
+
durationMs
|
|
24838
24934
|
};
|
|
24839
24935
|
}
|
|
24840
24936
|
var _semanticDeps, DIFF_CAP_BYTES = 12288, DEFAULT_RULES;
|
|
@@ -25017,7 +25113,8 @@ async function runReview(config2, workdir, executionConfig, qualityCommands, sto
|
|
|
25017
25113
|
/nax\/features\/[^/]+\/acceptance-refined\.json$/,
|
|
25018
25114
|
/\.nax-verifier-verdict\.json$/,
|
|
25019
25115
|
/\.nax-pids$/,
|
|
25020
|
-
/\.nax-wt
|
|
25116
|
+
/\.nax-wt\//,
|
|
25117
|
+
/\.nax-acceptance[^/]*$/
|
|
25021
25118
|
];
|
|
25022
25119
|
const uncommittedFiles = allUncommittedFiles.filter((f) => !NAX_RUNTIME_PATTERNS.some((pattern) => pattern.test(f)));
|
|
25023
25120
|
if (uncommittedFiles.length > 0) {
|
|
@@ -25042,7 +25139,11 @@ Stage and commit these files before running review.`
|
|
|
25042
25139
|
description: story?.description ?? "",
|
|
25043
25140
|
acceptanceCriteria: story?.acceptanceCriteria ?? []
|
|
25044
25141
|
};
|
|
25045
|
-
const semanticCfg = config2.semantic ?? {
|
|
25142
|
+
const semanticCfg = config2.semantic ?? {
|
|
25143
|
+
modelTier: "balanced",
|
|
25144
|
+
rules: [],
|
|
25145
|
+
timeoutMs: 600000
|
|
25146
|
+
};
|
|
25046
25147
|
const result2 = await _reviewSemanticDeps.runSemanticReview(workdir, storyGitRef, semanticStory, semanticCfg, modelResolver ?? (() => null));
|
|
25047
25148
|
checks3.push(result2);
|
|
25048
25149
|
if (!result2.success && !firstFailure) {
|
|
@@ -31077,7 +31178,7 @@ var init_init_context = __esm(() => {
|
|
|
31077
31178
|
|
|
31078
31179
|
// src/utils/path-security.ts
|
|
31079
31180
|
import { realpathSync as realpathSync3 } from "fs";
|
|
31080
|
-
import { dirname as dirname4, isAbsolute as
|
|
31181
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join31, normalize as normalize2, resolve as resolve5 } from "path";
|
|
31081
31182
|
function safeRealpathForComparison(p) {
|
|
31082
31183
|
try {
|
|
31083
31184
|
return realpathSync3(p);
|
|
@@ -31094,7 +31195,7 @@ function validateModulePath(modulePath, allowedRoots) {
|
|
|
31094
31195
|
return { valid: false, error: "Module path is empty" };
|
|
31095
31196
|
}
|
|
31096
31197
|
const resolvedRoots = allowedRoots.map((r) => safeRealpathForComparison(resolve5(r)));
|
|
31097
|
-
if (
|
|
31198
|
+
if (isAbsolute3(modulePath)) {
|
|
31098
31199
|
const normalized = normalize2(modulePath);
|
|
31099
31200
|
const resolved = safeRealpathForComparison(normalized);
|
|
31100
31201
|
const isWithin = resolvedRoots.some((root) => resolved.startsWith(`${root}/`) || resolved === root);
|
|
@@ -31722,7 +31823,8 @@ var init_checks_git = __esm(() => {
|
|
|
31722
31823
|
/^.{2} \.nax\/features\/[^/]+\/acceptance-refined\.json$/,
|
|
31723
31824
|
/^.{2} \.nax-verifier-verdict\.json$/,
|
|
31724
31825
|
/^.{2} \.nax-pids$/,
|
|
31725
|
-
/^.{2} \.nax-wt
|
|
31826
|
+
/^.{2} \.nax-wt\//,
|
|
31827
|
+
/^.{2} .*\.nax-acceptance[^/]*$/
|
|
31726
31828
|
];
|
|
31727
31829
|
});
|
|
31728
31830
|
|
|
@@ -31935,7 +32037,7 @@ var init_checks_blockers = __esm(() => {
|
|
|
31935
32037
|
|
|
31936
32038
|
// src/precheck/checks-warnings.ts
|
|
31937
32039
|
import { existsSync as existsSync30 } from "fs";
|
|
31938
|
-
import { isAbsolute as
|
|
32040
|
+
import { isAbsolute as isAbsolute5 } from "path";
|
|
31939
32041
|
async function checkClaudeMdExists(workdir) {
|
|
31940
32042
|
const claudeMdPath = `${workdir}/CLAUDE.md`;
|
|
31941
32043
|
const passed = existsSync30(claudeMdPath);
|
|
@@ -32035,7 +32137,8 @@ async function checkGitignoreCoversNax(workdir) {
|
|
|
32035
32137
|
".nax/metrics.json",
|
|
32036
32138
|
".nax/features/*/status.json",
|
|
32037
32139
|
".nax-pids",
|
|
32038
|
-
".nax-wt/"
|
|
32140
|
+
".nax-wt/",
|
|
32141
|
+
"**/.nax-acceptance*"
|
|
32039
32142
|
];
|
|
32040
32143
|
const missing = patterns.filter((pattern) => !content.includes(pattern));
|
|
32041
32144
|
const passed = missing.length === 0;
|
|
@@ -32067,7 +32170,7 @@ async function checkPromptOverrideFiles(config2, workdir) {
|
|
|
32067
32170
|
}
|
|
32068
32171
|
async function checkHomeEnvValid() {
|
|
32069
32172
|
const home = process.env.HOME ?? "";
|
|
32070
|
-
const passed = home !== "" &&
|
|
32173
|
+
const passed = home !== "" && isAbsolute5(home);
|
|
32071
32174
|
return {
|
|
32072
32175
|
name: "home-env-valid",
|
|
32073
32176
|
tier: "warning",
|
|
@@ -32170,6 +32273,24 @@ async function checkLanguageTools(profile, workdir) {
|
|
|
32170
32273
|
message: `Missing ${language} tools: ${missing.join(", ")}. ${toolConfig.installHint}`
|
|
32171
32274
|
};
|
|
32172
32275
|
}
|
|
32276
|
+
function checkBuildCommandInReviewChecks(config2) {
|
|
32277
|
+
const hasBuildCmd = !!(config2.review?.commands?.build || config2.quality?.commands?.build);
|
|
32278
|
+
const buildInChecks = config2.review?.checks?.includes("build") ?? false;
|
|
32279
|
+
if (hasBuildCmd && !buildInChecks) {
|
|
32280
|
+
return {
|
|
32281
|
+
name: "build-command-in-review-checks",
|
|
32282
|
+
tier: "warning",
|
|
32283
|
+
passed: false,
|
|
32284
|
+
message: 'A build command is configured but "build" is not in review.checks \u2014 the build step will never run. Add "build" to review.checks to enable it.'
|
|
32285
|
+
};
|
|
32286
|
+
}
|
|
32287
|
+
return {
|
|
32288
|
+
name: "build-command-in-review-checks",
|
|
32289
|
+
tier: "warning",
|
|
32290
|
+
passed: true,
|
|
32291
|
+
message: "build command check OK"
|
|
32292
|
+
};
|
|
32293
|
+
}
|
|
32173
32294
|
var _languageToolsDeps;
|
|
32174
32295
|
var init_checks_warnings = __esm(() => {
|
|
32175
32296
|
_languageToolsDeps = {
|
|
@@ -32361,7 +32482,8 @@ function getEnvironmentWarnings(config2, workdir) {
|
|
|
32361
32482
|
() => checkHomeEnvValid(),
|
|
32362
32483
|
() => checkPromptOverrideFiles(config2, workdir),
|
|
32363
32484
|
() => checkLanguageTools(config2.project, workdir),
|
|
32364
|
-
() => checkMultiAgentHealth()
|
|
32485
|
+
() => checkMultiAgentHealth(),
|
|
32486
|
+
() => Promise.resolve(checkBuildCommandInReviewChecks(config2))
|
|
32365
32487
|
];
|
|
32366
32488
|
}
|
|
32367
32489
|
function getProjectBlockers(prd) {
|
|
@@ -34698,12 +34820,12 @@ var init_parallel_executor = __esm(() => {
|
|
|
34698
34820
|
|
|
34699
34821
|
// src/pipeline/subscribers/events-writer.ts
|
|
34700
34822
|
import { appendFile as appendFile2, mkdir as mkdir2 } from "fs/promises";
|
|
34701
|
-
import { homedir as
|
|
34823
|
+
import { homedir as homedir5 } from "os";
|
|
34702
34824
|
import { basename as basename5, join as join49 } from "path";
|
|
34703
34825
|
function wireEventsWriter(bus, feature, runId, workdir) {
|
|
34704
34826
|
const logger = getSafeLogger();
|
|
34705
34827
|
const project = basename5(workdir);
|
|
34706
|
-
const eventsDir = join49(
|
|
34828
|
+
const eventsDir = join49(homedir5(), ".nax", "events", project);
|
|
34707
34829
|
const eventsFile = join49(eventsDir, "events.jsonl");
|
|
34708
34830
|
let dirReady = false;
|
|
34709
34831
|
const write = (line) => {
|
|
@@ -34877,12 +34999,12 @@ var init_interaction2 = __esm(() => {
|
|
|
34877
34999
|
|
|
34878
35000
|
// src/pipeline/subscribers/registry.ts
|
|
34879
35001
|
import { mkdir as mkdir3, writeFile } from "fs/promises";
|
|
34880
|
-
import { homedir as
|
|
35002
|
+
import { homedir as homedir6 } from "os";
|
|
34881
35003
|
import { basename as basename6, join as join50 } from "path";
|
|
34882
35004
|
function wireRegistry(bus, feature, runId, workdir) {
|
|
34883
35005
|
const logger = getSafeLogger();
|
|
34884
35006
|
const project = basename6(workdir);
|
|
34885
|
-
const runDir = join50(
|
|
35007
|
+
const runDir = join50(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
|
|
34886
35008
|
const metaFile = join50(runDir, "meta.json");
|
|
34887
35009
|
const unsub = bus.on("run:started", (_ev) => {
|
|
34888
35010
|
(async () => {
|
|
@@ -35746,7 +35868,8 @@ async function executeSequential(ctx, initialPrd) {
|
|
|
35746
35868
|
story: prd.userStories[0],
|
|
35747
35869
|
stories: prd.userStories,
|
|
35748
35870
|
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "" },
|
|
35749
|
-
hooks: ctx.hooks
|
|
35871
|
+
hooks: ctx.hooks,
|
|
35872
|
+
agentGetFn: ctx.agentGetFn
|
|
35750
35873
|
};
|
|
35751
35874
|
await runPipeline(preRunPipeline, preRunCtx, ctx.eventEmitter);
|
|
35752
35875
|
while (iterations < ctx.config.execution.maxIterations) {
|
|
@@ -67545,7 +67668,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
|
|
|
67545
67668
|
// bin/nax.ts
|
|
67546
67669
|
init_source();
|
|
67547
67670
|
import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
|
|
67548
|
-
import { homedir as
|
|
67671
|
+
import { homedir as homedir8 } from "os";
|
|
67549
67672
|
import { join as join56 } from "path";
|
|
67550
67673
|
|
|
67551
67674
|
// node_modules/commander/esm.mjs
|
|
@@ -71088,10 +71211,10 @@ import { readdir as readdir3 } from "fs/promises";
|
|
|
71088
71211
|
import { join as join39 } from "path";
|
|
71089
71212
|
|
|
71090
71213
|
// src/utils/paths.ts
|
|
71091
|
-
import { homedir as
|
|
71214
|
+
import { homedir as homedir4 } from "os";
|
|
71092
71215
|
import { join as join38 } from "path";
|
|
71093
71216
|
function getRunsDir() {
|
|
71094
|
-
return process.env.NAX_RUNS_DIR ?? join38(
|
|
71217
|
+
return process.env.NAX_RUNS_DIR ?? join38(homedir4(), ".nax", "runs");
|
|
71095
71218
|
}
|
|
71096
71219
|
|
|
71097
71220
|
// src/commands/logs-reader.ts
|
|
@@ -79655,7 +79778,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
79655
79778
|
config2.autoMode.defaultAgent = options.agent;
|
|
79656
79779
|
}
|
|
79657
79780
|
config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
|
|
79658
|
-
const globalNaxDir = join56(
|
|
79781
|
+
const globalNaxDir = join56(homedir8(), ".nax");
|
|
79659
79782
|
const hooks = await loadHooksConfig(naxDir, globalNaxDir);
|
|
79660
79783
|
const eventEmitter = new PipelineEventEmitter;
|
|
79661
79784
|
let tuiInstance;
|