@nathapp/nax 0.58.0 → 0.58.1

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 +251 -147
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -3596,75 +3596,106 @@ var init_env = __esm(() => {
3596
3596
  });
3597
3597
 
3598
3598
  // src/agents/acp/parser.ts
3599
- function parseAcpxJsonOutput(rawOutput) {
3600
- const lines = rawOutput.split(`
3601
- `).filter((l) => l.trim());
3602
- let text = "";
3603
- let tokenUsage;
3604
- let exactCostUsd;
3605
- let stopReason;
3606
- let error;
3607
- for (const line of lines) {
3608
- try {
3609
- const event = JSON.parse(line);
3610
- if (event.jsonrpc === "2.0") {
3611
- if (event.method === "session/update" && event.params?.update) {
3612
- const update = event.params.update;
3613
- if (update.sessionUpdate === "agent_message_chunk" && update.content?.type === "text" && update.content.text) {
3614
- text += update.content.text;
3615
- }
3616
- if (update.sessionUpdate === "usage_update" && typeof update.cost?.amount === "number") {
3617
- exactCostUsd = update.cost.amount;
3618
- }
3619
- }
3620
- if (event.id !== undefined && event.result && typeof event.result === "object") {
3621
- const result = event.result;
3622
- if (result.stopReason)
3623
- stopReason = result.stopReason;
3624
- if (result.stop_reason)
3625
- stopReason = result.stop_reason;
3626
- if (result.usage && typeof result.usage === "object") {
3627
- const u = result.usage;
3628
- tokenUsage = {
3629
- input_tokens: u.inputTokens ?? u.input_tokens ?? 0,
3630
- output_tokens: u.outputTokens ?? u.output_tokens ?? 0,
3631
- cache_read_input_tokens: u.cachedReadTokens ?? u.cache_read_input_tokens ?? 0,
3632
- cache_creation_input_tokens: u.cachedWriteTokens ?? u.cache_creation_input_tokens ?? 0
3633
- };
3634
- }
3599
+ function createParseState() {
3600
+ return { text: "", tokenUsage: undefined, exactCostUsd: undefined, stopReason: undefined, error: undefined };
3601
+ }
3602
+ function parseAcpxJsonLine(line, state) {
3603
+ try {
3604
+ const event = JSON.parse(line);
3605
+ if (event.jsonrpc === "2.0") {
3606
+ if (event.method === "session/update" && event.params?.update) {
3607
+ const update = event.params.update;
3608
+ if (update.sessionUpdate === "agent_message_chunk" && update.content?.type === "text" && update.content.text) {
3609
+ state.text += update.content.text;
3610
+ }
3611
+ if (update.sessionUpdate === "usage_update" && typeof update.cost?.amount === "number") {
3612
+ state.exactCostUsd = update.cost.amount;
3613
+ }
3614
+ }
3615
+ if (event.id !== undefined && event.result && typeof event.result === "object") {
3616
+ const result = event.result;
3617
+ if (result.stopReason)
3618
+ state.stopReason = result.stopReason;
3619
+ if (result.stop_reason)
3620
+ state.stopReason = result.stop_reason;
3621
+ if (result.usage && typeof result.usage === "object") {
3622
+ const u = result.usage;
3623
+ state.tokenUsage = {
3624
+ input_tokens: u.inputTokens ?? u.input_tokens ?? 0,
3625
+ output_tokens: u.outputTokens ?? u.output_tokens ?? 0,
3626
+ cache_read_input_tokens: u.cachedReadTokens ?? u.cache_read_input_tokens ?? 0,
3627
+ cache_creation_input_tokens: u.cachedWriteTokens ?? u.cache_creation_input_tokens ?? 0
3628
+ };
3635
3629
  }
3636
- continue;
3637
- }
3638
- if (event.content && typeof event.content === "string")
3639
- text += event.content;
3640
- if (event.text && typeof event.text === "string")
3641
- text += event.text;
3642
- if (event.result && typeof event.result === "string")
3643
- text = event.result;
3644
- if (event.cumulative_token_usage)
3645
- tokenUsage = event.cumulative_token_usage;
3646
- if (event.usage) {
3647
- tokenUsage = {
3648
- input_tokens: event.usage.input_tokens ?? event.usage.prompt_tokens ?? 0,
3649
- output_tokens: event.usage.output_tokens ?? event.usage.completion_tokens ?? 0
3650
- };
3651
- }
3652
- if (event.stopReason)
3653
- stopReason = event.stopReason;
3654
- if (event.stop_reason)
3655
- stopReason = event.stop_reason;
3656
- if (event.error) {
3657
- error = typeof event.error === "string" ? event.error : event.error.message ?? JSON.stringify(event.error);
3658
3630
  }
3659
- } catch {
3660
- if (!text)
3661
- text = line;
3631
+ return;
3632
+ }
3633
+ if (event.content && typeof event.content === "string")
3634
+ state.text += event.content;
3635
+ if (event.text && typeof event.text === "string")
3636
+ state.text += event.text;
3637
+ if (event.result && typeof event.result === "string")
3638
+ state.text = event.result;
3639
+ if (event.cumulative_token_usage)
3640
+ state.tokenUsage = event.cumulative_token_usage;
3641
+ if (event.usage) {
3642
+ state.tokenUsage = {
3643
+ input_tokens: event.usage.input_tokens ?? event.usage.prompt_tokens ?? 0,
3644
+ output_tokens: event.usage.output_tokens ?? event.usage.completion_tokens ?? 0
3645
+ };
3662
3646
  }
3647
+ if (event.stopReason)
3648
+ state.stopReason = event.stopReason;
3649
+ if (event.stop_reason)
3650
+ state.stopReason = event.stop_reason;
3651
+ if (event.error) {
3652
+ state.error = typeof event.error === "string" ? event.error : event.error.message ?? JSON.stringify(event.error);
3653
+ }
3654
+ } catch {
3655
+ if (!state.text)
3656
+ state.text = line;
3663
3657
  }
3664
- return { text: text.trim(), tokenUsage, exactCostUsd, stopReason, error };
3658
+ }
3659
+ function finalizeParseState(state) {
3660
+ return {
3661
+ text: state.text.trim(),
3662
+ tokenUsage: state.tokenUsage,
3663
+ exactCostUsd: state.exactCostUsd,
3664
+ stopReason: state.stopReason,
3665
+ error: state.error
3666
+ };
3665
3667
  }
3666
3668
 
3667
3669
  // src/agents/acp/spawn-client.ts
3670
+ async function readAndParseLines(stream, state) {
3671
+ const decoder = new TextDecoder;
3672
+ let remainder = "";
3673
+ const reader = stream.getReader();
3674
+ try {
3675
+ while (true) {
3676
+ const { done, value } = await reader.read();
3677
+ if (done)
3678
+ break;
3679
+ remainder += decoder.decode(value, { stream: true });
3680
+ for (;; ) {
3681
+ const nl = remainder.indexOf(`
3682
+ `);
3683
+ if (nl < 0)
3684
+ break;
3685
+ const line = remainder.slice(0, nl);
3686
+ remainder = remainder.slice(nl + 1);
3687
+ if (line.trim())
3688
+ parseAcpxJsonLine(line, state);
3689
+ }
3690
+ }
3691
+ remainder += decoder.decode();
3692
+ if (remainder.trim())
3693
+ parseAcpxJsonLine(remainder.trim(), state);
3694
+ } finally {
3695
+ reader.releaseLock();
3696
+ }
3697
+ }
3698
+
3668
3699
  class SpawnAcpSession {
3669
3700
  agentName;
3670
3701
  sessionName;
@@ -3729,13 +3760,22 @@ class SpawnAcpSession {
3729
3760
  session: this.sessionName
3730
3761
  });
3731
3762
  }
3732
- const stdoutPromise = new Response(proc.stdout).text().catch(() => "");
3763
+ const parseState = createParseState();
3764
+ const parsePromise = readAndParseLines(proc.stdout, parseState).catch(() => {});
3733
3765
  const stderrPromise = new Response(proc.stderr).text().catch(() => "");
3734
3766
  const exitCode = await proc.exited;
3735
- const drained = Bun.sleep(_spawnClientDeps.streamDrainTimeoutMs).then(() => "");
3736
- const [stdout, stderr] = await Promise.all([
3737
- Promise.race([stdoutPromise, drained]),
3738
- Promise.race([stderrPromise, drained])
3767
+ const makeDrain = (ms) => {
3768
+ let id;
3769
+ const promise = new Promise((resolve) => {
3770
+ id = setTimeout(() => resolve(""), ms);
3771
+ });
3772
+ return { promise, cancel: () => clearTimeout(id) };
3773
+ };
3774
+ const drainA = makeDrain(_spawnClientDeps.streamDrainTimeoutMs);
3775
+ const drainB = makeDrain(_spawnClientDeps.streamDrainTimeoutMs);
3776
+ const [, stderr] = await Promise.all([
3777
+ Promise.race([parsePromise, drainA.promise]).finally(() => drainA.cancel()),
3778
+ Promise.race([stderrPromise, drainB.promise]).finally(() => drainB.cancel())
3739
3779
  ]);
3740
3780
  if (exitCode !== 0) {
3741
3781
  getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
@@ -3748,7 +3788,7 @@ class SpawnAcpSession {
3748
3788
  };
3749
3789
  }
3750
3790
  try {
3751
- const parsed = parseAcpxJsonOutput(stdout);
3791
+ const parsed = finalizeParseState(parseState);
3752
3792
  return {
3753
3793
  messages: [{ role: "assistant", content: parsed.text || "" }],
3754
3794
  stopReason: parsed.stopReason ?? "end_turn",
@@ -18209,7 +18249,9 @@ var init_schemas3 = __esm(() => {
18209
18249
  typecheck: exports_external.string().optional(),
18210
18250
  lint: exports_external.string().optional(),
18211
18251
  test: exports_external.string().optional(),
18212
- build: exports_external.string().optional()
18252
+ build: exports_external.string().optional(),
18253
+ lintFix: exports_external.string().optional(),
18254
+ formatFix: exports_external.string().optional()
18213
18255
  }),
18214
18256
  pluginMode: exports_external.enum(["per-story", "deferred"]).default("per-story"),
18215
18257
  semantic: SemanticReviewConfigSchema.optional(),
@@ -20862,7 +20904,7 @@ function isPlainObject2(value) {
20862
20904
 
20863
20905
  // src/config/path-security.ts
20864
20906
  import { existsSync as existsSync3, lstatSync, realpathSync } from "fs";
20865
- import { isAbsolute as isAbsolute3, normalize, resolve } from "path";
20907
+ import { basename, isAbsolute as isAbsolute3, normalize, resolve } from "path";
20866
20908
  function validateDirectory(dirPath, baseDir) {
20867
20909
  const resolved = resolve(dirPath);
20868
20910
  if (!existsSync3(resolved)) {
@@ -20909,7 +20951,7 @@ function validateFilePath(filePath, baseDir) {
20909
20951
  const parent = resolve(resolved, "..");
20910
20952
  if (existsSync3(parent)) {
20911
20953
  const realParent = realpathSync(parent);
20912
- realPath = resolve(realParent, filePath.split("/").pop() || "");
20954
+ realPath = resolve(realParent, basename(resolved));
20913
20955
  } else {
20914
20956
  realPath = resolved;
20915
20957
  }
@@ -21093,7 +21135,7 @@ var init_profile = __esm(() => {
21093
21135
 
21094
21136
  // src/config/loader.ts
21095
21137
  import { existsSync as existsSync4 } from "fs";
21096
- import { basename, dirname, join as join6, resolve as resolve3 } from "path";
21138
+ import { basename as basename2, dirname, join as join6, resolve as resolve3 } from "path";
21097
21139
  function globalConfigPath() {
21098
21140
  return join6(globalConfigDir(), "config.json");
21099
21141
  }
@@ -21148,8 +21190,8 @@ function applyBatchModeCompat(conf) {
21148
21190
  }
21149
21191
  async function loadConfig(startDir, cliOverrides) {
21150
21192
  let rawConfig = structuredClone(DEFAULT_CONFIG);
21151
- const projDir = startDir ? basename(startDir) === PROJECT_NAX_DIR ? startDir : findProjectDir(startDir) : findProjectDir();
21152
- const projectRoot = startDir ? basename(startDir) === PROJECT_NAX_DIR ? dirname(startDir) : startDir : process.cwd();
21193
+ const projDir = startDir ? basename2(startDir) === PROJECT_NAX_DIR ? startDir : findProjectDir(startDir) : findProjectDir();
21194
+ const projectRoot = startDir ? basename2(startDir) === PROJECT_NAX_DIR ? dirname(startDir) : startDir : process.cwd();
21153
21195
  const profileName = await resolveProfileName(cliOverrides ?? {}, process.env, projectRoot);
21154
21196
  const globalConfRaw = await loadJsonFile(globalConfigPath(), "config");
21155
21197
  if (globalConfRaw) {
@@ -21202,7 +21244,7 @@ async function loadConfigForWorkdir(rootConfigPath, packageDir) {
21202
21244
  const packageConfigPath = join6(repoRoot, PROJECT_NAX_DIR, "mono", packageDir, "config.json");
21203
21245
  const packageOverride = await loadJsonFile(packageConfigPath, "config");
21204
21246
  if (!packageOverride) {
21205
- logger.debug("config", "Per-package config not found \u2014 falling back to root config", {
21247
+ logger.info("config", "Per-package config not found \u2014 falling back to root config", {
21206
21248
  packageConfigPath,
21207
21249
  packageDir
21208
21250
  });
@@ -22761,7 +22803,7 @@ var package_default;
22761
22803
  var init_package = __esm(() => {
22762
22804
  package_default = {
22763
22805
  name: "@nathapp/nax",
22764
- version: "0.58.0",
22806
+ version: "0.58.1",
22765
22807
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22766
22808
  type: "module",
22767
22809
  bin: {
@@ -22841,8 +22883,8 @@ var init_version = __esm(() => {
22841
22883
  NAX_VERSION = package_default.version;
22842
22884
  NAX_COMMIT = (() => {
22843
22885
  try {
22844
- if (/^[0-9a-f]{6,10}$/.test("073d6b70"))
22845
- return "073d6b70";
22886
+ if (/^[0-9a-f]{6,10}$/.test("2975c129"))
22887
+ return "2975c129";
22846
22888
  } catch {}
22847
22889
  try {
22848
22890
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -29092,8 +29134,8 @@ var init_autofix = __esm(() => {
29092
29134
  return { action: "continue" };
29093
29135
  }
29094
29136
  const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
29095
- const lintFixCmd = effectiveConfig.quality.commands.lintFix;
29096
- const formatFixCmd = effectiveConfig.quality.commands.formatFix;
29137
+ const lintFixCmd = effectiveConfig.quality.commands.lintFix ?? effectiveConfig.review.commands.lintFix;
29138
+ const formatFixCmd = effectiveConfig.quality.commands.formatFix ?? effectiveConfig.review.commands.formatFix;
29097
29139
  const effectiveWorkdir = ctx.story.workdir ? join20(ctx.workdir, ctx.story.workdir) : ctx.workdir;
29098
29140
  const failedCheckNames = new Set((reviewResult.checks ?? []).filter((c) => !c.success).map((c) => c.check));
29099
29141
  const hasLintFailure = failedCheckNames.has("lint");
@@ -29757,8 +29799,8 @@ function extractTestStructure(source) {
29757
29799
  function deriveTestPatterns(contextFiles) {
29758
29800
  const patterns = new Set;
29759
29801
  for (const filePath of contextFiles) {
29760
- const basename2 = path7.basename(filePath);
29761
- const basenameNoExt = basename2.replace(/\.(ts|js|tsx|jsx)$/, "");
29802
+ const basename3 = path7.basename(filePath);
29803
+ const basenameNoExt = basename3.replace(/\.(ts|js|tsx|jsx)$/, "");
29762
29804
  patterns.add(`${basenameNoExt}.test.ts`);
29763
29805
  patterns.add(`${basenameNoExt}.test.js`);
29764
29806
  patterns.add(`${basenameNoExt}.test.tsx`);
@@ -29811,8 +29853,8 @@ async function scanTestFiles(options) {
29811
29853
  const files = [];
29812
29854
  for await (const filePath of glob.scan({ cwd: scanDir, absolute: false })) {
29813
29855
  if (allowedBasenames !== null) {
29814
- const basename2 = path7.basename(filePath);
29815
- if (!allowedBasenames.has(basename2)) {
29856
+ const basename3 = path7.basename(filePath);
29857
+ if (!allowedBasenames.has(basename3)) {
29816
29858
  continue;
29817
29859
  }
29818
29860
  }
@@ -34044,8 +34086,8 @@ function extractSearchTerms(sourceFile) {
34044
34086
  const withoutSrc = sourceFile.replace(/^src\//, "");
34045
34087
  const withoutExt = withoutSrc.replace(/\.ts$/, "");
34046
34088
  const parts = withoutExt.split("/");
34047
- const basename2 = parts[parts.length - 1];
34048
- return [`/${basename2}`, withoutExt];
34089
+ const basename3 = parts[parts.length - 1];
34090
+ return [`/${basename3}`, withoutExt];
34049
34091
  }
34050
34092
  async function importGrepFallback(sourceFiles, workdir, testFilePatterns) {
34051
34093
  if (sourceFiles.length === 0 || testFilePatterns.length === 0)
@@ -34403,7 +34445,6 @@ var init_regression2 = __esm(() => {
34403
34445
  import { join as join28 } from "path";
34404
34446
  var routingStage, _routingDeps;
34405
34447
  var init_routing2 = __esm(() => {
34406
- init_registry();
34407
34448
  init_greenfield();
34408
34449
  init_logger2();
34409
34450
  init_prd();
@@ -34417,6 +34458,9 @@ var init_routing2 = __esm(() => {
34417
34458
  const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
34418
34459
  const agentName = effectiveConfig.execution?.agent ?? "claude";
34419
34460
  const adapter = ctx.agentGetFn ? ctx.agentGetFn(agentName) : undefined;
34461
+ if (ctx.story.id === ctx.stories[0]?.id) {
34462
+ _routingDeps.clearCache();
34463
+ }
34420
34464
  const decision = await _routingDeps.resolveRouting(ctx.story, effectiveConfig, ctx.plugins, adapter);
34421
34465
  const TIER_RANK = { fast: 0, balanced: 1, powerful: 2 };
34422
34466
  const derivedTier = decision.modelTier;
@@ -34467,8 +34511,7 @@ var init_routing2 = __esm(() => {
34467
34511
  complexityToModelTier,
34468
34512
  isGreenfieldStory,
34469
34513
  clearCache,
34470
- savePRD,
34471
- getAgent
34514
+ savePRD
34472
34515
  };
34473
34516
  });
34474
34517
 
@@ -34769,7 +34812,7 @@ __export(exports_init_context, {
34769
34812
  });
34770
34813
  import { existsSync as existsSync24 } from "fs";
34771
34814
  import { mkdir as mkdir2 } from "fs/promises";
34772
- import { basename as basename3, join as join33 } from "path";
34815
+ import { basename as basename4, join as join33 } from "path";
34773
34816
  async function findFiles(dir, maxFiles = 200) {
34774
34817
  try {
34775
34818
  const proc = Bun.spawnSync([
@@ -34857,7 +34900,7 @@ async function scanProject(projectRoot) {
34857
34900
  const readmeSnippet = await readReadmeSnippet(projectRoot);
34858
34901
  const entryPoints = await detectEntryPoints(projectRoot);
34859
34902
  const configFiles = await detectConfigFiles(projectRoot);
34860
- const projectName = packageManifest?.name || basename3(projectRoot);
34903
+ const projectName = packageManifest?.name || basename4(projectRoot);
34861
34904
  return {
34862
34905
  projectName,
34863
34906
  fileTree,
@@ -35439,11 +35482,11 @@ function getSafeLogger6() {
35439
35482
  return getSafeLogger();
35440
35483
  }
35441
35484
  function extractPluginName(pluginPath) {
35442
- const basename5 = path13.basename(pluginPath);
35443
- if (basename5 === "index.ts" || basename5 === "index.js" || basename5 === "index.mjs") {
35485
+ const basename6 = path13.basename(pluginPath);
35486
+ if (basename6 === "index.ts" || basename6 === "index.js" || basename6 === "index.mjs") {
35444
35487
  return path13.basename(path13.dirname(pluginPath));
35445
35488
  }
35446
- return basename5.replace(/\.(ts|js|mjs)$/, "");
35489
+ return basename6.replace(/\.(ts|js|mjs)$/, "");
35447
35490
  }
35448
35491
  async function loadPlugins(globalDir, projectDir, configPlugins, projectRoot, disabledPlugins) {
35449
35492
  const loadedPlugins = [];
@@ -35685,8 +35728,50 @@ function validateHookCommand(command) {
35685
35728
  }
35686
35729
  }
35687
35730
  function parseCommandToArgv(command) {
35688
- const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
35689
- return command.trim().split(/\s+/).map((token) => token.startsWith("~/") ? home + token.slice(1) : token);
35731
+ const safeEnv = buildAllowedEnv();
35732
+ const home = safeEnv.HOME ?? "";
35733
+ const args = [];
35734
+ let current = "";
35735
+ let i = 0;
35736
+ const s = command.trim();
35737
+ while (i < s.length) {
35738
+ const ch = s[i];
35739
+ if (ch === " " || ch === "\t") {
35740
+ if (current.length > 0) {
35741
+ args.push(current);
35742
+ current = "";
35743
+ }
35744
+ i++;
35745
+ continue;
35746
+ }
35747
+ if (ch === "'") {
35748
+ i++;
35749
+ while (i < s.length && s[i] !== "'") {
35750
+ current += s[i++];
35751
+ }
35752
+ i++;
35753
+ continue;
35754
+ }
35755
+ if (ch === '"') {
35756
+ i++;
35757
+ while (i < s.length && s[i] !== '"') {
35758
+ if (s[i] === "\\" && i + 1 < s.length && (s[i + 1] === '"' || s[i + 1] === "\\")) {
35759
+ current += s[i + 1];
35760
+ i += 2;
35761
+ } else {
35762
+ current += s[i++];
35763
+ }
35764
+ }
35765
+ i++;
35766
+ continue;
35767
+ }
35768
+ current += ch;
35769
+ i++;
35770
+ }
35771
+ if (current.length > 0) {
35772
+ args.push(current);
35773
+ }
35774
+ return args.map((token) => token.startsWith("~/") && home ? home + token.slice(1) : token);
35690
35775
  }
35691
35776
  async function executeHook(hookDef, ctx, workdir) {
35692
35777
  if (hookDef.enabled === false) {
@@ -35719,7 +35804,7 @@ async function executeHook(hookDef, ctx, workdir) {
35719
35804
  stdin: new Response(contextJson),
35720
35805
  stdout: "pipe",
35721
35806
  stderr: "pipe",
35722
- env: { ...process.env, ...env2 }
35807
+ env: buildAllowedEnv({ env: env2 })
35723
35808
  });
35724
35809
  const timeoutId = setTimeout(() => {
35725
35810
  killProcessGroup(proc.pid, "SIGTERM");
@@ -35769,6 +35854,7 @@ async function fireHook(config2, event, ctx, workdir) {
35769
35854
  }
35770
35855
  var DEFAULT_TIMEOUT = 5000;
35771
35856
  var init_runner4 = __esm(() => {
35857
+ init_env();
35772
35858
  init_logger2();
35773
35859
  init_json_file();
35774
35860
  });
@@ -35780,46 +35866,51 @@ var init_hooks = __esm(() => {
35780
35866
 
35781
35867
  // src/execution/crash-heartbeat.ts
35782
35868
  import { appendFileSync as appendFileSync2 } from "fs";
35783
- function startHeartbeat(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
35869
+ async function heartbeatLoop(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
35784
35870
  const logger = getSafeLogger();
35785
- stopHeartbeat();
35786
- heartbeatTimer = setInterval(() => {
35787
- (async () => {
35788
- try {
35789
- logger?.debug("crash-recovery", "Heartbeat");
35790
- if (jsonlFilePath) {
35791
- const heartbeatEntry = {
35792
- timestamp: new Date().toISOString(),
35793
- level: "debug",
35794
- stage: "heartbeat",
35795
- message: "Process alive",
35796
- data: {
35797
- pid: process.pid,
35798
- memoryUsageMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024)
35799
- }
35800
- };
35801
- const line = `${JSON.stringify(heartbeatEntry)}
35871
+ while (heartbeatActive) {
35872
+ await Bun.sleep(60000);
35873
+ if (!heartbeatActive)
35874
+ break;
35875
+ try {
35876
+ logger?.debug("crash-recovery", "Heartbeat");
35877
+ if (jsonlFilePath) {
35878
+ const heartbeatEntry = {
35879
+ timestamp: new Date().toISOString(),
35880
+ level: "debug",
35881
+ stage: "heartbeat",
35882
+ message: "Process alive",
35883
+ data: {
35884
+ pid: process.pid,
35885
+ memoryUsageMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024)
35886
+ }
35887
+ };
35888
+ const line = `${JSON.stringify(heartbeatEntry)}
35802
35889
  `;
35803
- appendFileSync2(jsonlFilePath, line);
35804
- }
35805
- await statusWriter.update(getTotalCost(), getIterations(), {
35806
- lastHeartbeat: new Date().toISOString()
35807
- });
35808
- } catch (err) {
35809
- logger?.warn("crash-recovery", "Failed during heartbeat", { error: err.message });
35890
+ appendFileSync2(jsonlFilePath, line);
35810
35891
  }
35811
- })().catch(() => {});
35812
- }, 60000);
35892
+ await statusWriter.update(getTotalCost(), getIterations(), {
35893
+ lastHeartbeat: new Date().toISOString()
35894
+ });
35895
+ } catch (err) {
35896
+ logger?.warn("crash-recovery", "Failed during heartbeat", { error: err.message });
35897
+ }
35898
+ }
35899
+ }
35900
+ function startHeartbeat(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
35901
+ const logger = getSafeLogger();
35902
+ stopHeartbeat();
35903
+ heartbeatActive = true;
35904
+ heartbeatLoop(statusWriter, getTotalCost, getIterations, jsonlFilePath).catch(() => {});
35813
35905
  logger?.debug("crash-recovery", "Heartbeat started (60s interval)");
35814
35906
  }
35815
35907
  function stopHeartbeat() {
35816
- if (heartbeatTimer) {
35817
- clearInterval(heartbeatTimer);
35818
- heartbeatTimer = null;
35908
+ if (heartbeatActive) {
35909
+ heartbeatActive = false;
35819
35910
  getSafeLogger()?.debug("crash-recovery", "Heartbeat stopped");
35820
35911
  }
35821
35912
  }
35822
- var heartbeatTimer = null;
35913
+ var heartbeatActive = false;
35823
35914
  var init_crash_heartbeat = __esm(() => {
35824
35915
  init_logger2();
35825
35916
  });
@@ -35847,7 +35938,8 @@ async function writeFatalLog(jsonlFilePath, signal, error48) {
35847
35938
  `;
35848
35939
  appendFileSync3(jsonlFilePath, line);
35849
35940
  } catch (err) {
35850
- console.error("[crash-recovery] Failed to write fatal log:", err);
35941
+ process.stderr.write(`[crash-recovery] Failed to write fatal log: ${String(err)}
35942
+ `);
35851
35943
  }
35852
35944
  }
35853
35945
  async function writeRunComplete(ctx, exitReason) {
@@ -35883,7 +35975,8 @@ async function writeRunComplete(ctx, exitReason) {
35883
35975
  appendFileSync3(ctx.jsonlFilePath, line);
35884
35976
  logger?.debug("crash-recovery", "run.complete event written", { exitReason });
35885
35977
  } catch (err) {
35886
- console.error("[crash-recovery] Failed to write run.complete event:", err);
35978
+ process.stderr.write(`[crash-recovery] Failed to write run.complete event: ${String(err)}
35979
+ `);
35887
35980
  }
35888
35981
  }
35889
35982
  async function updateStatusToCrashed(statusWriter, totalCost, iterations, signal, featureDir) {
@@ -35900,7 +35993,8 @@ async function updateStatusToCrashed(statusWriter, totalCost, iterations, signal
35900
35993
  });
35901
35994
  }
35902
35995
  } catch (err) {
35903
- console.error("[crash-recovery] Failed to update status.json:", err);
35996
+ process.stderr.write(`[crash-recovery] Failed to update status.json: ${String(err)}
35997
+ `);
35904
35998
  }
35905
35999
  }
35906
36000
  async function writeExitSummary(jsonlFilePath, totalCost, iterations, storiesCompleted, durationMs) {
@@ -37366,10 +37460,10 @@ var init_headless_formatter = __esm(() => {
37366
37460
  // src/pipeline/subscribers/events-writer.ts
37367
37461
  import { appendFile as appendFile3, mkdir as mkdir3 } from "fs/promises";
37368
37462
  import { homedir as homedir5 } from "os";
37369
- import { basename as basename6, join as join51 } from "path";
37463
+ import { basename as basename7, join as join51 } from "path";
37370
37464
  function wireEventsWriter(bus, feature, runId, workdir) {
37371
37465
  const logger = getSafeLogger();
37372
- const project = basename6(workdir);
37466
+ const project = basename7(workdir);
37373
37467
  const eventsDir = join51(homedir5(), ".nax", "events", project);
37374
37468
  const eventsFile = join51(eventsDir, "events.jsonl");
37375
37469
  let dirReady = false;
@@ -37552,10 +37646,10 @@ var init_interaction2 = __esm(() => {
37552
37646
  // src/pipeline/subscribers/registry.ts
37553
37647
  import { mkdir as mkdir4, writeFile } from "fs/promises";
37554
37648
  import { homedir as homedir6 } from "os";
37555
- import { basename as basename7, join as join52 } from "path";
37649
+ import { basename as basename8, join as join52 } from "path";
37556
37650
  function wireRegistry(bus, feature, runId, workdir) {
37557
37651
  const logger = getSafeLogger();
37558
- const project = basename7(workdir);
37652
+ const project = basename8(workdir);
37559
37653
  const runDir = join52(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
37560
37654
  const metaFile = join52(runDir, "meta.json");
37561
37655
  const unsub = bus.on("run:started", (_ev) => {
@@ -38499,7 +38593,7 @@ async function executeParallelBatch(stories, projectRoot, config2, context, work
38499
38593
  executing.delete(executePromise);
38500
38594
  });
38501
38595
  executing.add(executePromise);
38502
- if (executing.size >= maxConcurrency) {
38596
+ while (executing.size >= maxConcurrency) {
38503
38597
  await Promise.race(executing);
38504
38598
  }
38505
38599
  }
@@ -39051,12 +39145,10 @@ async function runParallelBatch(options) {
39051
39145
  }
39052
39146
  const rootConfigPath = path17.join(workdir, ".nax", "config.json");
39053
39147
  const storyEffectiveConfigs = new Map;
39054
- for (const story of stories) {
39055
- if (story.workdir) {
39056
- const effectiveConfig = await loadConfigForWorkdir(rootConfigPath, story.workdir);
39057
- storyEffectiveConfigs.set(story.id, effectiveConfig);
39058
- }
39059
- }
39148
+ await Promise.all(stories.filter((story) => story.workdir).map(async (story) => {
39149
+ const effectiveConfig = await loadConfigForWorkdir(rootConfigPath, story.workdir);
39150
+ storyEffectiveConfigs.set(story.id, effectiveConfig);
39151
+ }));
39060
39152
  const workerResult = await _parallelBatchDeps.executeParallelBatch(stories, workdir, config2, pipelineContext, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs.size > 0 ? storyEffectiveConfigs : undefined);
39061
39153
  const batchEndMs = Date.now();
39062
39154
  const completed = [];
@@ -72318,7 +72410,7 @@ init_logger2();
72318
72410
 
72319
72411
  // src/prd/decompose-mapper.ts
72320
72412
  init_errors();
72321
- function mapDecomposedStoriesToUserStories(stories, parentStoryId) {
72413
+ function mapDecomposedStoriesToUserStories(stories, parentStoryId, parentWorkdir) {
72322
72414
  return stories.map((story, entryIndex) => {
72323
72415
  if (!story.id) {
72324
72416
  throw new NaxError(`Entry at index ${entryIndex} is missing required field: id`, "DECOMPOSE_VALIDATION_FAILED", {
@@ -72348,6 +72440,7 @@ function mapDecomposedStoriesToUserStories(stories, parentStoryId) {
72348
72440
  escalations: [],
72349
72441
  attempts: 0,
72350
72442
  parentStoryId,
72443
+ ...parentWorkdir !== undefined && { workdir: parentWorkdir },
72351
72444
  routing: {
72352
72445
  complexity: story.complexity,
72353
72446
  testStrategy: story.testStrategy ?? "test-after",
@@ -73032,6 +73125,15 @@ async function planDecomposeCommand(workdir, config2, options) {
73032
73125
  const timeoutSeconds = config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2;
73033
73126
  const maxAcCount = config2?.precheck?.storySizeGate?.maxAcCount ?? Number.POSITIVE_INFINITY;
73034
73127
  const maxReplanAttempts = config2?.precheck?.storySizeGate?.maxReplanAttempts ?? 3;
73128
+ let decomposeModelDef;
73129
+ try {
73130
+ const decomposeTier = config2?.plan?.model ?? "balanced";
73131
+ const { resolveModelForAgent: resolveModelForAgent2 } = await Promise.resolve().then(() => (init_schema(), exports_schema));
73132
+ if (config2?.models) {
73133
+ const defaultAgent = config2.autoMode?.defaultAgent ?? "claude";
73134
+ decomposeModelDef = resolveModelForAgent2(config2.models, agentName, decomposeTier, defaultAgent);
73135
+ }
73136
+ } catch {}
73035
73137
  if (typeof adapter.decompose !== "function") {
73036
73138
  throw new NaxError(`Agent "${agentName}" does not support decompose() required by plan --decompose`, "DECOMPOSE_NOT_SUPPORTED", { stage: "decompose", agent: agentName, storyId: options.storyId });
73037
73139
  }
@@ -73078,7 +73180,8 @@ ${repairHint}` : codebaseContext;
73078
73180
  siblings,
73079
73181
  featureName: options.feature,
73080
73182
  storyId: options.storyId,
73081
- config: config2
73183
+ config: config2,
73184
+ modelDef: decomposeModelDef
73082
73185
  });
73083
73186
  decompStories = result.stories;
73084
73187
  }
@@ -73100,7 +73203,7 @@ ${repairHint}` : codebaseContext;
73100
73203
  repairHint = `REPAIR REQUIRED (attempt ${attempt + 1}/${maxReplanAttempts}): The following sub-stories exceeded maxAcCount of ${maxAcCount}: ${violationSummary}. Split each offending story further so every sub-story has at most ${maxAcCount} acceptance criteria.`;
73101
73204
  decompStories = undefined;
73102
73205
  }
73103
- const subStoriesWithParent = mapDecomposedStoriesToUserStories(decompStories, options.storyId);
73206
+ const subStoriesWithParent = mapDecomposedStoriesToUserStories(decompStories, options.storyId, targetStory.workdir);
73104
73207
  const updatedStories = prd.userStories.map((s) => s.id === options.storyId ? { ...s, status: "decomposed" } : s);
73105
73208
  const originalIndex = updatedStories.findIndex((s) => s.id === options.storyId);
73106
73209
  const finalStories = [
@@ -83184,8 +83287,9 @@ function usePipelineEvents(events, initialStories) {
83184
83287
  totalCost: 0,
83185
83288
  elapsedMs: 0
83186
83289
  }));
83187
- const startTime = Date.now();
83290
+ const startTimeRef = import_react32.useRef(Date.now());
83188
83291
  import_react32.useEffect(() => {
83292
+ const startTime = startTimeRef.current;
83189
83293
  let timer = null;
83190
83294
  const startTimer = () => {
83191
83295
  if (!timer) {
@@ -83272,7 +83376,7 @@ function usePipelineEvents(events, initialStories) {
83272
83376
  events.off("stage:enter", onStageEnter);
83273
83377
  events.off("run:complete", onRunComplete);
83274
83378
  };
83275
- }, [events, startTime]);
83379
+ }, [events]);
83276
83380
  return state;
83277
83381
  }
83278
83382
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.58.0",
3
+ "version": "0.58.1",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {