@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.
Files changed (2) hide show
  1. package/dist/nax.js +479 -286
  2. 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 "test_acceptance.py";
18873
+ return ".nax-acceptance.test.py";
18865
18874
  case "rust":
18866
- return "tests/acceptance.rs";
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
- - Output raw code only \u2014 no markdown fences, start directly with the language's import or package declaration
18939
- - **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')\`).`;
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, "acceptance.test.ts");
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 = buildAllowedEnv2();
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: _options?.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 isAbsolute3, normalize, resolve } from "path";
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 (!isAbsolute3(normalizedTarget) || !isAbsolute3(normalizedBase)) {
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 homedir3 } from "os";
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(homedir3(), ".nax");
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.1",
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("5282117"))
22401
- return "5282117";
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 testPath = path4.join(ctx.featureDir, effectiveConfig.acceptance.testPath);
24089
- const testFile = Bun.file(testPath);
24090
- const exists = await testFile.exists();
24091
- if (!exists) {
24092
- logger.warn("acceptance", "Acceptance test file not found \u2014 skipping", {
24093
- testPath
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
- return { action: "continue" };
24096
- }
24097
- const testCmdParts = buildAcceptanceRunCommand(testPath, effectiveConfig.project?.testFramework, effectiveConfig.acceptance.command);
24098
- logger.info("acceptance", "Running acceptance command", { cmd: testCmdParts.join(" ") });
24099
- const proc = Bun.spawn(testCmdParts, {
24100
- cwd: ctx.workdir,
24101
- stdout: "pipe",
24102
- stderr: "pipe"
24103
- });
24104
- const [exitCode, stdout, stderr] = await Promise.all([
24105
- proc.exited,
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
- const failedACs = parseTestFailures(output);
24112
- const overrides = ctx.prd.acceptanceOverrides || {};
24113
- const actualFailures = failedACs.filter((acId) => !overrides[acId]);
24114
- if (actualFailures.length === 0 && exitCode === 0) {
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
- logger.error("acceptance", "Acceptance tests failed", { failedACs: actualFailures });
24143
- logTestOutput(logger, "acceptance", output);
24144
- ctx.acceptanceFailures = {
24145
- failedACs: actualFailures,
24146
- testOutput: output
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 failed: ${actualFailures.join(", ")}`
24203
+ reason: `Acceptance tests errored (exit code ${errorExitCode}): syntax error, import failure, or unhandled exception`
24151
24204
  };
24152
24205
  }
24153
- logger.info("acceptance", "All acceptance tests passed");
24154
- return { action: "continue" };
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 fileExists = await _acceptanceSetupDeps.fileExists(testPath);
24342
- let shouldGenerate = !fileExists;
24343
- if (fileExists) {
24344
- const fingerprint = computeACFingerprint(allCriteria);
24345
- const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
24346
- if (!meta3 || meta3.acFingerprint !== fingerprint) {
24347
- await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
24348
- await _acceptanceSetupDeps.deleteFile(testPath);
24349
- shouldGenerate = true;
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 refinedCriteria;
24449
+ let allRefinedCriteria;
24357
24450
  if (ctx.config.acceptance.refinement) {
24358
- refinedCriteria = await _acceptanceSetupDeps.refine(allCriteria, {
24359
- storyId: ctx.prd.userStories[0]?.id ?? "US-001",
24360
- codebaseContext: "",
24361
- config: ctx.config,
24362
- testStrategy: ctx.config.acceptance.testStrategy,
24363
- testFramework: ctx.config.acceptance.testFramework
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
- refinedCriteria = allCriteria.map((c) => ({
24465
+ allRefinedCriteria = nonFixStories.flatMap((story) => story.acceptanceCriteria.map((c) => ({
24367
24466
  original: c,
24368
24467
  refined: c,
24369
24468
  testable: true,
24370
- storyId: ctx.prd.userStories[0]?.id ?? "US-001"
24371
- }));
24469
+ storyId: story.id
24470
+ })));
24372
24471
  }
24373
- testableCount = refinedCriteria.filter((r) => r.testable).length;
24374
- const result = await _acceptanceSetupDeps.generate(ctx.prd.userStories, refinedCriteria, {
24375
- featureName: ctx.prd.feature,
24376
- workdir: ctx.workdir,
24377
- featureDir: ctx.featureDir,
24378
- codebaseContext: "",
24379
- modelTier: ctx.config.acceptance.model ?? "fast",
24380
- modelDef: resolveModel(ctx.config.models[ctx.config.acceptance.model ?? "fast"]),
24381
- config: ctx.config,
24382
- testStrategy: ctx.config.acceptance.testStrategy,
24383
- testFramework: ctx.config.acceptance.testFramework,
24384
- adapter: agent ?? undefined
24385
- });
24386
- await _acceptanceSetupDeps.writeFile(testPath, result.testCode);
24387
- const fingerprint = computeACFingerprint(allCriteria);
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: fingerprint,
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
- const runCmd = buildAcceptanceRunCommand(testPath, effectiveConfig.project?.testFramework, effectiveConfig.acceptance.command);
24402
- getSafeLogger()?.info("acceptance-setup", "Running acceptance RED gate command", { cmd: runCmd.join(" ") });
24403
- const { exitCode } = await _acceptanceSetupDeps.runTest(testPath, ctx.workdir, runCmd);
24404
- if (exitCode === 0) {
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: 1 };
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
- const parsed = JSON.parse(raw);
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: Date.now() - startTime,
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: Date.now() - startTime
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 ?? { modelTier: "balanced", rules: [] };
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 isAbsolute4, join as join31, normalize as normalize2, resolve as resolve5 } from "path";
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 (isAbsolute4(modulePath)) {
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 isAbsolute6 } from "path";
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 !== "" && isAbsolute6(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(async () => {
32723
- logger?.debug("crash-recovery", "Heartbeat");
32724
- if (jsonlFilePath) {
32891
+ heartbeatTimer = setInterval(() => {
32892
+ (async () => {
32725
32893
  try {
32726
- const heartbeatEntry = {
32727
- timestamp: new Date().toISOString(),
32728
- level: "debug",
32729
- stage: "heartbeat",
32730
- message: "Process alive",
32731
- data: {
32732
- pid: process.pid,
32733
- memoryUsageMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024)
32734
- }
32735
- };
32736
- const line = `${JSON.stringify(heartbeatEntry)}
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
- appendFileSync2(jsonlFilePath, line);
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 to write heartbeat", { error: err.message });
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 homedir6 } from "os";
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(homedir6(), ".nax", "events", project);
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({ ts: new Date().toISOString(), event: "story:started", runId, feature, project, storyId: ev.storyId });
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({ ts: new Date().toISOString(), event: "story:completed", runId, feature, project, storyId: ev.storyId });
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({ ts: new Date().toISOString(), event: "story:failed", runId, feature, project, storyId: ev.storyId });
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 homedir7 } from "os";
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(homedir7(), ".nax", "runs", `${project}-${feature}-${runId}`);
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 homedir9 } from "os";
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, { model: autoModel, jsonMode: true, workdir, config: config2 });
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 homedir5 } from "os";
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(homedir5(), ".nax", "runs");
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 ? formatDuration2(summary.durationMs) : "?";
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 formatDuration3(ms) {
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(formatDuration3(row.durationMs), COL.duration),
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(homedir9(), ".nax");
79864
+ const globalNaxDir = join56(homedir8(), ".nax");
79672
79865
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
79673
79866
  const eventEmitter = new PipelineEventEmitter;
79674
79867
  let tuiInstance;