@nathapp/nax 0.54.5 → 0.54.6

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 +118 -44
  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.6",
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("955ab31"))
22433
+ return "955ab31";
22430
22434
  } catch {}
22431
22435
  try {
22432
22436
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -27464,6 +27468,9 @@ function shouldRetryRectification(state, config2) {
27464
27468
  if (state.attempt >= config2.maxRetries) {
27465
27469
  return false;
27466
27470
  }
27471
+ if (state.lastExitCode !== undefined && state.lastExitCode !== 0 && state.currentFailures === 0) {
27472
+ return true;
27473
+ }
27467
27474
  if (state.currentFailures === 0) {
27468
27475
  return false;
27469
27476
  }
@@ -29754,7 +29761,8 @@ async function runRectificationLoop2(opts) {
29754
29761
  const rectificationState = {
29755
29762
  attempt: 0,
29756
29763
  initialFailures: testSummary.failed,
29757
- currentFailures: testSummary.failed
29764
+ currentFailures: testSummary.failed,
29765
+ lastExitCode: 1
29758
29766
  };
29759
29767
  logger?.info("rectification", `Starting ${label} loop`, {
29760
29768
  storyId: story.id,
@@ -29832,6 +29840,7 @@ ${rectificationPrompt}`;
29832
29840
  if (retryVerification.output) {
29833
29841
  const newTestSummary = parseBunTestOutput(retryVerification.output);
29834
29842
  rectificationState.currentFailures = newTestSummary.failed;
29843
+ rectificationState.lastExitCode = retryVerification.status === "SUCCESS" ? 0 : 1;
29835
29844
  testSummary.failures = newTestSummary.failures;
29836
29845
  testSummary.failed = newTestSummary.failed;
29837
29846
  testSummary.passed = newTestSummary.passed;
@@ -30040,7 +30049,8 @@ function makeFailResult(storyId, strategy, status, opts = {}) {
30040
30049
  failures: opts.failures ?? [],
30041
30050
  rawOutput: opts.rawOutput,
30042
30051
  durationMs: opts.durationMs ?? 0,
30043
- countsTowardEscalation: opts.countsTowardEscalation ?? true
30052
+ countsTowardEscalation: opts.countsTowardEscalation ?? true,
30053
+ exitCode: opts.exitCode
30044
30054
  };
30045
30055
  }
30046
30056
  function makePassResult(storyId, strategy, opts = {}) {
@@ -30449,7 +30459,8 @@ class ScopedStrategy {
30449
30459
  passCount: parsed.passed,
30450
30460
  failCount: parsed.failed,
30451
30461
  failures: parsed.failures,
30452
- durationMs
30462
+ durationMs,
30463
+ exitCode: result.status === "TEST_FAILURE" ? 1 : undefined
30453
30464
  });
30454
30465
  }
30455
30466
  }
@@ -34298,7 +34309,7 @@ async function executeStoryInWorktree(story, worktreePath, context, routing, eve
34298
34309
  };
34299
34310
  }
34300
34311
  }
34301
- async function executeParallelBatch(stories, projectRoot, config2, context, worktreePaths, maxConcurrency, eventEmitter) {
34312
+ async function executeParallelBatch(stories, projectRoot, config2, context, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs) {
34302
34313
  const logger = getSafeLogger();
34303
34314
  const results = {
34304
34315
  pipelinePassed: [],
@@ -34319,7 +34330,9 @@ async function executeParallelBatch(stories, projectRoot, config2, context, work
34319
34330
  continue;
34320
34331
  }
34321
34332
  const routing = routeTask(story.title, story.description, story.acceptanceCriteria, story.tags, config2);
34322
- const executePromise = executeStoryInWorktree(story, worktreePath, context, routing, eventEmitter).then((result) => {
34333
+ const storyConfig = storyEffectiveConfigs?.get(story.id);
34334
+ const storyContext = storyConfig ? { ...context, effectiveConfig: storyConfig } : context;
34335
+ const executePromise = executeStoryInWorktree(story, worktreePath, storyContext, routing, eventEmitter).then((result) => {
34323
34336
  results.totalCost += result.cost;
34324
34337
  results.storyCosts.set(story.id, result.cost);
34325
34338
  if (result.success) {
@@ -34354,6 +34367,7 @@ var init_parallel_worker = __esm(() => {
34354
34367
  });
34355
34368
 
34356
34369
  // src/execution/parallel-coordinator.ts
34370
+ import { existsSync as existsSync33, symlinkSync as symlinkSync2 } from "fs";
34357
34371
  import os3 from "os";
34358
34372
  import { join as join48 } from "path";
34359
34373
  function groupStoriesByDependencies(stories) {
@@ -34398,7 +34412,7 @@ function resolveMaxConcurrency(parallel) {
34398
34412
  }
34399
34413
  return Math.max(1, parallel);
34400
34414
  }
34401
- async function executeParallel(stories, prdPath, projectRoot, config2, hooks, plugins, prd, featureDir, parallel, eventEmitter, agentGetFn) {
34415
+ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, plugins, prd, featureDir, parallel, eventEmitter, agentGetFn, pidRegistry, interactionChain) {
34402
34416
  const logger = getSafeLogger();
34403
34417
  const maxConcurrency = resolveMaxConcurrency(parallel);
34404
34418
  const worktreeManager = new WorktreeManager;
@@ -34430,9 +34444,12 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
34430
34444
  hooks,
34431
34445
  plugins,
34432
34446
  storyStartTime: new Date().toISOString(),
34433
- agentGetFn
34447
+ agentGetFn,
34448
+ pidRegistry,
34449
+ interaction: interactionChain ?? undefined
34434
34450
  };
34435
34451
  const worktreePaths = new Map;
34452
+ const storyEffectiveConfigs = new Map;
34436
34453
  for (const story of batch) {
34437
34454
  const worktreePath = join48(projectRoot, ".nax-wt", story.id);
34438
34455
  try {
@@ -34442,6 +34459,27 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
34442
34459
  storyId: story.id,
34443
34460
  worktreePath
34444
34461
  });
34462
+ if (story.workdir) {
34463
+ const pkgNodeModulesSrc = join48(projectRoot, story.workdir, "node_modules");
34464
+ const pkgNodeModulesDst = join48(worktreePath, story.workdir, "node_modules");
34465
+ if (existsSync33(pkgNodeModulesSrc) && !existsSync33(pkgNodeModulesDst)) {
34466
+ try {
34467
+ symlinkSync2(pkgNodeModulesSrc, pkgNodeModulesDst, "dir");
34468
+ logger?.debug("parallel", "Symlinked package node_modules", {
34469
+ storyId: story.id,
34470
+ src: pkgNodeModulesSrc
34471
+ });
34472
+ } catch (symlinkError) {
34473
+ logger?.warn("parallel", "Failed to symlink package node_modules \u2014 test runner may not find deps", {
34474
+ storyId: story.id,
34475
+ error: errorMessage(symlinkError)
34476
+ });
34477
+ }
34478
+ }
34479
+ }
34480
+ const rootConfigPath = join48(projectRoot, ".nax", "config.json");
34481
+ const effectiveConfig = story.workdir ? await loadConfigForWorkdir(rootConfigPath, story.workdir) : config2;
34482
+ storyEffectiveConfigs.set(story.id, effectiveConfig);
34445
34483
  } catch (error48) {
34446
34484
  markStoryFailed(currentPrd, story.id, undefined, undefined);
34447
34485
  logger?.error("parallel", "Failed to create worktree", {
@@ -34450,7 +34488,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
34450
34488
  });
34451
34489
  }
34452
34490
  }
34453
- const batchResult = await executeParallelBatch(batch, projectRoot, config2, baseContext, worktreePaths, maxConcurrency, eventEmitter);
34491
+ const batchResult = await executeParallelBatch(batch, projectRoot, config2, baseContext, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs);
34454
34492
  totalCost += batchResult.totalCost;
34455
34493
  if (batchResult.pipelinePassed.length > 0) {
34456
34494
  const successfulIds = batchResult.pipelinePassed.map((s) => s.id);
@@ -34520,6 +34558,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
34520
34558
  return { storiesCompleted, totalCost, updatedPrd: currentPrd, mergeConflicts: allMergeConflicts };
34521
34559
  }
34522
34560
  var init_parallel_coordinator = __esm(() => {
34561
+ init_loader();
34523
34562
  init_logger2();
34524
34563
  init_prd();
34525
34564
  init_manager();
@@ -34807,7 +34846,7 @@ async function runParallelExecution(options, initialPrd) {
34807
34846
  const batchStoryMetrics = [];
34808
34847
  let conflictedStories = [];
34809
34848
  try {
34810
- const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter, options.agentGetFn);
34849
+ const parallelResult = await _parallelExecutorDeps.executeParallel(readyStories, prdPath, workdir, config2, hooks, pluginRegistry, prd, featureDir, parallelCount, eventEmitter, options.agentGetFn, options.pidRegistry, options.interactionChain);
34811
34850
  const batchDurationMs = Date.now() - batchStartMs;
34812
34851
  const batchCompletedAt = new Date().toISOString();
34813
34852
  prd = parallelResult.updatedPrd;
@@ -67812,7 +67851,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
67812
67851
 
67813
67852
  // bin/nax.ts
67814
67853
  init_source();
67815
- import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
67854
+ import { existsSync as existsSync35, mkdirSync as mkdirSync6 } from "fs";
67816
67855
  import { homedir as homedir8 } from "os";
67817
67856
  import { join as join56 } from "path";
67818
67857
 
@@ -69067,9 +69106,10 @@ async function planCommand(workdir, config2, options) {
69067
69106
  const timeoutSeconds = config2?.execution?.sessionTimeoutSeconds ?? 600;
69068
69107
  let rawResponse;
69069
69108
  if (options.auto) {
69070
- const prompt = buildPlanningPrompt(specContent, codebaseContext, undefined, relativePackages, packageDetails, config2?.project);
69071
- const cliAdapter = _planDeps.getAgent(agentName);
69072
- if (!cliAdapter)
69109
+ const isAcp = config2?.agent?.protocol === "acp";
69110
+ const prompt = buildPlanningPrompt(specContent, codebaseContext, isAcp ? outputPath : undefined, relativePackages, packageDetails, config2?.project);
69111
+ const adapter = _planDeps.getAgent(agentName, config2);
69112
+ if (!adapter)
69073
69113
  throw new Error(`[plan] No agent adapter found for '${agentName}'`);
69074
69114
  let autoModel;
69075
69115
  try {
@@ -69080,20 +69120,52 @@ async function planCommand(workdir, config2, options) {
69080
69120
  if (entry)
69081
69121
  autoModel = resolveModel2(entry).model;
69082
69122
  } 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;
69123
+ if (isAcp) {
69124
+ logger?.info("plan", "Starting ACP auto planning session", {
69125
+ agent: agentName,
69126
+ model: autoModel ?? config2?.plan?.model ?? "balanced",
69127
+ workdir,
69128
+ feature: options.feature,
69129
+ timeoutSeconds
69130
+ });
69131
+ const pidRegistry = new PidRegistry(workdir);
69132
+ try {
69133
+ await adapter.plan({
69134
+ prompt,
69135
+ workdir,
69136
+ interactive: false,
69137
+ timeoutSeconds,
69138
+ config: config2,
69139
+ modelTier: config2?.plan?.model ?? "balanced",
69140
+ dangerouslySkipPermissions: resolvePermissions(config2, "plan").skipPermissions,
69141
+ maxInteractionTurns: config2?.agent?.maxInteractionTurns,
69142
+ featureName: options.feature,
69143
+ pidRegistry,
69144
+ sessionRole: "plan"
69145
+ });
69146
+ } finally {
69147
+ await pidRegistry.killAll().catch(() => {});
69095
69148
  }
69096
- } catch {}
69149
+ if (!_planDeps.existsSync(outputPath)) {
69150
+ throw new Error(`[plan] ACP agent did not write PRD to ${outputPath}. Check agent logs for errors.`);
69151
+ }
69152
+ rawResponse = await _planDeps.readFile(outputPath);
69153
+ } else {
69154
+ rawResponse = await adapter.complete(prompt, {
69155
+ model: autoModel,
69156
+ jsonMode: true,
69157
+ workdir,
69158
+ config: config2,
69159
+ featureName: options.feature,
69160
+ sessionRole: "plan"
69161
+ });
69162
+ try {
69163
+ const envelope = JSON.parse(rawResponse);
69164
+ if (envelope?.type === "result" && typeof envelope?.result === "string") {
69165
+ rawResponse = envelope.result;
69166
+ }
69167
+ } catch {}
69168
+ }
69097
69169
  } else {
69098
69170
  const prompt = buildPlanningPrompt(specContent, codebaseContext, outputPath, relativePackages, packageDetails, config2?.project);
69099
69171
  const adapter = _planDeps.getAgent(agentName, config2);
@@ -72116,7 +72188,9 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
72116
72188
  pluginRegistry,
72117
72189
  formatterMode: options.formatterMode,
72118
72190
  headless: options.headless,
72119
- agentGetFn: options.agentGetFn
72191
+ agentGetFn: options.agentGetFn,
72192
+ pidRegistry: options.pidRegistry,
72193
+ interactionChain: options.interactionChain
72120
72194
  }, prd);
72121
72195
  prd = parallelResult.prd;
72122
72196
  totalCost = parallelResult.totalCost;
@@ -79712,7 +79786,7 @@ Next: nax generate --package ${options.package}`));
79712
79786
  return;
79713
79787
  }
79714
79788
  const naxDir = join56(workdir, ".nax");
79715
- if (existsSync34(naxDir) && !options.force) {
79789
+ if (existsSync35(naxDir) && !options.force) {
79716
79790
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
79717
79791
  return;
79718
79792
  }
@@ -79829,7 +79903,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
79829
79903
  console.error(source_default.red("Error: --plan requires --from <spec-path>"));
79830
79904
  process.exit(1);
79831
79905
  }
79832
- if (options.from && !existsSync34(options.from)) {
79906
+ if (options.from && !existsSync35(options.from)) {
79833
79907
  console.error(source_default.red(`Error: File not found: ${options.from} (required with --plan)`));
79834
79908
  process.exit(1);
79835
79909
  }
@@ -79861,7 +79935,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
79861
79935
  const featureDir = join56(naxDir, "features", options.feature);
79862
79936
  const prdPath = join56(featureDir, "prd.json");
79863
79937
  if (options.plan && options.from) {
79864
- if (existsSync34(prdPath) && !options.force) {
79938
+ if (existsSync35(prdPath) && !options.force) {
79865
79939
  console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
79866
79940
  console.error(source_default.dim(" Use --force to overwrite, or run without --plan to use the existing PRD."));
79867
79941
  process.exit(1);
@@ -79917,7 +79991,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
79917
79991
  process.exit(1);
79918
79992
  }
79919
79993
  }
79920
- if (!existsSync34(prdPath)) {
79994
+ if (!existsSync35(prdPath)) {
79921
79995
  console.error(source_default.red(`Feature "${options.feature}" not found or missing prd.json`));
79922
79996
  process.exit(1);
79923
79997
  }
@@ -79992,7 +80066,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
79992
80066
  });
79993
80067
  const latestSymlink = join56(runsDir, "latest.jsonl");
79994
80068
  try {
79995
- if (existsSync34(latestSymlink)) {
80069
+ if (existsSync35(latestSymlink)) {
79996
80070
  Bun.spawnSync(["rm", latestSymlink]);
79997
80071
  }
79998
80072
  Bun.spawnSync(["ln", "-s", `${runId}.jsonl`, latestSymlink], {
@@ -80090,7 +80164,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
80090
80164
  process.exit(1);
80091
80165
  }
80092
80166
  const featuresDir = join56(naxDir, "features");
80093
- if (!existsSync34(featuresDir)) {
80167
+ if (!existsSync35(featuresDir)) {
80094
80168
  console.log(source_default.dim("No features yet."));
80095
80169
  return;
80096
80170
  }
@@ -80105,7 +80179,7 @@ Features:
80105
80179
  `));
80106
80180
  for (const name of entries) {
80107
80181
  const prdPath = join56(featuresDir, name, "prd.json");
80108
- if (existsSync34(prdPath)) {
80182
+ if (existsSync35(prdPath)) {
80109
80183
  const prd = await loadPRD(prdPath);
80110
80184
  const c = countStories(prd);
80111
80185
  console.log(` ${name} \u2014 ${c.passed}/${c.total} stories done`);
@@ -80176,7 +80250,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
80176
80250
  process.exit(1);
80177
80251
  }
80178
80252
  const featureDir = join56(naxDir, "features", options.feature);
80179
- if (!existsSync34(featureDir)) {
80253
+ if (!existsSync35(featureDir)) {
80180
80254
  console.error(source_default.red(`Feature "${options.feature}" not found.`));
80181
80255
  process.exit(1);
80182
80256
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.54.5",
3
+ "version": "0.54.6",
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",