@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.
Files changed (3) hide show
  1. package/README.md +4 -2
  2. package/dist/nax.js +335 -212
  3. 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 reporting
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 "test_acceptance.py";
18869
+ return ".nax-acceptance.test.py";
18864
18870
  case "rust":
18865
- return "tests/acceptance.rs";
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 will be saved at \`<repo-root>/.nax/features/${options.featureName}/${acceptanceTestFilename(options.language)}\` and will ALWAYS run from the repo root. The repo root is exactly 4 \`../\` levels above \`__dirname\`: \`join(__dirname, '..', '..', '..', '..')\`. For monorepo projects, navigate into packages from root (e.g. \`join(root, 'apps/api/src')\`).`;
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, "acceptance.test.ts");
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 = buildAllowedEnv2();
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 isAbsolute3, normalize, resolve } from "path";
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 (!isAbsolute3(normalizedTarget) || !isAbsolute3(normalizedBase)) {
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 homedir3 } from "os";
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(homedir3(), ".nax");
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.0",
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("f0107a4"))
22380
- return "f0107a4";
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 testPath = path4.join(ctx.featureDir, effectiveConfig.acceptance.testPath);
24067
- const testFile = Bun.file(testPath);
24068
- const exists = await testFile.exists();
24069
- if (!exists) {
24070
- logger.warn("acceptance", "Acceptance test file not found \u2014 skipping", {
24071
- testPath
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
- return { action: "continue" };
24074
- }
24075
- const acceptanceCmd = effectiveConfig.acceptance.command;
24076
- let testCmdParts;
24077
- if (acceptanceCmd) {
24078
- const resolved = acceptanceCmd.includes("{{FILE}}") ? acceptanceCmd.replace("{{FILE}}", testPath) : acceptanceCmd;
24079
- testCmdParts = resolved.trim().split(/\s+/);
24080
- } else {
24081
- testCmdParts = ["bun", "test", testPath, "--timeout=60000"];
24082
- }
24083
- const proc = Bun.spawn(testCmdParts, {
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
- const failedACs = parseTestFailures(output);
24096
- const overrides = ctx.prd.acceptanceOverrides || {};
24097
- const actualFailures = failedACs.filter((acId) => !overrides[acId]);
24098
- if (actualFailures.length === 0 && exitCode === 0) {
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
- logger.error("acceptance", "Acceptance tests failed", { failedACs: actualFailures });
24127
- logTestOutput(logger, "acceptance", output);
24128
- ctx.acceptanceFailures = {
24129
- failedACs: actualFailures,
24130
- testOutput: output
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 failed: ${actualFailures.join(", ")}`
24157
+ reason: `Acceptance tests errored (exit code ${errorExitCode}): syntax error, import failure, or unhandled exception`
24135
24158
  };
24136
24159
  }
24137
- logger.info("acceptance", "All acceptance tests passed");
24138
- return { action: "continue" };
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, _testCmd) => {
24286
- let cmd;
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 fileExists = await _acceptanceSetupDeps.fileExists(testPath);
24331
- let shouldGenerate = !fileExists;
24332
- if (fileExists) {
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
- await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
24337
- await _acceptanceSetupDeps.deleteFile(testPath);
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 refinedCriteria;
24401
+ let allRefinedCriteria;
24346
24402
  if (ctx.config.acceptance.refinement) {
24347
- refinedCriteria = await _acceptanceSetupDeps.refine(allCriteria, {
24348
- storyId: ctx.prd.userStories[0]?.id ?? "US-001",
24349
- codebaseContext: "",
24350
- config: ctx.config,
24351
- testStrategy: ctx.config.acceptance.testStrategy,
24352
- testFramework: ctx.config.acceptance.testFramework
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
- refinedCriteria = allCriteria.map((c) => ({
24415
+ allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
24356
24416
  original: c,
24357
24417
  refined: c,
24358
24418
  testable: true,
24359
- storyId: ctx.prd.userStories[0]?.id ?? "US-001"
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 testCmd = ctx.config.quality?.commands?.test;
24390
- const { exitCode } = await _acceptanceSetupDeps.runTest(testPath, ctx.workdir, testCmd);
24391
- if (exitCode === 0) {
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: 1 };
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
- const parsed = JSON.parse(raw);
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: Date.now() - startTime,
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: Date.now() - startTime
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 ?? { modelTier: "balanced", rules: [] };
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 isAbsolute4, join as join31, normalize as normalize2, resolve as resolve5 } from "path";
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 (isAbsolute4(modulePath)) {
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 isAbsolute6 } from "path";
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 !== "" && isAbsolute6(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 homedir6 } from "os";
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(homedir6(), ".nax", "events", project);
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 homedir7 } from "os";
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(homedir7(), ".nax", "runs", `${project}-${feature}-${runId}`);
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 homedir9 } from "os";
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 homedir5 } from "os";
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(homedir5(), ".nax", "runs");
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(homedir9(), ".nax");
79781
+ const globalNaxDir = join56(homedir8(), ".nax");
79659
79782
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
79660
79783
  const eventEmitter = new PipelineEventEmitter;
79661
79784
  let tuiInstance;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.54.0",
3
+ "version": "0.54.2",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {