@nathapp/nax 0.54.1 → 0.54.3
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/dist/nax.js +479 -286
- package/package.json +1 -1
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: {
|
|
@@ -18781,7 +18786,7 @@ async function refineAcceptanceCriteria(criteria, context) {
|
|
|
18781
18786
|
if (criteria.length === 0) {
|
|
18782
18787
|
return [];
|
|
18783
18788
|
}
|
|
18784
|
-
const { storyId, codebaseContext, config: config2, testStrategy, testFramework } = context;
|
|
18789
|
+
const { storyId, featureName, workdir, codebaseContext, config: config2, testStrategy, testFramework } = context;
|
|
18785
18790
|
const logger = getLogger();
|
|
18786
18791
|
const modelTier = config2.acceptance?.model ?? "fast";
|
|
18787
18792
|
const modelEntry = config2.models[modelTier] ?? config2.models.fast;
|
|
@@ -18796,7 +18801,11 @@ async function refineAcceptanceCriteria(criteria, context) {
|
|
|
18796
18801
|
jsonMode: true,
|
|
18797
18802
|
maxTokens: 4096,
|
|
18798
18803
|
model: modelDef.model,
|
|
18799
|
-
config: config2
|
|
18804
|
+
config: config2,
|
|
18805
|
+
featureName,
|
|
18806
|
+
storyId,
|
|
18807
|
+
workdir,
|
|
18808
|
+
sessionRole: "refine"
|
|
18800
18809
|
});
|
|
18801
18810
|
} catch (error48) {
|
|
18802
18811
|
const reason = errorMessage(error48);
|
|
@@ -18859,13 +18868,13 @@ function skeletonImportLine(testFramework) {
|
|
|
18859
18868
|
function acceptanceTestFilename(language) {
|
|
18860
18869
|
switch (language?.toLowerCase()) {
|
|
18861
18870
|
case "go":
|
|
18862
|
-
return "acceptance_test.go";
|
|
18871
|
+
return ".nax-acceptance_test.go";
|
|
18863
18872
|
case "python":
|
|
18864
|
-
return "
|
|
18873
|
+
return ".nax-acceptance.test.py";
|
|
18865
18874
|
case "rust":
|
|
18866
|
-
return "
|
|
18875
|
+
return ".nax-acceptance.rs";
|
|
18867
18876
|
default:
|
|
18868
|
-
return "acceptance.test.ts";
|
|
18877
|
+
return ".nax-acceptance.test.ts";
|
|
18869
18878
|
}
|
|
18870
18879
|
}
|
|
18871
18880
|
function buildAcceptanceRunCommand(testPath, testFramework, commandOverride) {
|
|
@@ -18935,27 +18944,58 @@ Rules:
|
|
|
18935
18944
|
- **NEVER use placeholder assertions** \u2014 no always-passing or always-failing stubs, no TODO comments as the only content, no empty test bodies
|
|
18936
18945
|
- Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
|
|
18937
18946
|
- **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.
|
|
18938
|
-
-
|
|
18939
|
-
- **Path anchor (CRITICAL)**:
|
|
18947
|
+
- **File output (REQUIRED)**: Write the acceptance test file DIRECTLY to the path shown below. Do NOT output the test code in your response. After writing the file, reply with a brief confirmation.
|
|
18948
|
+
- **Path anchor (CRITICAL)**: Write the test file to this exact path: \`${options.featureDir}/${acceptanceTestFilename(options.language)}\`. Import from package sources using relative paths like \`./src/...\`. No deep \`../../../../\` traversal needed.`;
|
|
18940
18949
|
const prompt = basePrompt;
|
|
18941
18950
|
logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
|
|
18942
18951
|
const rawOutput = await (options.adapter ?? _generatorPRDDeps.adapter).complete(prompt, {
|
|
18943
18952
|
model: options.modelDef.model,
|
|
18944
18953
|
config: options.config,
|
|
18945
18954
|
timeoutMs: options.config?.acceptance?.timeoutMs ?? 1800000,
|
|
18946
|
-
workdir: options.workdir
|
|
18955
|
+
workdir: options.workdir,
|
|
18956
|
+
featureName: options.featureName,
|
|
18957
|
+
sessionRole: "acceptance-gen"
|
|
18947
18958
|
});
|
|
18948
18959
|
let testCode = extractTestCode(rawOutput);
|
|
18960
|
+
logger.debug("acceptance", "Received raw output from LLM", {
|
|
18961
|
+
hasCode: testCode !== null,
|
|
18962
|
+
outputLength: rawOutput.length,
|
|
18963
|
+
outputPreview: rawOutput.slice(0, 300)
|
|
18964
|
+
});
|
|
18949
18965
|
if (!testCode) {
|
|
18950
|
-
const targetPath = join2(options.featureDir,
|
|
18966
|
+
const targetPath = join2(options.featureDir, acceptanceTestFilename(options.language));
|
|
18967
|
+
let recoveryFailed = false;
|
|
18968
|
+
logger.debug("acceptance", "BUG-076 recovery: checking for agent-written file", { targetPath });
|
|
18951
18969
|
try {
|
|
18952
18970
|
const existing = await Bun.file(targetPath).text();
|
|
18953
18971
|
const recovered = extractTestCode(existing);
|
|
18972
|
+
logger.debug("acceptance", "BUG-076 recovery: file check result", {
|
|
18973
|
+
fileSize: existing.length,
|
|
18974
|
+
extractedCode: recovered !== null,
|
|
18975
|
+
filePreview: existing.slice(0, 300)
|
|
18976
|
+
});
|
|
18954
18977
|
if (recovered) {
|
|
18955
18978
|
logger.info("acceptance", "Acceptance test written directly by agent \u2014 using existing file", { targetPath });
|
|
18956
18979
|
testCode = recovered;
|
|
18980
|
+
} else {
|
|
18981
|
+
recoveryFailed = true;
|
|
18982
|
+
logger.error("acceptance", "BUG-076: ACP adapter wrote file but no code extractable \u2014 falling back to skeleton", {
|
|
18983
|
+
targetPath,
|
|
18984
|
+
filePreview: existing.slice(0, 300)
|
|
18985
|
+
});
|
|
18957
18986
|
}
|
|
18958
|
-
} catch {
|
|
18987
|
+
} catch {
|
|
18988
|
+
recoveryFailed = true;
|
|
18989
|
+
logger.debug("acceptance", "BUG-076 recovery: no file written by agent, falling back to skeleton", {
|
|
18990
|
+
targetPath,
|
|
18991
|
+
rawOutputPreview: rawOutput.slice(0, 500)
|
|
18992
|
+
});
|
|
18993
|
+
}
|
|
18994
|
+
if (recoveryFailed) {
|
|
18995
|
+
logger.error("acceptance", "BUG-076: LLM returned non-code output and no file was written by agent \u2014 falling back to skeleton", {
|
|
18996
|
+
rawOutputPreview: rawOutput.slice(0, 500)
|
|
18997
|
+
});
|
|
18998
|
+
}
|
|
18959
18999
|
}
|
|
18960
19000
|
if (!testCode) {
|
|
18961
19001
|
logger.warn("acceptance", "LLM returned non-code output for acceptance tests \u2014 falling back to skeleton", {
|
|
@@ -19056,7 +19096,9 @@ async function generateAcceptanceTests(adapter, options) {
|
|
|
19056
19096
|
model: options.modelDef.model,
|
|
19057
19097
|
config: options.config,
|
|
19058
19098
|
timeoutMs: options.config?.acceptance?.timeoutMs ?? 1800000,
|
|
19059
|
-
workdir: options.workdir
|
|
19099
|
+
workdir: options.workdir,
|
|
19100
|
+
featureName: options.featureName,
|
|
19101
|
+
sessionRole: "acceptance-gen"
|
|
19060
19102
|
});
|
|
19061
19103
|
const testCode = extractTestCode(output);
|
|
19062
19104
|
if (!testCode) {
|
|
@@ -19307,7 +19349,10 @@ async function generateFixStories(adapter, options) {
|
|
|
19307
19349
|
try {
|
|
19308
19350
|
const fixDescription = await adapter.complete(prompt, {
|
|
19309
19351
|
model: modelDef.model,
|
|
19310
|
-
config: options.config
|
|
19352
|
+
config: options.config,
|
|
19353
|
+
featureName: options.prd.feature,
|
|
19354
|
+
workdir: options.workdir,
|
|
19355
|
+
sessionRole: "fix-gen"
|
|
19311
19356
|
});
|
|
19312
19357
|
fixStories.push({
|
|
19313
19358
|
id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
|
|
@@ -19471,37 +19516,6 @@ function parseAcpxJsonOutput(rawOutput) {
|
|
|
19471
19516
|
}
|
|
19472
19517
|
|
|
19473
19518
|
// src/agents/acp/spawn-client.ts
|
|
19474
|
-
import { homedir as homedir2 } from "os";
|
|
19475
|
-
import { isAbsolute as isAbsolute2 } from "path";
|
|
19476
|
-
function buildAllowedEnv2(extraEnv) {
|
|
19477
|
-
const allowed = {};
|
|
19478
|
-
const essentialVars = ["PATH", "TMPDIR", "NODE_ENV", "USER", "LOGNAME"];
|
|
19479
|
-
for (const varName of essentialVars) {
|
|
19480
|
-
if (process.env[varName])
|
|
19481
|
-
allowed[varName] = process.env[varName];
|
|
19482
|
-
}
|
|
19483
|
-
const rawHome = process.env.HOME ?? "";
|
|
19484
|
-
const safeHome = rawHome && isAbsolute2(rawHome) ? rawHome : homedir2();
|
|
19485
|
-
if (rawHome !== safeHome) {
|
|
19486
|
-
getSafeLogger()?.warn("env", `HOME env is not absolute ("${rawHome}"), falling back to os.homedir(): ${safeHome}`);
|
|
19487
|
-
}
|
|
19488
|
-
allowed.HOME = safeHome;
|
|
19489
|
-
const apiKeyVars = ["ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", "GOOGLE_API_KEY", "CLAUDE_API_KEY"];
|
|
19490
|
-
for (const varName of apiKeyVars) {
|
|
19491
|
-
if (process.env[varName])
|
|
19492
|
-
allowed[varName] = process.env[varName];
|
|
19493
|
-
}
|
|
19494
|
-
const allowedPrefixes = ["CLAUDE_", "NAX_", "CLAW_", "TURBO_", "ACPX_", "CODEX_", "GEMINI_"];
|
|
19495
|
-
for (const [key, value] of Object.entries(process.env)) {
|
|
19496
|
-
if (allowedPrefixes.some((prefix) => key.startsWith(prefix))) {
|
|
19497
|
-
allowed[key] = value;
|
|
19498
|
-
}
|
|
19499
|
-
}
|
|
19500
|
-
if (extraEnv)
|
|
19501
|
-
Object.assign(allowed, extraEnv);
|
|
19502
|
-
return allowed;
|
|
19503
|
-
}
|
|
19504
|
-
|
|
19505
19519
|
class SpawnAcpSession {
|
|
19506
19520
|
agentName;
|
|
19507
19521
|
sessionName;
|
|
@@ -19591,7 +19605,7 @@ class SpawnAcpSession {
|
|
|
19591
19605
|
await this.pidRegistry?.unregister(processPid);
|
|
19592
19606
|
}
|
|
19593
19607
|
}
|
|
19594
|
-
async close() {
|
|
19608
|
+
async close(options) {
|
|
19595
19609
|
if (this.activeProc) {
|
|
19596
19610
|
try {
|
|
19597
19611
|
this.activeProc.kill(15);
|
|
@@ -19610,6 +19624,14 @@ class SpawnAcpSession {
|
|
|
19610
19624
|
stderr: stderr.slice(0, 200)
|
|
19611
19625
|
});
|
|
19612
19626
|
}
|
|
19627
|
+
if (options?.forceTerminate) {
|
|
19628
|
+
try {
|
|
19629
|
+
const stopProc = _spawnClientDeps.spawn(["acpx", this.agentName, "stop"], { stdout: "pipe", stderr: "pipe" });
|
|
19630
|
+
await stopProc.exited;
|
|
19631
|
+
} catch (err) {
|
|
19632
|
+
getSafeLogger()?.debug("acp-adapter", "acpx stop failed (swallowed)", { cause: String(err) });
|
|
19633
|
+
}
|
|
19634
|
+
}
|
|
19613
19635
|
}
|
|
19614
19636
|
async cancelActivePrompt() {
|
|
19615
19637
|
if (this.activeProc) {
|
|
@@ -19643,7 +19665,7 @@ class SpawnAcpClient {
|
|
|
19643
19665
|
this.agentName = lastToken;
|
|
19644
19666
|
this.cwd = cwd || process.cwd();
|
|
19645
19667
|
this.timeoutSeconds = timeoutSeconds || 1800;
|
|
19646
|
-
this.env =
|
|
19668
|
+
this.env = buildAllowedEnv();
|
|
19647
19669
|
this.pidRegistry = pidRegistry;
|
|
19648
19670
|
}
|
|
19649
19671
|
async start() {}
|
|
@@ -19695,6 +19717,7 @@ var _spawnClientDeps;
|
|
|
19695
19717
|
var init_spawn_client = __esm(() => {
|
|
19696
19718
|
init_logger2();
|
|
19697
19719
|
init_bun_deps();
|
|
19720
|
+
init_env();
|
|
19698
19721
|
_spawnClientDeps = {
|
|
19699
19722
|
spawn: typedSpawn
|
|
19700
19723
|
};
|
|
@@ -20116,11 +20139,13 @@ class AcpAgentAdapter {
|
|
|
20116
20139
|
const client = _acpAdapterDeps.createClient(cmdStr, workdir);
|
|
20117
20140
|
await client.start();
|
|
20118
20141
|
let session = null;
|
|
20142
|
+
let hadError = false;
|
|
20119
20143
|
try {
|
|
20144
|
+
const completeSessionName = _options?.sessionName ?? buildSessionName(workdir ?? process.cwd(), _options?.featureName, _options?.storyId, _options?.sessionRole);
|
|
20120
20145
|
session = await client.createSession({
|
|
20121
20146
|
agentName: this.name,
|
|
20122
20147
|
permissionMode,
|
|
20123
|
-
sessionName:
|
|
20148
|
+
sessionName: completeSessionName
|
|
20124
20149
|
});
|
|
20125
20150
|
let timeoutId;
|
|
20126
20151
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -20157,6 +20182,7 @@ class AcpAgentAdapter {
|
|
|
20157
20182
|
}
|
|
20158
20183
|
return unwrapped;
|
|
20159
20184
|
} catch (err) {
|
|
20185
|
+
hadError = true;
|
|
20160
20186
|
const error48 = err instanceof Error ? err : new Error(String(err));
|
|
20161
20187
|
lastError = error48;
|
|
20162
20188
|
const shouldRetry = isRateLimitError(error48) && attempt < MAX_RATE_LIMIT_RETRIES - 1;
|
|
@@ -20170,7 +20196,7 @@ class AcpAgentAdapter {
|
|
|
20170
20196
|
await _acpAdapterDeps.sleep(backoffMs);
|
|
20171
20197
|
} finally {
|
|
20172
20198
|
if (session) {
|
|
20173
|
-
await session.close().catch(() => {});
|
|
20199
|
+
await session.close({ forceTerminate: hadError }).catch(() => {});
|
|
20174
20200
|
}
|
|
20175
20201
|
await client.close().catch(() => {});
|
|
20176
20202
|
}
|
|
@@ -20225,7 +20251,9 @@ class AcpAgentAdapter {
|
|
|
20225
20251
|
output = await this.complete(prompt, {
|
|
20226
20252
|
model,
|
|
20227
20253
|
jsonMode: true,
|
|
20228
|
-
config: options.config
|
|
20254
|
+
config: options.config,
|
|
20255
|
+
workdir: options.workdir,
|
|
20256
|
+
sessionRole: "decompose"
|
|
20229
20257
|
});
|
|
20230
20258
|
} catch (err) {
|
|
20231
20259
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -20978,7 +21006,7 @@ function isPlainObject2(value) {
|
|
|
20978
21006
|
|
|
20979
21007
|
// src/config/path-security.ts
|
|
20980
21008
|
import { existsSync as existsSync4, lstatSync, realpathSync } from "fs";
|
|
20981
|
-
import { isAbsolute as
|
|
21009
|
+
import { isAbsolute as isAbsolute2, normalize, resolve } from "path";
|
|
20982
21010
|
function validateDirectory(dirPath, baseDir) {
|
|
20983
21011
|
const resolved = resolve(dirPath);
|
|
20984
21012
|
if (!existsSync4(resolved)) {
|
|
@@ -21010,7 +21038,7 @@ function validateDirectory(dirPath, baseDir) {
|
|
|
21010
21038
|
function isWithinDirectory(targetPath, basePath) {
|
|
21011
21039
|
const normalizedTarget = normalize(targetPath);
|
|
21012
21040
|
const normalizedBase = normalize(basePath);
|
|
21013
|
-
if (!
|
|
21041
|
+
if (!isAbsolute2(normalizedTarget) || !isAbsolute2(normalizedBase)) {
|
|
21014
21042
|
return false;
|
|
21015
21043
|
}
|
|
21016
21044
|
const baseWithSlash = normalizedBase.endsWith("/") ? normalizedBase : `${normalizedBase}/`;
|
|
@@ -21046,10 +21074,10 @@ var MAX_DIRECTORY_DEPTH = 10;
|
|
|
21046
21074
|
var init_path_security = () => {};
|
|
21047
21075
|
|
|
21048
21076
|
// src/config/paths.ts
|
|
21049
|
-
import { homedir as
|
|
21077
|
+
import { homedir as homedir2 } from "os";
|
|
21050
21078
|
import { join as join5, resolve as resolve2 } from "path";
|
|
21051
21079
|
function globalConfigDir() {
|
|
21052
|
-
return join5(
|
|
21080
|
+
return join5(homedir2(), ".nax");
|
|
21053
21081
|
}
|
|
21054
21082
|
var PROJECT_NAX_DIR = ".nax";
|
|
21055
21083
|
var init_paths = () => {};
|
|
@@ -22320,7 +22348,7 @@ var package_default;
|
|
|
22320
22348
|
var init_package = __esm(() => {
|
|
22321
22349
|
package_default = {
|
|
22322
22350
|
name: "@nathapp/nax",
|
|
22323
|
-
version: "0.54.
|
|
22351
|
+
version: "0.54.3",
|
|
22324
22352
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
22325
22353
|
type: "module",
|
|
22326
22354
|
bin: {
|
|
@@ -22397,8 +22425,8 @@ var init_version = __esm(() => {
|
|
|
22397
22425
|
NAX_VERSION = package_default.version;
|
|
22398
22426
|
NAX_COMMIT = (() => {
|
|
22399
22427
|
try {
|
|
22400
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
22401
|
-
return "
|
|
22428
|
+
if (/^[0-9a-f]{6,10}$/.test("5acee1f"))
|
|
22429
|
+
return "5acee1f";
|
|
22402
22430
|
} catch {}
|
|
22403
22431
|
try {
|
|
22404
22432
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -23642,7 +23670,10 @@ class AutoInteractionPlugin {
|
|
|
23642
23670
|
const output = await adapter.complete(prompt, {
|
|
23643
23671
|
...modelArg && { model: modelArg },
|
|
23644
23672
|
jsonMode: true,
|
|
23645
|
-
...this.config.naxConfig && { config: this.config.naxConfig }
|
|
23673
|
+
...this.config.naxConfig && { config: this.config.naxConfig },
|
|
23674
|
+
featureName: request.featureName,
|
|
23675
|
+
storyId: request.storyId,
|
|
23676
|
+
sessionRole: "auto"
|
|
23646
23677
|
});
|
|
23647
23678
|
return this.parseResponse(output);
|
|
23648
23679
|
}
|
|
@@ -24085,53 +24116,44 @@ var init_acceptance2 = __esm(() => {
|
|
|
24085
24116
|
logger.warn("acceptance", "No feature directory \u2014 skipping acceptance tests");
|
|
24086
24117
|
return { action: "continue" };
|
|
24087
24118
|
}
|
|
24088
|
-
const
|
|
24089
|
-
|
|
24090
|
-
|
|
24091
|
-
|
|
24092
|
-
|
|
24093
|
-
|
|
24119
|
+
const testGroups = ctx.acceptanceTestPaths ?? [
|
|
24120
|
+
{
|
|
24121
|
+
testPath: path4.join(ctx.featureDir, effectiveConfig.acceptance.testPath),
|
|
24122
|
+
packageDir: ctx.workdir
|
|
24123
|
+
}
|
|
24124
|
+
];
|
|
24125
|
+
const allFailedACs = [];
|
|
24126
|
+
const allOutputParts = [];
|
|
24127
|
+
let anyError = false;
|
|
24128
|
+
let errorExitCode = 0;
|
|
24129
|
+
for (const { testPath, packageDir } of testGroups) {
|
|
24130
|
+
const testFile = Bun.file(testPath);
|
|
24131
|
+
const exists = await testFile.exists();
|
|
24132
|
+
if (!exists) {
|
|
24133
|
+
logger.warn("acceptance", "Acceptance test file not found \u2014 skipping", { testPath });
|
|
24134
|
+
continue;
|
|
24135
|
+
}
|
|
24136
|
+
const testCmdParts = buildAcceptanceRunCommand(testPath, effectiveConfig.project?.testFramework, effectiveConfig.acceptance.command);
|
|
24137
|
+
logger.info("acceptance", "Running acceptance command", {
|
|
24138
|
+
cmd: testCmdParts.join(" "),
|
|
24139
|
+
packageDir
|
|
24094
24140
|
});
|
|
24095
|
-
|
|
24096
|
-
|
|
24097
|
-
|
|
24098
|
-
|
|
24099
|
-
|
|
24100
|
-
|
|
24101
|
-
|
|
24102
|
-
|
|
24103
|
-
|
|
24104
|
-
|
|
24105
|
-
|
|
24106
|
-
new Response(proc.stdout).text(),
|
|
24107
|
-
new Response(proc.stderr).text()
|
|
24108
|
-
]);
|
|
24109
|
-
const output = `${stdout}
|
|
24141
|
+
const proc = Bun.spawn(testCmdParts, {
|
|
24142
|
+
cwd: packageDir,
|
|
24143
|
+
stdout: "pipe",
|
|
24144
|
+
stderr: "pipe"
|
|
24145
|
+
});
|
|
24146
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
24147
|
+
proc.exited,
|
|
24148
|
+
new Response(proc.stdout).text(),
|
|
24149
|
+
new Response(proc.stderr).text()
|
|
24150
|
+
]);
|
|
24151
|
+
const output = `${stdout}
|
|
24110
24152
|
${stderr}`;
|
|
24111
|
-
|
|
24112
|
-
|
|
24113
|
-
|
|
24114
|
-
|
|
24115
|
-
logger.info("acceptance", "All acceptance tests passed");
|
|
24116
|
-
return { action: "continue" };
|
|
24117
|
-
}
|
|
24118
|
-
if (failedACs.length > 0 && actualFailures.length === 0) {
|
|
24119
|
-
logger.info("acceptance", "All failed ACs are overridden \u2014 treating as pass");
|
|
24120
|
-
return { action: "continue" };
|
|
24121
|
-
}
|
|
24122
|
-
if (failedACs.length === 0 && exitCode !== 0) {
|
|
24123
|
-
logger.error("acceptance", "Tests errored with no AC failures parsed", { exitCode });
|
|
24124
|
-
logTestOutput(logger, "acceptance", output);
|
|
24125
|
-
ctx.acceptanceFailures = {
|
|
24126
|
-
failedACs: ["AC-ERROR"],
|
|
24127
|
-
testOutput: output
|
|
24128
|
-
};
|
|
24129
|
-
return {
|
|
24130
|
-
action: "fail",
|
|
24131
|
-
reason: `Acceptance tests errored (exit code ${exitCode}): syntax error, import failure, or unhandled exception`
|
|
24132
|
-
};
|
|
24133
|
-
}
|
|
24134
|
-
if (actualFailures.length > 0) {
|
|
24153
|
+
allOutputParts.push(output);
|
|
24154
|
+
const failedACs = parseTestFailures(output);
|
|
24155
|
+
const overrides = ctx.prd.acceptanceOverrides ?? {};
|
|
24156
|
+
const actualFailures = failedACs.filter((acId) => !overrides[acId]);
|
|
24135
24157
|
const overriddenFailures = failedACs.filter((acId) => overrides[acId]);
|
|
24136
24158
|
if (overriddenFailures.length > 0) {
|
|
24137
24159
|
logger.warn("acceptance", "Skipped failures (overridden)", {
|
|
@@ -24139,19 +24161,52 @@ ${stderr}`;
|
|
|
24139
24161
|
overrides: overriddenFailures.map((acId) => ({ acId, reason: overrides[acId] }))
|
|
24140
24162
|
});
|
|
24141
24163
|
}
|
|
24142
|
-
|
|
24143
|
-
|
|
24144
|
-
|
|
24145
|
-
|
|
24146
|
-
|
|
24147
|
-
|
|
24164
|
+
if (failedACs.length === 0 && exitCode !== 0) {
|
|
24165
|
+
logger.error("acceptance", "Tests errored with no AC failures parsed", {
|
|
24166
|
+
exitCode,
|
|
24167
|
+
packageDir
|
|
24168
|
+
});
|
|
24169
|
+
logTestOutput(logger, "acceptance", output);
|
|
24170
|
+
anyError = true;
|
|
24171
|
+
errorExitCode = exitCode;
|
|
24172
|
+
allFailedACs.push("AC-ERROR");
|
|
24173
|
+
continue;
|
|
24174
|
+
}
|
|
24175
|
+
for (const acId of actualFailures) {
|
|
24176
|
+
if (!allFailedACs.includes(acId)) {
|
|
24177
|
+
allFailedACs.push(acId);
|
|
24178
|
+
}
|
|
24179
|
+
}
|
|
24180
|
+
if (actualFailures.length > 0) {
|
|
24181
|
+
logger.error("acceptance", "Acceptance tests failed", {
|
|
24182
|
+
failedACs: actualFailures,
|
|
24183
|
+
packageDir
|
|
24184
|
+
});
|
|
24185
|
+
logTestOutput(logger, "acceptance", output);
|
|
24186
|
+
} else if (exitCode === 0) {
|
|
24187
|
+
logger.info("acceptance", "Package acceptance tests passed", { packageDir });
|
|
24188
|
+
}
|
|
24189
|
+
}
|
|
24190
|
+
const combinedOutput = allOutputParts.join(`
|
|
24191
|
+
`);
|
|
24192
|
+
if (allFailedACs.length === 0) {
|
|
24193
|
+
logger.info("acceptance", "All acceptance tests passed");
|
|
24194
|
+
return { action: "continue" };
|
|
24195
|
+
}
|
|
24196
|
+
ctx.acceptanceFailures = {
|
|
24197
|
+
failedACs: allFailedACs,
|
|
24198
|
+
testOutput: combinedOutput
|
|
24199
|
+
};
|
|
24200
|
+
if (anyError) {
|
|
24148
24201
|
return {
|
|
24149
24202
|
action: "fail",
|
|
24150
|
-
reason: `Acceptance tests
|
|
24203
|
+
reason: `Acceptance tests errored (exit code ${errorExitCode}): syntax error, import failure, or unhandled exception`
|
|
24151
24204
|
};
|
|
24152
24205
|
}
|
|
24153
|
-
|
|
24154
|
-
|
|
24206
|
+
return {
|
|
24207
|
+
action: "fail",
|
|
24208
|
+
reason: `Acceptance tests failed: ${allFailedACs.join(", ")}`
|
|
24209
|
+
};
|
|
24155
24210
|
}
|
|
24156
24211
|
};
|
|
24157
24212
|
});
|
|
@@ -24333,82 +24388,142 @@ ${stderr}` };
|
|
|
24333
24388
|
return { action: "fail", reason: "[acceptance-setup] featureDir is not set" };
|
|
24334
24389
|
}
|
|
24335
24390
|
const language = (ctx.effectiveConfig ?? ctx.config).project?.language;
|
|
24336
|
-
const testPath = path5.join(ctx.featureDir, acceptanceTestFilename(language));
|
|
24337
24391
|
const metaPath = path5.join(ctx.featureDir, "acceptance-meta.json");
|
|
24338
24392
|
const allCriteria = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-")).flatMap((s) => s.acceptanceCriteria);
|
|
24393
|
+
const nonFixStories = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-"));
|
|
24394
|
+
const workdirGroups = new Map;
|
|
24395
|
+
for (const story of nonFixStories) {
|
|
24396
|
+
const wd = story.workdir ?? "";
|
|
24397
|
+
if (!workdirGroups.has(wd)) {
|
|
24398
|
+
workdirGroups.set(wd, { stories: [], criteria: [] });
|
|
24399
|
+
}
|
|
24400
|
+
const group = workdirGroups.get(wd);
|
|
24401
|
+
if (group) {
|
|
24402
|
+
group.stories.push(story);
|
|
24403
|
+
group.criteria.push(...story.acceptanceCriteria);
|
|
24404
|
+
}
|
|
24405
|
+
}
|
|
24406
|
+
if (workdirGroups.size === 0) {
|
|
24407
|
+
workdirGroups.set("", { stories: [], criteria: [] });
|
|
24408
|
+
}
|
|
24409
|
+
const testPaths = [];
|
|
24410
|
+
for (const [workdir] of workdirGroups) {
|
|
24411
|
+
const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
|
|
24412
|
+
const testPath = path5.join(packageDir, acceptanceTestFilename(language));
|
|
24413
|
+
testPaths.push({ testPath, packageDir });
|
|
24414
|
+
}
|
|
24339
24415
|
let totalCriteria = 0;
|
|
24340
24416
|
let testableCount = 0;
|
|
24341
|
-
const
|
|
24342
|
-
|
|
24343
|
-
|
|
24344
|
-
|
|
24345
|
-
|
|
24346
|
-
|
|
24347
|
-
|
|
24348
|
-
|
|
24349
|
-
|
|
24417
|
+
const fingerprint = computeACFingerprint(allCriteria);
|
|
24418
|
+
const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
|
|
24419
|
+
getSafeLogger()?.debug("acceptance-setup", "Fingerprint check", {
|
|
24420
|
+
currentFingerprint: fingerprint,
|
|
24421
|
+
storedFingerprint: meta3?.acFingerprint ?? "none",
|
|
24422
|
+
match: meta3?.acFingerprint === fingerprint
|
|
24423
|
+
});
|
|
24424
|
+
let shouldGenerate = false;
|
|
24425
|
+
if (!meta3 || meta3.acFingerprint !== fingerprint) {
|
|
24426
|
+
if (!meta3) {
|
|
24427
|
+
getSafeLogger()?.info("acceptance-setup", "No acceptance meta \u2014 generating acceptance tests");
|
|
24428
|
+
} else {
|
|
24429
|
+
getSafeLogger()?.info("acceptance-setup", "ACs changed \u2014 regenerating acceptance tests", {
|
|
24430
|
+
reason: "fingerprint mismatch",
|
|
24431
|
+
currentFingerprint: fingerprint,
|
|
24432
|
+
storedFingerprint: meta3.acFingerprint
|
|
24433
|
+
});
|
|
24434
|
+
}
|
|
24435
|
+
for (const { testPath } of testPaths) {
|
|
24436
|
+
if (await _acceptanceSetupDeps.fileExists(testPath)) {
|
|
24437
|
+
await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
|
|
24438
|
+
await _acceptanceSetupDeps.deleteFile(testPath);
|
|
24439
|
+
}
|
|
24350
24440
|
}
|
|
24441
|
+
shouldGenerate = true;
|
|
24442
|
+
} else {
|
|
24443
|
+
getSafeLogger()?.info("acceptance-setup", "Reusing existing acceptance tests (fingerprint match)");
|
|
24351
24444
|
}
|
|
24352
24445
|
if (shouldGenerate) {
|
|
24353
24446
|
totalCriteria = allCriteria.length;
|
|
24354
24447
|
const { getAgent: getAgent2 } = await Promise.resolve().then(() => (init_agents(), exports_agents));
|
|
24355
24448
|
const agent = (ctx.agentGetFn ?? _acceptanceSetupDeps.getAgent)(ctx.config.autoMode.defaultAgent);
|
|
24356
|
-
let
|
|
24449
|
+
let allRefinedCriteria;
|
|
24357
24450
|
if (ctx.config.acceptance.refinement) {
|
|
24358
|
-
|
|
24359
|
-
|
|
24360
|
-
|
|
24361
|
-
|
|
24362
|
-
|
|
24363
|
-
|
|
24364
|
-
|
|
24451
|
+
allRefinedCriteria = [];
|
|
24452
|
+
for (const story of nonFixStories) {
|
|
24453
|
+
const storyRefined = await _acceptanceSetupDeps.refine(story.acceptanceCriteria, {
|
|
24454
|
+
storyId: story.id,
|
|
24455
|
+
featureName: ctx.prd.feature,
|
|
24456
|
+
workdir: ctx.workdir,
|
|
24457
|
+
codebaseContext: "",
|
|
24458
|
+
config: ctx.config,
|
|
24459
|
+
testStrategy: ctx.config.acceptance.testStrategy,
|
|
24460
|
+
testFramework: ctx.config.acceptance.testFramework
|
|
24461
|
+
});
|
|
24462
|
+
allRefinedCriteria = allRefinedCriteria.concat(storyRefined);
|
|
24463
|
+
}
|
|
24365
24464
|
} else {
|
|
24366
|
-
|
|
24465
|
+
allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
|
|
24367
24466
|
original: c,
|
|
24368
24467
|
refined: c,
|
|
24369
24468
|
testable: true,
|
|
24370
|
-
storyId:
|
|
24371
|
-
}));
|
|
24469
|
+
storyId: story.id
|
|
24470
|
+
})));
|
|
24372
24471
|
}
|
|
24373
|
-
testableCount =
|
|
24374
|
-
const
|
|
24375
|
-
|
|
24376
|
-
|
|
24377
|
-
|
|
24378
|
-
|
|
24379
|
-
|
|
24380
|
-
|
|
24381
|
-
|
|
24382
|
-
|
|
24383
|
-
|
|
24384
|
-
|
|
24385
|
-
|
|
24386
|
-
|
|
24387
|
-
|
|
24472
|
+
testableCount = allRefinedCriteria.filter((r) => r.testable).length;
|
|
24473
|
+
for (const [workdir, group] of workdirGroups) {
|
|
24474
|
+
const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
|
|
24475
|
+
const testPath = path5.join(packageDir, acceptanceTestFilename(language));
|
|
24476
|
+
const groupStoryIds = new Set(group.stories.map((s) => s.id));
|
|
24477
|
+
const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
|
|
24478
|
+
const result = await _acceptanceSetupDeps.generate(group.stories, groupRefined, {
|
|
24479
|
+
featureName: ctx.prd.feature,
|
|
24480
|
+
workdir: packageDir,
|
|
24481
|
+
featureDir: ctx.featureDir,
|
|
24482
|
+
codebaseContext: "",
|
|
24483
|
+
modelTier: ctx.config.acceptance.model ?? "fast",
|
|
24484
|
+
modelDef: resolveModel(ctx.config.models[ctx.config.acceptance.model ?? "fast"]),
|
|
24485
|
+
config: ctx.config,
|
|
24486
|
+
testStrategy: ctx.config.acceptance.testStrategy,
|
|
24487
|
+
testFramework: ctx.config.acceptance.testFramework,
|
|
24488
|
+
adapter: agent ?? undefined
|
|
24489
|
+
});
|
|
24490
|
+
await _acceptanceSetupDeps.writeFile(testPath, result.testCode);
|
|
24491
|
+
}
|
|
24492
|
+
const fingerprint2 = computeACFingerprint(allCriteria);
|
|
24388
24493
|
await _acceptanceSetupDeps.writeMeta(metaPath, {
|
|
24389
24494
|
generatedAt: new Date().toISOString(),
|
|
24390
|
-
acFingerprint:
|
|
24495
|
+
acFingerprint: fingerprint2,
|
|
24391
24496
|
storyCount: ctx.prd.userStories.length,
|
|
24392
24497
|
acCount: totalCriteria,
|
|
24393
24498
|
generator: "nax"
|
|
24394
24499
|
});
|
|
24395
24500
|
}
|
|
24501
|
+
ctx.acceptanceTestPaths = testPaths;
|
|
24396
24502
|
if (ctx.config.acceptance.redGate === false) {
|
|
24397
24503
|
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
|
|
24398
24504
|
return { action: "continue" };
|
|
24399
24505
|
}
|
|
24400
24506
|
const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
|
|
24401
|
-
|
|
24402
|
-
|
|
24403
|
-
|
|
24404
|
-
|
|
24507
|
+
let redFailCount = 0;
|
|
24508
|
+
for (const { testPath, packageDir } of testPaths) {
|
|
24509
|
+
const runCmd = buildAcceptanceRunCommand(testPath, effectiveConfig.project?.testFramework, effectiveConfig.acceptance.command);
|
|
24510
|
+
getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", {
|
|
24511
|
+
cmd: runCmd.join(" "),
|
|
24512
|
+
packageDir
|
|
24513
|
+
});
|
|
24514
|
+
const { exitCode } = await _acceptanceSetupDeps.runTest(testPath, packageDir, runCmd);
|
|
24515
|
+
if (exitCode !== 0) {
|
|
24516
|
+
redFailCount++;
|
|
24517
|
+
}
|
|
24518
|
+
}
|
|
24519
|
+
if (redFailCount === 0) {
|
|
24405
24520
|
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount: 0 };
|
|
24406
24521
|
return {
|
|
24407
24522
|
action: "skip",
|
|
24408
24523
|
reason: "[acceptance-setup] Acceptance tests already pass \u2014 they are not testing new behavior. Skipping acceptance gate."
|
|
24409
24524
|
};
|
|
24410
24525
|
}
|
|
24411
|
-
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount
|
|
24526
|
+
ctx.acceptanceSetup = { totalCriteria, testableCount, redFailCount };
|
|
24412
24527
|
return { action: "continue" };
|
|
24413
24528
|
}
|
|
24414
24529
|
};
|
|
@@ -24736,7 +24851,11 @@ If the implementation looks correct, respond with { "passed": true, "findings":
|
|
|
24736
24851
|
}
|
|
24737
24852
|
function parseLLMResponse(raw) {
|
|
24738
24853
|
try {
|
|
24739
|
-
|
|
24854
|
+
let cleaned = raw.trim();
|
|
24855
|
+
const fenceMatch = cleaned.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?\s*```$/);
|
|
24856
|
+
if (fenceMatch)
|
|
24857
|
+
cleaned = fenceMatch[1].trim();
|
|
24858
|
+
const parsed = JSON.parse(cleaned);
|
|
24740
24859
|
if (typeof parsed !== "object" || parsed === null)
|
|
24741
24860
|
return null;
|
|
24742
24861
|
const obj = parsed;
|
|
@@ -24784,6 +24903,7 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
|
|
|
24784
24903
|
durationMs: Date.now() - startTime
|
|
24785
24904
|
};
|
|
24786
24905
|
}
|
|
24906
|
+
logger?.info("review", "Running semantic check", { storyId: story.id, modelTier: semanticConfig.modelTier });
|
|
24787
24907
|
const rawDiff = await collectDiff(workdir, storyGitRef);
|
|
24788
24908
|
const diff = truncateDiff(rawDiff);
|
|
24789
24909
|
const agent = modelResolver(semanticConfig.modelTier);
|
|
@@ -24803,7 +24923,11 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
|
|
|
24803
24923
|
const prompt = buildPrompt(story, semanticConfig, diff);
|
|
24804
24924
|
let rawResponse;
|
|
24805
24925
|
try {
|
|
24806
|
-
rawResponse = await agent.complete(prompt
|
|
24926
|
+
rawResponse = await agent.complete(prompt, {
|
|
24927
|
+
sessionName: `nax-semantic-${story.id}`,
|
|
24928
|
+
workdir,
|
|
24929
|
+
timeoutMs: semanticConfig.timeoutMs
|
|
24930
|
+
});
|
|
24807
24931
|
} catch (err) {
|
|
24808
24932
|
logger?.warn("semantic", "LLM call failed \u2014 fail-open", { cause: String(err) });
|
|
24809
24933
|
return {
|
|
@@ -24828,6 +24952,21 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
|
|
|
24828
24952
|
};
|
|
24829
24953
|
}
|
|
24830
24954
|
if (!parsed.passed && parsed.findings.length > 0) {
|
|
24955
|
+
const durationMs2 = Date.now() - startTime;
|
|
24956
|
+
logger?.warn("review", `Semantic review failed: ${parsed.findings.length} findings`, {
|
|
24957
|
+
storyId: story.id,
|
|
24958
|
+
durationMs: durationMs2
|
|
24959
|
+
});
|
|
24960
|
+
logger?.debug("review", "Semantic review findings", {
|
|
24961
|
+
storyId: story.id,
|
|
24962
|
+
findings: parsed.findings.map((f) => ({
|
|
24963
|
+
severity: f.severity,
|
|
24964
|
+
file: f.file,
|
|
24965
|
+
line: f.line,
|
|
24966
|
+
issue: f.issue,
|
|
24967
|
+
suggestion: f.suggestion
|
|
24968
|
+
}))
|
|
24969
|
+
});
|
|
24831
24970
|
const output = `Semantic review failed:
|
|
24832
24971
|
|
|
24833
24972
|
${formatFindings(parsed.findings)}`;
|
|
@@ -24837,17 +24976,21 @@ ${formatFindings(parsed.findings)}`;
|
|
|
24837
24976
|
command: "",
|
|
24838
24977
|
exitCode: 1,
|
|
24839
24978
|
output,
|
|
24840
|
-
durationMs:
|
|
24979
|
+
durationMs: durationMs2,
|
|
24841
24980
|
findings: toReviewFindings(parsed.findings)
|
|
24842
24981
|
};
|
|
24843
24982
|
}
|
|
24983
|
+
const durationMs = Date.now() - startTime;
|
|
24984
|
+
if (parsed.passed) {
|
|
24985
|
+
logger?.info("review", "Semantic review passed", { storyId: story.id, durationMs });
|
|
24986
|
+
}
|
|
24844
24987
|
return {
|
|
24845
24988
|
check: "semantic",
|
|
24846
24989
|
success: parsed.passed,
|
|
24847
24990
|
command: "",
|
|
24848
24991
|
exitCode: parsed.passed ? 0 : 1,
|
|
24849
24992
|
output: parsed.passed ? "Semantic review passed" : "Semantic review failed (no findings)",
|
|
24850
|
-
durationMs
|
|
24993
|
+
durationMs
|
|
24851
24994
|
};
|
|
24852
24995
|
}
|
|
24853
24996
|
var _semanticDeps, DIFF_CAP_BYTES = 12288, DEFAULT_RULES;
|
|
@@ -25030,7 +25173,8 @@ async function runReview(config2, workdir, executionConfig, qualityCommands, sto
|
|
|
25030
25173
|
/nax\/features\/[^/]+\/acceptance-refined\.json$/,
|
|
25031
25174
|
/\.nax-verifier-verdict\.json$/,
|
|
25032
25175
|
/\.nax-pids$/,
|
|
25033
|
-
/\.nax-wt
|
|
25176
|
+
/\.nax-wt\//,
|
|
25177
|
+
/\.nax-acceptance[^/]*$/
|
|
25034
25178
|
];
|
|
25035
25179
|
const uncommittedFiles = allUncommittedFiles.filter((f) => !NAX_RUNTIME_PATTERNS.some((pattern) => pattern.test(f)));
|
|
25036
25180
|
if (uncommittedFiles.length > 0) {
|
|
@@ -25055,7 +25199,11 @@ Stage and commit these files before running review.`
|
|
|
25055
25199
|
description: story?.description ?? "",
|
|
25056
25200
|
acceptanceCriteria: story?.acceptanceCriteria ?? []
|
|
25057
25201
|
};
|
|
25058
|
-
const semanticCfg = config2.semantic ?? {
|
|
25202
|
+
const semanticCfg = config2.semantic ?? {
|
|
25203
|
+
modelTier: "balanced",
|
|
25204
|
+
rules: [],
|
|
25205
|
+
timeoutMs: 600000
|
|
25206
|
+
};
|
|
25059
25207
|
const result2 = await _reviewSemanticDeps.runSemanticReview(workdir, storyGitRef, semanticStory, semanticCfg, modelResolver ?? (() => null));
|
|
25060
25208
|
checks3.push(result2);
|
|
25061
25209
|
if (!result2.success && !firstFailure) {
|
|
@@ -31090,7 +31238,7 @@ var init_init_context = __esm(() => {
|
|
|
31090
31238
|
|
|
31091
31239
|
// src/utils/path-security.ts
|
|
31092
31240
|
import { realpathSync as realpathSync3 } from "fs";
|
|
31093
|
-
import { dirname as dirname4, isAbsolute as
|
|
31241
|
+
import { dirname as dirname4, isAbsolute as isAbsolute3, join as join31, normalize as normalize2, resolve as resolve5 } from "path";
|
|
31094
31242
|
function safeRealpathForComparison(p) {
|
|
31095
31243
|
try {
|
|
31096
31244
|
return realpathSync3(p);
|
|
@@ -31107,7 +31255,7 @@ function validateModulePath(modulePath, allowedRoots) {
|
|
|
31107
31255
|
return { valid: false, error: "Module path is empty" };
|
|
31108
31256
|
}
|
|
31109
31257
|
const resolvedRoots = allowedRoots.map((r) => safeRealpathForComparison(resolve5(r)));
|
|
31110
|
-
if (
|
|
31258
|
+
if (isAbsolute3(modulePath)) {
|
|
31111
31259
|
const normalized = normalize2(modulePath);
|
|
31112
31260
|
const resolved = safeRealpathForComparison(normalized);
|
|
31113
31261
|
const isWithin = resolvedRoots.some((root) => resolved.startsWith(`${root}/`) || resolved === root);
|
|
@@ -31735,7 +31883,8 @@ var init_checks_git = __esm(() => {
|
|
|
31735
31883
|
/^.{2} \.nax\/features\/[^/]+\/acceptance-refined\.json$/,
|
|
31736
31884
|
/^.{2} \.nax-verifier-verdict\.json$/,
|
|
31737
31885
|
/^.{2} \.nax-pids$/,
|
|
31738
|
-
/^.{2} \.nax-wt
|
|
31886
|
+
/^.{2} \.nax-wt\//,
|
|
31887
|
+
/^.{2} .*\.nax-acceptance[^/]*$/
|
|
31739
31888
|
];
|
|
31740
31889
|
});
|
|
31741
31890
|
|
|
@@ -31948,7 +32097,7 @@ var init_checks_blockers = __esm(() => {
|
|
|
31948
32097
|
|
|
31949
32098
|
// src/precheck/checks-warnings.ts
|
|
31950
32099
|
import { existsSync as existsSync30 } from "fs";
|
|
31951
|
-
import { isAbsolute as
|
|
32100
|
+
import { isAbsolute as isAbsolute5 } from "path";
|
|
31952
32101
|
async function checkClaudeMdExists(workdir) {
|
|
31953
32102
|
const claudeMdPath = `${workdir}/CLAUDE.md`;
|
|
31954
32103
|
const passed = existsSync30(claudeMdPath);
|
|
@@ -32048,7 +32197,8 @@ async function checkGitignoreCoversNax(workdir) {
|
|
|
32048
32197
|
".nax/metrics.json",
|
|
32049
32198
|
".nax/features/*/status.json",
|
|
32050
32199
|
".nax-pids",
|
|
32051
|
-
".nax-wt/"
|
|
32200
|
+
".nax-wt/",
|
|
32201
|
+
"**/.nax-acceptance*"
|
|
32052
32202
|
];
|
|
32053
32203
|
const missing = patterns.filter((pattern) => !content.includes(pattern));
|
|
32054
32204
|
const passed = missing.length === 0;
|
|
@@ -32080,7 +32230,7 @@ async function checkPromptOverrideFiles(config2, workdir) {
|
|
|
32080
32230
|
}
|
|
32081
32231
|
async function checkHomeEnvValid() {
|
|
32082
32232
|
const home = process.env.HOME ?? "";
|
|
32083
|
-
const passed = home !== "" &&
|
|
32233
|
+
const passed = home !== "" && isAbsolute5(home);
|
|
32084
32234
|
return {
|
|
32085
32235
|
name: "home-env-valid",
|
|
32086
32236
|
tier: "warning",
|
|
@@ -32183,6 +32333,24 @@ async function checkLanguageTools(profile, workdir) {
|
|
|
32183
32333
|
message: `Missing ${language} tools: ${missing.join(", ")}. ${toolConfig.installHint}`
|
|
32184
32334
|
};
|
|
32185
32335
|
}
|
|
32336
|
+
function checkBuildCommandInReviewChecks(config2) {
|
|
32337
|
+
const hasBuildCmd = !!(config2.review?.commands?.build || config2.quality?.commands?.build);
|
|
32338
|
+
const buildInChecks = config2.review?.checks?.includes("build") ?? false;
|
|
32339
|
+
if (hasBuildCmd && !buildInChecks) {
|
|
32340
|
+
return {
|
|
32341
|
+
name: "build-command-in-review-checks",
|
|
32342
|
+
tier: "warning",
|
|
32343
|
+
passed: false,
|
|
32344
|
+
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.'
|
|
32345
|
+
};
|
|
32346
|
+
}
|
|
32347
|
+
return {
|
|
32348
|
+
name: "build-command-in-review-checks",
|
|
32349
|
+
tier: "warning",
|
|
32350
|
+
passed: true,
|
|
32351
|
+
message: "build command check OK"
|
|
32352
|
+
};
|
|
32353
|
+
}
|
|
32186
32354
|
var _languageToolsDeps;
|
|
32187
32355
|
var init_checks_warnings = __esm(() => {
|
|
32188
32356
|
_languageToolsDeps = {
|
|
@@ -32374,7 +32542,8 @@ function getEnvironmentWarnings(config2, workdir) {
|
|
|
32374
32542
|
() => checkHomeEnvValid(),
|
|
32375
32543
|
() => checkPromptOverrideFiles(config2, workdir),
|
|
32376
32544
|
() => checkLanguageTools(config2.project, workdir),
|
|
32377
|
-
() => checkMultiAgentHealth()
|
|
32545
|
+
() => checkMultiAgentHealth(),
|
|
32546
|
+
() => Promise.resolve(checkBuildCommandInReviewChecks(config2))
|
|
32378
32547
|
];
|
|
32379
32548
|
}
|
|
32380
32549
|
function getProjectBlockers(prd) {
|
|
@@ -32719,36 +32888,32 @@ import { appendFileSync as appendFileSync2 } from "fs";
|
|
|
32719
32888
|
function startHeartbeat(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
|
|
32720
32889
|
const logger = getSafeLogger();
|
|
32721
32890
|
stopHeartbeat();
|
|
32722
|
-
heartbeatTimer = setInterval(
|
|
32723
|
-
|
|
32724
|
-
if (jsonlFilePath) {
|
|
32891
|
+
heartbeatTimer = setInterval(() => {
|
|
32892
|
+
(async () => {
|
|
32725
32893
|
try {
|
|
32726
|
-
|
|
32727
|
-
|
|
32728
|
-
|
|
32729
|
-
|
|
32730
|
-
|
|
32731
|
-
|
|
32732
|
-
|
|
32733
|
-
|
|
32734
|
-
|
|
32735
|
-
|
|
32736
|
-
|
|
32894
|
+
logger?.debug("crash-recovery", "Heartbeat");
|
|
32895
|
+
if (jsonlFilePath) {
|
|
32896
|
+
const heartbeatEntry = {
|
|
32897
|
+
timestamp: new Date().toISOString(),
|
|
32898
|
+
level: "debug",
|
|
32899
|
+
stage: "heartbeat",
|
|
32900
|
+
message: "Process alive",
|
|
32901
|
+
data: {
|
|
32902
|
+
pid: process.pid,
|
|
32903
|
+
memoryUsageMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024)
|
|
32904
|
+
}
|
|
32905
|
+
};
|
|
32906
|
+
const line = `${JSON.stringify(heartbeatEntry)}
|
|
32737
32907
|
`;
|
|
32738
|
-
|
|
32908
|
+
appendFileSync2(jsonlFilePath, line);
|
|
32909
|
+
}
|
|
32910
|
+
await statusWriter.update(getTotalCost(), getIterations(), {
|
|
32911
|
+
lastHeartbeat: new Date().toISOString()
|
|
32912
|
+
});
|
|
32739
32913
|
} catch (err) {
|
|
32740
|
-
logger?.warn("crash-recovery", "Failed
|
|
32914
|
+
logger?.warn("crash-recovery", "Failed during heartbeat", { error: err.message });
|
|
32741
32915
|
}
|
|
32742
|
-
}
|
|
32743
|
-
try {
|
|
32744
|
-
await statusWriter.update(getTotalCost(), getIterations(), {
|
|
32745
|
-
lastHeartbeat: new Date().toISOString()
|
|
32746
|
-
});
|
|
32747
|
-
} catch (err) {
|
|
32748
|
-
logger?.warn("crash-recovery", "Failed to update status during heartbeat", {
|
|
32749
|
-
error: err.message
|
|
32750
|
-
});
|
|
32751
|
-
}
|
|
32916
|
+
})().catch(() => {});
|
|
32752
32917
|
}, 60000);
|
|
32753
32918
|
logger?.debug("crash-recovery", "Heartbeat started (60s interval)");
|
|
32754
32919
|
}
|
|
@@ -32907,6 +33072,10 @@ function createSignalHandler(ctx) {
|
|
|
32907
33072
|
}
|
|
32908
33073
|
function createUncaughtExceptionHandler(ctx) {
|
|
32909
33074
|
return async (error48) => {
|
|
33075
|
+
process.stderr.write(`
|
|
33076
|
+
[nax crash] Uncaught exception: ${error48.message}
|
|
33077
|
+
${error48.stack ?? ""}
|
|
33078
|
+
`);
|
|
32910
33079
|
const logger = getSafeLogger();
|
|
32911
33080
|
logger?.error("crash-recovery", "Uncaught exception", {
|
|
32912
33081
|
error: error48.message,
|
|
@@ -32927,6 +33096,10 @@ function createUncaughtExceptionHandler(ctx) {
|
|
|
32927
33096
|
function createUnhandledRejectionHandler(ctx) {
|
|
32928
33097
|
return async (reason) => {
|
|
32929
33098
|
const error48 = reason instanceof Error ? reason : new Error(String(reason));
|
|
33099
|
+
process.stderr.write(`
|
|
33100
|
+
[nax crash] Unhandled rejection: ${error48.message}
|
|
33101
|
+
${error48.stack ?? ""}
|
|
33102
|
+
`);
|
|
32930
33103
|
const logger = getSafeLogger();
|
|
32931
33104
|
logger?.error("crash-recovery", "Unhandled promise rejection", {
|
|
32932
33105
|
error: error48.message,
|
|
@@ -34711,16 +34884,16 @@ var init_parallel_executor = __esm(() => {
|
|
|
34711
34884
|
|
|
34712
34885
|
// src/pipeline/subscribers/events-writer.ts
|
|
34713
34886
|
import { appendFile as appendFile2, mkdir as mkdir2 } from "fs/promises";
|
|
34714
|
-
import { homedir as
|
|
34887
|
+
import { homedir as homedir5 } from "os";
|
|
34715
34888
|
import { basename as basename5, join as join49 } from "path";
|
|
34716
34889
|
function wireEventsWriter(bus, feature, runId, workdir) {
|
|
34717
34890
|
const logger = getSafeLogger();
|
|
34718
34891
|
const project = basename5(workdir);
|
|
34719
|
-
const eventsDir = join49(
|
|
34892
|
+
const eventsDir = join49(homedir5(), ".nax", "events", project);
|
|
34720
34893
|
const eventsFile = join49(eventsDir, "events.jsonl");
|
|
34721
34894
|
let dirReady = false;
|
|
34722
34895
|
const write = (line) => {
|
|
34723
|
-
(async () => {
|
|
34896
|
+
return (async () => {
|
|
34724
34897
|
try {
|
|
34725
34898
|
if (!dirReady) {
|
|
34726
34899
|
await mkdir2(eventsDir, { recursive: true });
|
|
@@ -34738,16 +34911,30 @@ function wireEventsWriter(bus, feature, runId, workdir) {
|
|
|
34738
34911
|
};
|
|
34739
34912
|
const unsubs = [];
|
|
34740
34913
|
unsubs.push(bus.on("run:started", (_ev) => {
|
|
34741
|
-
write({ ts: new Date().toISOString(), event: "run:started", runId, feature, project });
|
|
34914
|
+
return write({ ts: new Date().toISOString(), event: "run:started", runId, feature, project });
|
|
34742
34915
|
}));
|
|
34743
34916
|
unsubs.push(bus.on("story:started", (ev) => {
|
|
34744
|
-
write({
|
|
34917
|
+
return write({
|
|
34918
|
+
ts: new Date().toISOString(),
|
|
34919
|
+
event: "story:started",
|
|
34920
|
+
runId,
|
|
34921
|
+
feature,
|
|
34922
|
+
project,
|
|
34923
|
+
storyId: ev.storyId
|
|
34924
|
+
});
|
|
34745
34925
|
}));
|
|
34746
34926
|
unsubs.push(bus.on("story:completed", (ev) => {
|
|
34747
|
-
write({
|
|
34927
|
+
return write({
|
|
34928
|
+
ts: new Date().toISOString(),
|
|
34929
|
+
event: "story:completed",
|
|
34930
|
+
runId,
|
|
34931
|
+
feature,
|
|
34932
|
+
project,
|
|
34933
|
+
storyId: ev.storyId
|
|
34934
|
+
});
|
|
34748
34935
|
}));
|
|
34749
34936
|
unsubs.push(bus.on("story:decomposed", (ev) => {
|
|
34750
|
-
write({
|
|
34937
|
+
return write({
|
|
34751
34938
|
ts: new Date().toISOString(),
|
|
34752
34939
|
event: "story:decomposed",
|
|
34753
34940
|
runId,
|
|
@@ -34758,13 +34945,20 @@ function wireEventsWriter(bus, feature, runId, workdir) {
|
|
|
34758
34945
|
});
|
|
34759
34946
|
}));
|
|
34760
34947
|
unsubs.push(bus.on("story:failed", (ev) => {
|
|
34761
|
-
write({
|
|
34948
|
+
return write({
|
|
34949
|
+
ts: new Date().toISOString(),
|
|
34950
|
+
event: "story:failed",
|
|
34951
|
+
runId,
|
|
34952
|
+
feature,
|
|
34953
|
+
project,
|
|
34954
|
+
storyId: ev.storyId
|
|
34955
|
+
});
|
|
34762
34956
|
}));
|
|
34763
34957
|
unsubs.push(bus.on("run:completed", (_ev) => {
|
|
34764
|
-
write({ ts: new Date().toISOString(), event: "on-complete", runId, feature, project });
|
|
34958
|
+
return write({ ts: new Date().toISOString(), event: "on-complete", runId, feature, project });
|
|
34765
34959
|
}));
|
|
34766
34960
|
unsubs.push(bus.on("run:paused", (ev) => {
|
|
34767
|
-
write({
|
|
34961
|
+
return write({
|
|
34768
34962
|
ts: new Date().toISOString(),
|
|
34769
34963
|
event: "run:paused",
|
|
34770
34964
|
runId,
|
|
@@ -34786,44 +34980,44 @@ var init_events_writer = __esm(() => {
|
|
|
34786
34980
|
function wireHooks(bus, hooks, workdir, feature) {
|
|
34787
34981
|
const logger = getSafeLogger();
|
|
34788
34982
|
const safe = (name, fn) => {
|
|
34789
|
-
fn().catch((err) => logger?.warn("hooks-subscriber", `Hook "${name}" failed`, { error: String(err) }));
|
|
34983
|
+
return fn().catch((err) => logger?.warn("hooks-subscriber", `Hook "${name}" failed`, { error: String(err) })).catch(() => {});
|
|
34790
34984
|
};
|
|
34791
34985
|
const unsubs = [];
|
|
34792
34986
|
unsubs.push(bus.on("run:started", (ev) => {
|
|
34793
|
-
safe("on-start", () => fireHook(hooks, "on-start", hookCtx(feature, { status: "running" }), workdir));
|
|
34987
|
+
return safe("on-start", () => fireHook(hooks, "on-start", hookCtx(feature, { status: "running" }), workdir));
|
|
34794
34988
|
}));
|
|
34795
34989
|
unsubs.push(bus.on("story:started", (ev) => {
|
|
34796
|
-
safe("on-story-start", () => fireHook(hooks, "on-story-start", hookCtx(feature, { storyId: ev.storyId, model: ev.modelTier, agent: ev.agent }), workdir));
|
|
34990
|
+
return safe("on-story-start", () => fireHook(hooks, "on-story-start", hookCtx(feature, { storyId: ev.storyId, model: ev.modelTier, agent: ev.agent }), workdir));
|
|
34797
34991
|
}));
|
|
34798
34992
|
unsubs.push(bus.on("story:completed", (ev) => {
|
|
34799
|
-
safe("on-story-complete", () => fireHook(hooks, "on-story-complete", hookCtx(feature, { storyId: ev.storyId, status: "passed", cost: ev.cost }), workdir));
|
|
34993
|
+
return safe("on-story-complete", () => fireHook(hooks, "on-story-complete", hookCtx(feature, { storyId: ev.storyId, status: "passed", cost: ev.cost }), workdir));
|
|
34800
34994
|
}));
|
|
34801
34995
|
unsubs.push(bus.on("story:decomposed", (ev) => {
|
|
34802
|
-
safe("on-story-complete (decomposed)", () => fireHook(hooks, "on-story-complete", hookCtx(feature, { storyId: ev.storyId, status: "decomposed", subStoryCount: ev.subStoryCount }), workdir));
|
|
34996
|
+
return safe("on-story-complete (decomposed)", () => fireHook(hooks, "on-story-complete", hookCtx(feature, { storyId: ev.storyId, status: "decomposed", subStoryCount: ev.subStoryCount }), workdir));
|
|
34803
34997
|
}));
|
|
34804
34998
|
unsubs.push(bus.on("story:failed", (ev) => {
|
|
34805
|
-
safe("on-story-fail", () => fireHook(hooks, "on-story-fail", hookCtx(feature, { storyId: ev.storyId, status: "failed", reason: ev.reason }), workdir));
|
|
34999
|
+
return safe("on-story-fail", () => fireHook(hooks, "on-story-fail", hookCtx(feature, { storyId: ev.storyId, status: "failed", reason: ev.reason }), workdir));
|
|
34806
35000
|
}));
|
|
34807
35001
|
unsubs.push(bus.on("story:paused", (ev) => {
|
|
34808
|
-
safe("on-pause (story)", () => fireHook(hooks, "on-pause", hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }), workdir));
|
|
35002
|
+
return safe("on-pause (story)", () => fireHook(hooks, "on-pause", hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }), workdir));
|
|
34809
35003
|
}));
|
|
34810
35004
|
unsubs.push(bus.on("run:paused", (ev) => {
|
|
34811
|
-
safe("on-pause (run)", () => fireHook(hooks, "on-pause", hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }), workdir));
|
|
35005
|
+
return safe("on-pause (run)", () => fireHook(hooks, "on-pause", hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }), workdir));
|
|
34812
35006
|
}));
|
|
34813
35007
|
unsubs.push(bus.on("run:completed", (ev) => {
|
|
34814
|
-
safe("on-complete", () => fireHook(hooks, "on-complete", hookCtx(feature, { status: "complete", cost: ev.totalCost ?? 0 }), workdir));
|
|
35008
|
+
return safe("on-complete", () => fireHook(hooks, "on-complete", hookCtx(feature, { status: "complete", cost: ev.totalCost ?? 0 }), workdir));
|
|
34815
35009
|
}));
|
|
34816
35010
|
unsubs.push(bus.on("run:resumed", (ev) => {
|
|
34817
|
-
safe("on-resume", () => fireHook(hooks, "on-resume", hookCtx(feature, { status: "running" }), workdir));
|
|
35011
|
+
return safe("on-resume", () => fireHook(hooks, "on-resume", hookCtx(feature, { status: "running" }), workdir));
|
|
34818
35012
|
}));
|
|
34819
35013
|
unsubs.push(bus.on("story:completed", (ev) => {
|
|
34820
|
-
safe("on-session-end (completed)", () => fireHook(hooks, "on-session-end", hookCtx(feature, { storyId: ev.storyId, status: "passed" }), workdir));
|
|
35014
|
+
return safe("on-session-end (completed)", () => fireHook(hooks, "on-session-end", hookCtx(feature, { storyId: ev.storyId, status: "passed" }), workdir));
|
|
34821
35015
|
}));
|
|
34822
35016
|
unsubs.push(bus.on("story:failed", (ev) => {
|
|
34823
|
-
safe("on-session-end (failed)", () => fireHook(hooks, "on-session-end", hookCtx(feature, { storyId: ev.storyId, status: "failed" }), workdir));
|
|
35017
|
+
return safe("on-session-end (failed)", () => fireHook(hooks, "on-session-end", hookCtx(feature, { storyId: ev.storyId, status: "failed" }), workdir));
|
|
34824
35018
|
}));
|
|
34825
35019
|
unsubs.push(bus.on("run:errored", (ev) => {
|
|
34826
|
-
safe("on-error", () => fireHook(hooks, "on-error", hookCtx(feature, { reason: ev.reason }), workdir));
|
|
35020
|
+
return safe("on-error", () => fireHook(hooks, "on-error", hookCtx(feature, { reason: ev.reason }), workdir));
|
|
34827
35021
|
}));
|
|
34828
35022
|
return () => {
|
|
34829
35023
|
for (const u of unsubs)
|
|
@@ -34890,15 +35084,15 @@ var init_interaction2 = __esm(() => {
|
|
|
34890
35084
|
|
|
34891
35085
|
// src/pipeline/subscribers/registry.ts
|
|
34892
35086
|
import { mkdir as mkdir3, writeFile } from "fs/promises";
|
|
34893
|
-
import { homedir as
|
|
35087
|
+
import { homedir as homedir6 } from "os";
|
|
34894
35088
|
import { basename as basename6, join as join50 } from "path";
|
|
34895
35089
|
function wireRegistry(bus, feature, runId, workdir) {
|
|
34896
35090
|
const logger = getSafeLogger();
|
|
34897
35091
|
const project = basename6(workdir);
|
|
34898
|
-
const runDir = join50(
|
|
35092
|
+
const runDir = join50(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
|
|
34899
35093
|
const metaFile = join50(runDir, "meta.json");
|
|
34900
35094
|
const unsub = bus.on("run:started", (_ev) => {
|
|
34901
|
-
(async () => {
|
|
35095
|
+
return (async () => {
|
|
34902
35096
|
try {
|
|
34903
35097
|
await mkdir3(runDir, { recursive: true });
|
|
34904
35098
|
const meta3 = {
|
|
@@ -34929,11 +35123,11 @@ var init_registry3 = __esm(() => {
|
|
|
34929
35123
|
function wireReporters(bus, pluginRegistry, runId, startTime) {
|
|
34930
35124
|
const logger = getSafeLogger();
|
|
34931
35125
|
const safe = (name, fn) => {
|
|
34932
|
-
fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) }));
|
|
35126
|
+
return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
|
|
34933
35127
|
};
|
|
34934
35128
|
const unsubs = [];
|
|
34935
35129
|
unsubs.push(bus.on("run:started", (ev) => {
|
|
34936
|
-
safe("onRunStart", async () => {
|
|
35130
|
+
return safe("onRunStart", async () => {
|
|
34937
35131
|
const reporters = pluginRegistry.getReporters();
|
|
34938
35132
|
for (const r of reporters) {
|
|
34939
35133
|
if (r.onRunStart) {
|
|
@@ -34952,7 +35146,7 @@ function wireReporters(bus, pluginRegistry, runId, startTime) {
|
|
|
34952
35146
|
});
|
|
34953
35147
|
}));
|
|
34954
35148
|
unsubs.push(bus.on("story:completed", (ev) => {
|
|
34955
|
-
safe("onStoryComplete(completed)", async () => {
|
|
35149
|
+
return safe("onStoryComplete(completed)", async () => {
|
|
34956
35150
|
const reporters = pluginRegistry.getReporters();
|
|
34957
35151
|
for (const r of reporters) {
|
|
34958
35152
|
if (r.onStoryComplete) {
|
|
@@ -34974,7 +35168,7 @@ function wireReporters(bus, pluginRegistry, runId, startTime) {
|
|
|
34974
35168
|
});
|
|
34975
35169
|
}));
|
|
34976
35170
|
unsubs.push(bus.on("story:failed", (ev) => {
|
|
34977
|
-
safe("onStoryComplete(failed)", async () => {
|
|
35171
|
+
return safe("onStoryComplete(failed)", async () => {
|
|
34978
35172
|
const reporters = pluginRegistry.getReporters();
|
|
34979
35173
|
for (const r of reporters) {
|
|
34980
35174
|
if (r.onStoryComplete) {
|
|
@@ -34996,7 +35190,7 @@ function wireReporters(bus, pluginRegistry, runId, startTime) {
|
|
|
34996
35190
|
});
|
|
34997
35191
|
}));
|
|
34998
35192
|
unsubs.push(bus.on("story:paused", (ev) => {
|
|
34999
|
-
safe("onStoryComplete(paused)", async () => {
|
|
35193
|
+
return safe("onStoryComplete(paused)", async () => {
|
|
35000
35194
|
const reporters = pluginRegistry.getReporters();
|
|
35001
35195
|
for (const r of reporters) {
|
|
35002
35196
|
if (r.onStoryComplete) {
|
|
@@ -35018,7 +35212,7 @@ function wireReporters(bus, pluginRegistry, runId, startTime) {
|
|
|
35018
35212
|
});
|
|
35019
35213
|
}));
|
|
35020
35214
|
unsubs.push(bus.on("run:completed", (ev) => {
|
|
35021
|
-
safe("onRunEnd", async () => {
|
|
35215
|
+
return safe("onRunEnd", async () => {
|
|
35022
35216
|
const reporters = pluginRegistry.getReporters();
|
|
35023
35217
|
for (const r of reporters) {
|
|
35024
35218
|
if (r.onRunEnd) {
|
|
@@ -35759,7 +35953,8 @@ async function executeSequential(ctx, initialPrd) {
|
|
|
35759
35953
|
story: prd.userStories[0],
|
|
35760
35954
|
stories: prd.userStories,
|
|
35761
35955
|
routing: { complexity: "simple", modelTier: "fast", testStrategy: "test-after", reasoning: "" },
|
|
35762
|
-
hooks: ctx.hooks
|
|
35956
|
+
hooks: ctx.hooks,
|
|
35957
|
+
agentGetFn: ctx.agentGetFn
|
|
35763
35958
|
};
|
|
35764
35959
|
await runPipeline(preRunPipeline, preRunCtx, ctx.eventEmitter);
|
|
35765
35960
|
while (iterations < ctx.config.execution.maxIterations) {
|
|
@@ -67558,7 +67753,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
|
|
|
67558
67753
|
// bin/nax.ts
|
|
67559
67754
|
init_source();
|
|
67560
67755
|
import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
|
|
67561
|
-
import { homedir as
|
|
67756
|
+
import { homedir as homedir8 } from "os";
|
|
67562
67757
|
import { join as join56 } from "path";
|
|
67563
67758
|
|
|
67564
67759
|
// node_modules/commander/esm.mjs
|
|
@@ -68814,7 +69009,14 @@ async function planCommand(workdir, config2, options) {
|
|
|
68814
69009
|
if (entry)
|
|
68815
69010
|
autoModel = resolveModel2(entry).model;
|
|
68816
69011
|
} catch {}
|
|
68817
|
-
rawResponse = await cliAdapter.complete(prompt, {
|
|
69012
|
+
rawResponse = await cliAdapter.complete(prompt, {
|
|
69013
|
+
model: autoModel,
|
|
69014
|
+
jsonMode: true,
|
|
69015
|
+
workdir,
|
|
69016
|
+
config: config2,
|
|
69017
|
+
featureName: options.feature,
|
|
69018
|
+
sessionRole: "plan"
|
|
69019
|
+
});
|
|
68818
69020
|
try {
|
|
68819
69021
|
const envelope = JSON.parse(rawResponse);
|
|
68820
69022
|
if (envelope?.type === "result" && typeof envelope?.result === "string") {
|
|
@@ -71101,10 +71303,10 @@ import { readdir as readdir3 } from "fs/promises";
|
|
|
71101
71303
|
import { join as join39 } from "path";
|
|
71102
71304
|
|
|
71103
71305
|
// src/utils/paths.ts
|
|
71104
|
-
import { homedir as
|
|
71306
|
+
import { homedir as homedir4 } from "os";
|
|
71105
71307
|
import { join as join38 } from "path";
|
|
71106
71308
|
function getRunsDir() {
|
|
71107
|
-
return process.env.NAX_RUNS_DIR ?? join38(
|
|
71309
|
+
return process.env.NAX_RUNS_DIR ?? join38(homedir4(), ".nax", "runs");
|
|
71108
71310
|
}
|
|
71109
71311
|
|
|
71110
71312
|
// src/commands/logs-reader.ts
|
|
@@ -71239,7 +71441,7 @@ Runs:
|
|
|
71239
71441
|
const summary = await extractRunSummary(filePath);
|
|
71240
71442
|
const timestamp = file2.replace(".jsonl", "");
|
|
71241
71443
|
const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
|
|
71242
|
-
const duration3 = summary ?
|
|
71444
|
+
const duration3 = summary ? formatDuration(summary.durationMs) : "?";
|
|
71243
71445
|
const cost = summary ? `$${summary.totalCost.toFixed(4)}` : "$?.????";
|
|
71244
71446
|
const status = summary ? summary.failed === 0 ? source_default.green("\u2713") : source_default.red("\u2717") : "?";
|
|
71245
71447
|
console.log(` ${timestamp} ${stories.padEnd(7)} ${duration3.padEnd(8)} ${cost.padEnd(8)} ${status}`);
|
|
@@ -71333,17 +71535,6 @@ function shouldDisplayEntry(entry, options) {
|
|
|
71333
71535
|
}
|
|
71334
71536
|
return true;
|
|
71335
71537
|
}
|
|
71336
|
-
function formatDuration2(ms) {
|
|
71337
|
-
if (ms < 1000) {
|
|
71338
|
-
return `${ms}ms`;
|
|
71339
|
-
}
|
|
71340
|
-
if (ms < 60000) {
|
|
71341
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
71342
|
-
}
|
|
71343
|
-
const minutes = Math.floor(ms / 60000);
|
|
71344
|
-
const seconds = Math.floor(ms % 60000 / 1000);
|
|
71345
|
-
return `${minutes}m${seconds}s`;
|
|
71346
|
-
}
|
|
71347
71538
|
|
|
71348
71539
|
// src/commands/logs.ts
|
|
71349
71540
|
async function logsCommand(options) {
|
|
@@ -71445,7 +71636,7 @@ var DEFAULT_LIMIT = 20;
|
|
|
71445
71636
|
var _runsCmdDeps = {
|
|
71446
71637
|
getRunsDir
|
|
71447
71638
|
};
|
|
71448
|
-
function
|
|
71639
|
+
function formatDuration2(ms) {
|
|
71449
71640
|
if (ms <= 0)
|
|
71450
71641
|
return "-";
|
|
71451
71642
|
const minutes = Math.floor(ms / 60000);
|
|
@@ -71558,7 +71749,7 @@ async function runsCommand(options = {}) {
|
|
|
71558
71749
|
pad3(row.feature, COL.feature),
|
|
71559
71750
|
pad3(colored, COL.status + (colored.length - visibleLength(colored))),
|
|
71560
71751
|
pad3(`${row.passed}/${row.total}`, COL.stories),
|
|
71561
|
-
pad3(
|
|
71752
|
+
pad3(formatDuration2(row.durationMs), COL.duration),
|
|
71562
71753
|
formatDate(row.registeredAt)
|
|
71563
71754
|
].join(" ");
|
|
71564
71755
|
console.log(line);
|
|
@@ -71881,7 +72072,8 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
|
|
|
71881
72072
|
startTime: options.startTime,
|
|
71882
72073
|
batchPlan,
|
|
71883
72074
|
agentGetFn: options.agentGetFn,
|
|
71884
|
-
pidRegistry: options.pidRegistry
|
|
72075
|
+
pidRegistry: options.pidRegistry,
|
|
72076
|
+
interactionChain: options.interactionChain
|
|
71885
72077
|
}, prd);
|
|
71886
72078
|
prd = sequentialResult.prd;
|
|
71887
72079
|
iterations = sequentialResult.iterations;
|
|
@@ -72007,7 +72199,8 @@ async function run(options) {
|
|
|
72007
72199
|
parallel,
|
|
72008
72200
|
runParallelExecution: _runnerDeps.runParallelExecution ?? undefined,
|
|
72009
72201
|
agentGetFn,
|
|
72010
|
-
pidRegistry
|
|
72202
|
+
pidRegistry,
|
|
72203
|
+
interactionChain
|
|
72011
72204
|
}, prd, pluginRegistry);
|
|
72012
72205
|
prd = executionResult.prd;
|
|
72013
72206
|
iterations = executionResult.iterations;
|
|
@@ -79668,7 +79861,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
79668
79861
|
config2.autoMode.defaultAgent = options.agent;
|
|
79669
79862
|
}
|
|
79670
79863
|
config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
|
|
79671
|
-
const globalNaxDir = join56(
|
|
79864
|
+
const globalNaxDir = join56(homedir8(), ".nax");
|
|
79672
79865
|
const hooks = await loadHooksConfig(naxDir, globalNaxDir);
|
|
79673
79866
|
const eventEmitter = new PipelineEventEmitter;
|
|
79674
79867
|
let tuiInstance;
|