@nathapp/nax 0.54.5 → 0.54.7

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 +160 -50
  2. package/package.json +2 -2
package/dist/nax.js CHANGED
@@ -19826,7 +19826,7 @@ async function saveAcpSession(workdir, featureName, storyId, sessionName, agentN
19826
19826
  getSafeLogger()?.warn("acp-adapter", "Failed to save session to sidecar", { error: String(err) });
19827
19827
  }
19828
19828
  }
19829
- async function clearAcpSession(workdir, featureName, storyId) {
19829
+ async function clearAcpSession(workdir, featureName, storyId, sessionRole) {
19830
19830
  try {
19831
19831
  const path = acpSessionsPath(workdir, featureName);
19832
19832
  let data = {};
@@ -19836,7 +19836,8 @@ async function clearAcpSession(workdir, featureName, storyId) {
19836
19836
  } catch {
19837
19837
  return;
19838
19838
  }
19839
- delete data[storyId];
19839
+ const sidecarKey = sessionRole ? `${storyId}:${sessionRole}` : storyId;
19840
+ delete data[sidecarKey];
19840
19841
  await Bun.write(path, JSON.stringify(data, null, 2));
19841
19842
  } catch (err) {
19842
19843
  getSafeLogger()?.warn("acp-adapter", "Failed to clear session from sidecar", { error: String(err) });
@@ -20024,7 +20025,8 @@ class AcpAgentAdapter {
20024
20025
  await client.start();
20025
20026
  let sessionName = options.acpSessionName;
20026
20027
  if (!sessionName && options.featureName && options.storyId) {
20027
- sessionName = await readAcpSession(options.workdir, options.featureName, options.storyId) ?? undefined;
20028
+ const sidecarKey = options.sessionRole ? `${options.storyId}:${options.sessionRole}` : options.storyId;
20029
+ sessionName = await readAcpSession(options.workdir, options.featureName, sidecarKey) ?? undefined;
20028
20030
  }
20029
20031
  sessionName ??= buildSessionName(options.workdir, options.featureName, options.storyId, options.sessionRole);
20030
20032
  const resolvedPerm = resolvePermissions(options.config, options.pipelineStage ?? "run");
@@ -20035,7 +20037,8 @@ class AcpAgentAdapter {
20035
20037
  });
20036
20038
  const session = await ensureAcpSession(client, sessionName, this.name, permissionMode);
20037
20039
  if (options.featureName && options.storyId) {
20038
- await saveAcpSession(options.workdir, options.featureName, options.storyId, sessionName, this.name);
20040
+ const sidecarKey = options.sessionRole ? `${options.storyId}:${options.sessionRole}` : options.storyId;
20041
+ await saveAcpSession(options.workdir, options.featureName, sidecarKey, sessionName, this.name);
20039
20042
  }
20040
20043
  let lastResponse = null;
20041
20044
  let timedOut = false;
@@ -20072,7 +20075,8 @@ class AcpAgentAdapter {
20072
20075
  totalExactCostUsd = (totalExactCostUsd ?? 0) + lastResponse.exactCostUsd;
20073
20076
  }
20074
20077
  const outputText = extractOutput(lastResponse);
20075
- const question = extractQuestion(outputText);
20078
+ const isEndTurn = lastResponse.stopReason === "end_turn";
20079
+ const question = isEndTurn ? extractQuestion(outputText) : null;
20076
20080
  if (!question || !options.interactionBridge)
20077
20081
  break;
20078
20082
  getSafeLogger()?.debug("acp-adapter", "Agent asked question, routing to interactionBridge", { question });
@@ -22348,7 +22352,7 @@ var package_default;
22348
22352
  var init_package = __esm(() => {
22349
22353
  package_default = {
22350
22354
  name: "@nathapp/nax",
22351
- version: "0.54.5",
22355
+ version: "0.54.7",
22352
22356
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22353
22357
  type: "module",
22354
22358
  bin: {
@@ -22361,7 +22365,7 @@ var init_package = __esm(() => {
22361
22365
  typecheck: "bun x tsc --noEmit",
22362
22366
  lint: "bun x biome check src/ bin/",
22363
22367
  release: "bun scripts/release.ts",
22364
- test: "bun test test/ --timeout=60000",
22368
+ test: "bun test test/unit/ --timeout=60000 && bun test test/integration/ --timeout=60000 && bun test test/ui/ --timeout=60000",
22365
22369
  "test:watch": "bun test --watch",
22366
22370
  "test:unit": "bun test ./test/unit/ --timeout=60000",
22367
22371
  "test:integration": "bun test ./test/integration/ --timeout=60000",
@@ -22425,8 +22429,8 @@ var init_version = __esm(() => {
22425
22429
  NAX_VERSION = package_default.version;
22426
22430
  NAX_COMMIT = (() => {
22427
22431
  try {
22428
- if (/^[0-9a-f]{6,10}$/.test("e98d5b1"))
22429
- return "e98d5b1";
22432
+ if (/^[0-9a-f]{6,10}$/.test("ccfd70c"))
22433
+ return "ccfd70c";
22430
22434
  } catch {}
22431
22435
  try {
22432
22436
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -24866,14 +24870,34 @@ async function collectDiff(workdir, storyGitRef) {
24866
24870
  }
24867
24871
  return stdout;
24868
24872
  }
24869
- function truncateDiff(diff) {
24873
+ async function collectDiffStat(workdir, storyGitRef) {
24874
+ const proc = _semanticDeps.spawn({
24875
+ cmd: ["git", "diff", "--stat", `${storyGitRef}..HEAD`],
24876
+ cwd: workdir,
24877
+ stdout: "pipe",
24878
+ stderr: "pipe"
24879
+ });
24880
+ const [exitCode, stdout] = await Promise.all([
24881
+ proc.exited,
24882
+ new Response(proc.stdout).text(),
24883
+ new Response(proc.stderr).text()
24884
+ ]);
24885
+ return exitCode === 0 ? stdout.trim() : "";
24886
+ }
24887
+ function truncateDiff(diff, stat) {
24870
24888
  if (diff.length <= DIFF_CAP_BYTES) {
24871
24889
  return diff;
24872
24890
  }
24873
24891
  const truncated = diff.slice(0, DIFF_CAP_BYTES);
24874
- const fileCount = (truncated.match(/^diff --git/gm) ?? []).length;
24875
- return `${truncated}
24876
- ... (truncated, showing first ${fileCount} files)`;
24892
+ const visibleFiles = (truncated.match(/^diff --git/gm) ?? []).length;
24893
+ const totalFiles = (diff.match(/^diff --git/gm) ?? []).length;
24894
+ const statPreamble = stat ? `## File Summary (all changed files)
24895
+ ${stat}
24896
+
24897
+ ## Diff (truncated \u2014 ${visibleFiles}/${totalFiles} files shown)
24898
+ ` : "";
24899
+ return `${statPreamble}${truncated}
24900
+ ... (truncated at ${DIFF_CAP_BYTES} bytes, showing ${visibleFiles}/${totalFiles} files)`;
24877
24901
  }
24878
24902
  function buildPrompt(story, semanticConfig, diff) {
24879
24903
  const acList = story.acceptanceCriteria.map((ac, i) => `${i + 1}. ${ac}`).join(`
@@ -24980,7 +25004,9 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
24980
25004
  }
24981
25005
  logger?.info("review", "Running semantic check", { storyId: story.id, modelTier: semanticConfig.modelTier });
24982
25006
  const rawDiff = await collectDiff(workdir, storyGitRef);
24983
- const diff = truncateDiff(rawDiff);
25007
+ const needsTruncation = rawDiff.length > DIFF_CAP_BYTES;
25008
+ const stat = needsTruncation ? await collectDiffStat(workdir, storyGitRef) : undefined;
25009
+ const diff = truncateDiff(rawDiff, stat);
24984
25010
  const agent = modelResolver(semanticConfig.modelTier);
24985
25011
  if (!agent) {
24986
25012
  logger?.warn("semantic", "No agent available for semantic review \u2014 skipping", {
@@ -25016,6 +25042,20 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
25016
25042
  }
25017
25043
  const parsed = parseLLMResponse(rawResponse);
25018
25044
  if (!parsed) {
25045
+ const looksLikeFail = /"passed"\s*:\s*false/.test(rawResponse);
25046
+ if (looksLikeFail) {
25047
+ logger?.warn("semantic", "LLM returned truncated JSON with passed:false \u2014 treating as failure", {
25048
+ rawResponse: rawResponse.slice(0, 200)
25049
+ });
25050
+ return {
25051
+ check: "semantic",
25052
+ success: false,
25053
+ command: "",
25054
+ exitCode: 1,
25055
+ output: "semantic review: LLM response truncated but indicated failure (passed:false found in partial response)",
25056
+ durationMs: Date.now() - startTime
25057
+ };
25058
+ }
25019
25059
  logger?.warn("semantic", "LLM returned invalid JSON \u2014 fail-open", { rawResponse: rawResponse.slice(0, 200) });
25020
25060
  return {
25021
25061
  check: "semantic",
@@ -25068,7 +25108,7 @@ ${formatFindings(parsed.findings)}`;
25068
25108
  durationMs
25069
25109
  };
25070
25110
  }
25071
- var _semanticDeps, DIFF_CAP_BYTES = 12288, DEFAULT_RULES;
25111
+ var _semanticDeps, DIFF_CAP_BYTES = 102400, DEFAULT_RULES;
25072
25112
  var init_semantic = __esm(() => {
25073
25113
  init_logger2();
25074
25114
  _semanticDeps = {
@@ -27464,6 +27504,9 @@ function shouldRetryRectification(state, config2) {
27464
27504
  if (state.attempt >= config2.maxRetries) {
27465
27505
  return false;
27466
27506
  }
27507
+ if (state.lastExitCode !== undefined && state.lastExitCode !== 0 && state.currentFailures === 0) {
27508
+ return true;
27509
+ }
27467
27510
  if (state.currentFailures === 0) {
27468
27511
  return false;
27469
27512
  }
@@ -29754,7 +29797,8 @@ async function runRectificationLoop2(opts) {
29754
29797
  const rectificationState = {
29755
29798
  attempt: 0,
29756
29799
  initialFailures: testSummary.failed,
29757
- currentFailures: testSummary.failed
29800
+ currentFailures: testSummary.failed,
29801
+ lastExitCode: 1
29758
29802
  };
29759
29803
  logger?.info("rectification", `Starting ${label} loop`, {
29760
29804
  storyId: story.id,
@@ -29832,6 +29876,7 @@ ${rectificationPrompt}`;
29832
29876
  if (retryVerification.output) {
29833
29877
  const newTestSummary = parseBunTestOutput(retryVerification.output);
29834
29878
  rectificationState.currentFailures = newTestSummary.failed;
29879
+ rectificationState.lastExitCode = retryVerification.status === "SUCCESS" ? 0 : 1;
29835
29880
  testSummary.failures = newTestSummary.failures;
29836
29881
  testSummary.failed = newTestSummary.failed;
29837
29882
  testSummary.passed = newTestSummary.passed;
@@ -30040,7 +30085,8 @@ function makeFailResult(storyId, strategy, status, opts = {}) {
30040
30085
  failures: opts.failures ?? [],
30041
30086
  rawOutput: opts.rawOutput,
30042
30087
  durationMs: opts.durationMs ?? 0,
30043
- countsTowardEscalation: opts.countsTowardEscalation ?? true
30088
+ countsTowardEscalation: opts.countsTowardEscalation ?? true,
30089
+ exitCode: opts.exitCode
30044
30090
  };
30045
30091
  }
30046
30092
  function makePassResult(storyId, strategy, opts = {}) {
@@ -30449,7 +30495,8 @@ class ScopedStrategy {
30449
30495
  passCount: parsed.passed,
30450
30496
  failCount: parsed.failed,
30451
30497
  failures: parsed.failures,
30452
- durationMs
30498
+ durationMs,
30499
+ exitCode: result.status === "TEST_FAILURE" ? 1 : undefined
30453
30500
  });
30454
30501
  }
30455
30502
  }
@@ -34298,7 +34345,7 @@ async function executeStoryInWorktree(story, worktreePath, context, routing, eve
34298
34345
  };
34299
34346
  }
34300
34347
  }
34301
- async function executeParallelBatch(stories, projectRoot, config2, context, worktreePaths, maxConcurrency, eventEmitter) {
34348
+ async function executeParallelBatch(stories, projectRoot, config2, context, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs) {
34302
34349
  const logger = getSafeLogger();
34303
34350
  const results = {
34304
34351
  pipelinePassed: [],
@@ -34319,7 +34366,9 @@ async function executeParallelBatch(stories, projectRoot, config2, context, work
34319
34366
  continue;
34320
34367
  }
34321
34368
  const routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, config2);
34322
- const executePromise = executeStoryInWorktree(story, worktreePath, context, routing, eventEmitter).then((result) => {
34369
+ const storyConfig = storyEffectiveConfigs?.get(story.id);
34370
+ const storyContext = storyConfig ? { ...context, effectiveConfig: storyConfig } : context;
34371
+ const executePromise = executeStoryInWorktree(story, worktreePath, storyContext, routing, eventEmitter).then((result) => {
34323
34372
  results.totalCost += result.cost;
34324
34373
  results.storyCosts.set(story.id, result.cost);
34325
34374
  if (result.success) {
@@ -34354,6 +34403,7 @@ var init_parallel_worker = __esm(() => {
34354
34403
  });
34355
34404
 
34356
34405
  // src/execution/parallel-coordinator.ts
34406
+ import { existsSync as existsSync33, symlinkSync as symlinkSync2 } from "fs";
34357
34407
  import os3 from "os";
34358
34408
  import { join as join48 } from "path";
34359
34409
  function groupStoriesByDependencies(stories) {
@@ -34398,7 +34448,7 @@ function resolveMaxConcurrency(parallel) {
34398
34448
  }
34399
34449
  return Math.max(1, parallel);
34400
34450
  }
34401
- async function executeParallel(stories, prdPath, projectRoot, config2, hooks, plugins, prd, featureDir, parallel, eventEmitter, agentGetFn) {
34451
+ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, plugins, prd, featureDir, parallel, eventEmitter, agentGetFn, pidRegistry, interactionChain) {
34402
34452
  const logger = getSafeLogger();
34403
34453
  const maxConcurrency = resolveMaxConcurrency(parallel);
34404
34454
  const worktreeManager = new WorktreeManager;
@@ -34430,9 +34480,12 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
34430
34480
  hooks,
34431
34481
  plugins,
34432
34482
  storyStartTime: new Date().toISOString(),
34433
- agentGetFn
34483
+ agentGetFn,
34484
+ pidRegistry,
34485
+ interaction: interactionChain ?? undefined
34434
34486
  };
34435
34487
  const worktreePaths = new Map;
34488
+ const storyEffectiveConfigs = new Map;
34436
34489
  for (const story of batch) {
34437
34490
  const worktreePath = join48(projectRoot, ".nax-wt", story.id);
34438
34491
  try {
@@ -34442,6 +34495,27 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
34442
34495
  storyId: story.id,
34443
34496
  worktreePath
34444
34497
  });
34498
+ if (story.workdir) {
34499
+ const pkgNodeModulesSrc = join48(projectRoot, story.workdir, "node_modules");
34500
+ const pkgNodeModulesDst = join48(worktreePath, story.workdir, "node_modules");
34501
+ if (existsSync33(pkgNodeModulesSrc) && !existsSync33(pkgNodeModulesDst)) {
34502
+ try {
34503
+ symlinkSync2(pkgNodeModulesSrc, pkgNodeModulesDst, "dir");
34504
+ logger?.debug("parallel", "Symlinked package node_modules", {
34505
+ storyId: story.id,
34506
+ src: pkgNodeModulesSrc
34507
+ });
34508
+ } catch (symlinkError) {
34509
+ logger?.warn("parallel", "Failed to symlink package node_modules \u2014 test runner may not find deps", {
34510
+ storyId: story.id,
34511
+ error: errorMessage(symlinkError)
34512
+ });
34513
+ }
34514
+ }
34515
+ }
34516
+ const rootConfigPath = join48(projectRoot, ".nax", "config.json");
34517
+ const effectiveConfig = story.workdir ? await loadConfigForWorkdir(rootConfigPath, story.workdir) : config2;
34518
+ storyEffectiveConfigs.set(story.id, effectiveConfig);
34445
34519
  } catch (error48) {
34446
34520
  markStoryFailed(currentPrd, story.id, undefined, undefined);
34447
34521
  logger?.error("parallel", "Failed to create worktree", {
@@ -34450,7 +34524,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
34450
34524
  });
34451
34525
  }
34452
34526
  }
34453
- const batchResult = await executeParallelBatch(batch, projectRoot, config2, baseContext, worktreePaths, maxConcurrency, eventEmitter);
34527
+ const batchResult = await executeParallelBatch(batch, projectRoot, config2, baseContext, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs);
34454
34528
  totalCost += batchResult.totalCost;
34455
34529
  if (batchResult.pipelinePassed.length > 0) {
34456
34530
  const successfulIds = batchResult.pipelinePassed.map((s) => s.id);
@@ -34520,6 +34594,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
34520
34594
  return { storiesCompleted, totalCost, updatedPrd: currentPrd, mergeConflicts: allMergeConflicts };
34521
34595
  }
34522
34596
  var init_parallel_coordinator = __esm(() => {
34597
+ init_loader();
34523
34598
  init_logger2();
34524
34599
  init_prd();
34525
34600
  init_manager();
@@ -34807,7 +34882,7 @@ async function runParallelExecution(options, initialPrd) {
34807
34882
  const batchStoryMetrics = [];
34808
34883
  let conflictedStories = [];
34809
34884
  try {
34810
- const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter, options.agentGetFn);
34885
+ const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter, options.agentGetFn, options.pidRegistry, options.interactionChain);
34811
34886
  const batchDurationMs = Date.now() - batchStartMs;
34812
34887
  const batchCompletedAt = new Date().toISOString();
34813
34888
  prd = parallelResult.updatedPrd;
@@ -67812,7 +67887,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
67812
67887
 
67813
67888
  // bin/nax.ts
67814
67889
  init_source();
67815
- import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
67890
+ import { existsSync as existsSync35, mkdirSync as mkdirSync6 } from "fs";
67816
67891
  import { homedir as homedir8 } from "os";
67817
67892
  import { join as join56 } from "path";
67818
67893
 
@@ -69067,9 +69142,10 @@ async function planCommand(workdir, config2, options) {
69067
69142
  const timeoutSeconds = config2?.execution?.sessionTimeoutSeconds ?? 600;
69068
69143
  let rawResponse;
69069
69144
  if (options.auto) {
69070
- const prompt = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails, config2?.project);
69071
- const cliAdapter = _planDeps.getAgent(agentName);
69072
- if (!cliAdapter)
69145
+ const isAcp = config2?.agent?.protocol === "acp";
69146
+ const prompt = buildPlanningPrompt(specContent, codebaseContext, isAcp ? outputPath : undefined, relativePackages, packageDetails, config2?.project);
69147
+ const adapter = _planDeps.getAgent(agentName, config2);
69148
+ if (!adapter)
69073
69149
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
69074
69150
  let autoModel;
69075
69151
  try {
@@ -69080,20 +69156,52 @@ async function planCommand(workdir, config2, options) {
69080
69156
  if (entry)
69081
69157
  autoModel = resolveModel2(entry).model;
69082
69158
  } catch {}
69083
- rawResponse = await cliAdapter.complete(prompt, {
69084
- model: autoModel,
69085
- jsonMode: true,
69086
- workdir,
69087
- config: config2,
69088
- featureName: options.feature,
69089
- sessionRole: "plan"
69090
- });
69091
- try {
69092
- const envelope = JSON.parse(rawResponse);
69093
- if (envelope?.type === "result" && typeof envelope?.result === "string") {
69094
- rawResponse = envelope.result;
69159
+ if (isAcp) {
69160
+ logger?.info("plan", "Starting ACP auto planning session", {
69161
+ agent: agentName,
69162
+ model: autoModel ?? config2?.plan?.model ?? "balanced",
69163
+ workdir,
69164
+ feature: options.feature,
69165
+ timeoutSeconds
69166
+ });
69167
+ const pidRegistry = new PidRegistry(workdir);
69168
+ try {
69169
+ await adapter.plan({
69170
+ prompt,
69171
+ workdir,
69172
+ interactive: false,
69173
+ timeoutSeconds,
69174
+ config: config2,
69175
+ modelTier: config2?.plan?.model ?? "balanced",
69176
+ dangerouslySkipPermissions: resolvePermissions(config2, "plan").skipPermissions,
69177
+ maxInteractionTurns: config2?.agent?.maxInteractionTurns,
69178
+ featureName: options.feature,
69179
+ pidRegistry,
69180
+ sessionRole: "plan"
69181
+ });
69182
+ } finally {
69183
+ await pidRegistry.killAll().catch(() => {});
69095
69184
  }
69096
- } catch {}
69185
+ if (!_planDeps.existsSync(outputPath)) {
69186
+ throw new Error(`[plan] ACP agent did not write PRD to ${outputPath}. Check agent logs for errors.`);
69187
+ }
69188
+ rawResponse = await _planDeps.readFile(outputPath);
69189
+ } else {
69190
+ rawResponse = await adapter.complete(prompt, {
69191
+ model: autoModel,
69192
+ jsonMode: true,
69193
+ workdir,
69194
+ config: config2,
69195
+ featureName: options.feature,
69196
+ sessionRole: "plan"
69197
+ });
69198
+ try {
69199
+ const envelope = JSON.parse(rawResponse);
69200
+ if (envelope?.type === "result" && typeof envelope?.result === "string") {
69201
+ rawResponse = envelope.result;
69202
+ }
69203
+ } catch {}
69204
+ }
69097
69205
  } else {
69098
69206
  const prompt = buildPlanningPrompt(specContent, codebaseContext, outputPath, relativePackages, packageDetails, config2?.project);
69099
69207
  const adapter = _planDeps.getAgent(agentName, config2);
@@ -72116,7 +72224,9 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
72116
72224
  pluginRegistry,
72117
72225
  formatterMode: options.formatterMode,
72118
72226
  headless: options.headless,
72119
- agentGetFn: options.agentGetFn
72227
+ agentGetFn: options.agentGetFn,
72228
+ pidRegistry: options.pidRegistry,
72229
+ interactionChain: options.interactionChain
72120
72230
  }, prd);
72121
72231
  prd = parallelResult.prd;
72122
72232
  totalCost = parallelResult.totalCost;
@@ -79712,7 +79822,7 @@ Next: nax generate --package ${options.package}`));
79712
79822
  return;
79713
79823
  }
79714
79824
  const naxDir = join56(workdir, ".nax");
79715
- if (existsSync34(naxDir) && !options.force) {
79825
+ if (existsSync35(naxDir) && !options.force) {
79716
79826
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
79717
79827
  return;
79718
79828
  }
@@ -79829,7 +79939,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
79829
79939
  console.error(source_default.red("Error: --plan requires --from <spec-path>"));
79830
79940
  process.exit(1);
79831
79941
  }
79832
- if (options.from && !existsSync34(options.from)) {
79942
+ if (options.from && !existsSync35(options.from)) {
79833
79943
  console.error(source_default.red(`Error: File not found: ${options.from} (required with --plan)`));
79834
79944
  process.exit(1);
79835
79945
  }
@@ -79861,7 +79971,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
79861
79971
  const featureDir = join56(naxDir, "features", options.feature);
79862
79972
  const prdPath = join56(featureDir, "prd.json");
79863
79973
  if (options.plan && options.from) {
79864
- if (existsSync34(prdPath) && !options.force) {
79974
+ if (existsSync35(prdPath) && !options.force) {
79865
79975
  console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
79866
79976
  console.error(source_default.dim(" Use --force to overwrite, or run without --plan to use the existing PRD."));
79867
79977
  process.exit(1);
@@ -79917,7 +80027,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
79917
80027
  process.exit(1);
79918
80028
  }
79919
80029
  }
79920
- if (!existsSync34(prdPath)) {
80030
+ if (!existsSync35(prdPath)) {
79921
80031
  console.error(source_default.red(`Feature "${options.feature}" not found or missing prd.json`));
79922
80032
  process.exit(1);
79923
80033
  }
@@ -79992,7 +80102,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
79992
80102
  });
79993
80103
  const latestSymlink = join56(runsDir, "latest.jsonl");
79994
80104
  try {
79995
- if (existsSync34(latestSymlink)) {
80105
+ if (existsSync35(latestSymlink)) {
79996
80106
  Bun.spawnSync(["rm", latestSymlink]);
79997
80107
  }
79998
80108
  Bun.spawnSync(["ln", "-s", `${runId}.jsonl`, latestSymlink], {
@@ -80090,7 +80200,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
80090
80200
  process.exit(1);
80091
80201
  }
80092
80202
  const featuresDir = join56(naxDir, "features");
80093
- if (!existsSync34(featuresDir)) {
80203
+ if (!existsSync35(featuresDir)) {
80094
80204
  console.log(source_default.dim("No features yet."));
80095
80205
  return;
80096
80206
  }
@@ -80105,7 +80215,7 @@ Features:
80105
80215
  `));
80106
80216
  for (const name of entries) {
80107
80217
  const prdPath = join56(featuresDir, name, "prd.json");
80108
- if (existsSync34(prdPath)) {
80218
+ if (existsSync35(prdPath)) {
80109
80219
  const prd = await loadPRD(prdPath);
80110
80220
  const c = countStories(prd);
80111
80221
  console.log(` ${name} \u2014 ${c.passed}/${c.total} stories done`);
@@ -80176,7 +80286,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
80176
80286
  process.exit(1);
80177
80287
  }
80178
80288
  const featureDir = join56(naxDir, "features", options.feature);
80179
- if (!existsSync34(featureDir)) {
80289
+ if (!existsSync35(featureDir)) {
80180
80290
  console.error(source_default.red(`Feature "${options.feature}" not found.`));
80181
80291
  process.exit(1);
80182
80292
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.54.5",
3
+ "version": "0.54.7",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,7 +13,7 @@
13
13
  "typecheck": "bun x tsc --noEmit",
14
14
  "lint": "bun x biome check src/ bin/",
15
15
  "release": "bun scripts/release.ts",
16
- "test": "bun test test/ --timeout=60000",
16
+ "test": "bun test test/unit/ --timeout=60000 && bun test test/integration/ --timeout=60000 && bun test test/ui/ --timeout=60000",
17
17
  "test:watch": "bun test --watch",
18
18
  "test:unit": "bun test ./test/unit/ --timeout=60000",
19
19
  "test:integration": "bun test ./test/integration/ --timeout=60000",