@nathapp/nax 0.54.2 → 0.54.4

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 +281 -147
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -18786,7 +18786,7 @@ async function refineAcceptanceCriteria(criteria, context) {
18786
18786
  if (criteria.length === 0) {
18787
18787
  return [];
18788
18788
  }
18789
- const { storyId, codebaseContext, config: config2, testStrategy, testFramework } = context;
18789
+ const { storyId, featureName, workdir, codebaseContext, config: config2, testStrategy, testFramework } = context;
18790
18790
  const logger = getLogger();
18791
18791
  const modelTier = config2.acceptance?.model ?? "fast";
18792
18792
  const modelEntry = config2.models[modelTier] ?? config2.models.fast;
@@ -18801,7 +18801,11 @@ async function refineAcceptanceCriteria(criteria, context) {
18801
18801
  jsonMode: true,
18802
18802
  maxTokens: 4096,
18803
18803
  model: modelDef.model,
18804
- config: config2
18804
+ config: config2,
18805
+ featureName,
18806
+ storyId,
18807
+ workdir,
18808
+ sessionRole: "refine"
18805
18809
  });
18806
18810
  } catch (error48) {
18807
18811
  const reason = errorMessage(error48);
@@ -18940,27 +18944,58 @@ Rules:
18940
18944
  - **NEVER use placeholder assertions** \u2014 no always-passing or always-failing stubs, no TODO comments as the only content, no empty test bodies
18941
18945
  - Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
18942
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.
18943
- - Output raw code only \u2014 no markdown fences, start directly with the language's import or package declaration
18944
- - **Path anchor (CRITICAL)**: This test file lives at \`<package-root>/${acceptanceTestFilename(options.language)}\` and runs from the package root. Import from package sources using relative paths like \`./src/...\`. No deep \`../../../../\` traversal needed.`;
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.`;
18945
18949
  const prompt = basePrompt;
18946
18950
  logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
18947
18951
  const rawOutput = await (options.adapter ?? _generatorPRDDeps.adapter).complete(prompt, {
18948
18952
  model: options.modelDef.model,
18949
18953
  config: options.config,
18950
18954
  timeoutMs: options.config?.acceptance?.timeoutMs ?? 1800000,
18951
- workdir: options.workdir
18955
+ workdir: options.workdir,
18956
+ featureName: options.featureName,
18957
+ sessionRole: "acceptance-gen"
18952
18958
  });
18953
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
+ });
18954
18965
  if (!testCode) {
18955
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 });
18956
18969
  try {
18957
18970
  const existing = await Bun.file(targetPath).text();
18958
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
+ });
18959
18977
  if (recovered) {
18960
18978
  logger.info("acceptance", "Acceptance test written directly by agent \u2014 using existing file", { targetPath });
18961
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
+ });
18962
18986
  }
18963
- } 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
+ }
18964
18999
  }
18965
19000
  if (!testCode) {
18966
19001
  logger.warn("acceptance", "LLM returned non-code output for acceptance tests \u2014 falling back to skeleton", {
@@ -19061,7 +19096,9 @@ async function generateAcceptanceTests(adapter, options) {
19061
19096
  model: options.modelDef.model,
19062
19097
  config: options.config,
19063
19098
  timeoutMs: options.config?.acceptance?.timeoutMs ?? 1800000,
19064
- workdir: options.workdir
19099
+ workdir: options.workdir,
19100
+ featureName: options.featureName,
19101
+ sessionRole: "acceptance-gen"
19065
19102
  });
19066
19103
  const testCode = extractTestCode(output);
19067
19104
  if (!testCode) {
@@ -19312,7 +19349,10 @@ async function generateFixStories(adapter, options) {
19312
19349
  try {
19313
19350
  const fixDescription = await adapter.complete(prompt, {
19314
19351
  model: modelDef.model,
19315
- config: options.config
19352
+ config: options.config,
19353
+ featureName: options.prd.feature,
19354
+ workdir: options.workdir,
19355
+ sessionRole: "fix-gen"
19316
19356
  });
19317
19357
  fixStories.push({
19318
19358
  id: `US-FIX-${String(i + 1).padStart(3, "0")}`,
@@ -20101,10 +20141,11 @@ class AcpAgentAdapter {
20101
20141
  let session = null;
20102
20142
  let hadError = false;
20103
20143
  try {
20144
+ const completeSessionName = _options?.sessionName ?? buildSessionName(workdir ?? process.cwd(), _options?.featureName, _options?.storyId, _options?.sessionRole);
20104
20145
  session = await client.createSession({
20105
20146
  agentName: this.name,
20106
20147
  permissionMode,
20107
- sessionName: _options?.sessionName
20148
+ sessionName: completeSessionName
20108
20149
  });
20109
20150
  let timeoutId;
20110
20151
  const timeoutPromise = new Promise((_, reject) => {
@@ -20210,7 +20251,9 @@ class AcpAgentAdapter {
20210
20251
  output = await this.complete(prompt, {
20211
20252
  model,
20212
20253
  jsonMode: true,
20213
- config: options.config
20254
+ config: options.config,
20255
+ workdir: options.workdir,
20256
+ sessionRole: "decompose"
20214
20257
  });
20215
20258
  } catch (err) {
20216
20259
  const msg = err instanceof Error ? err.message : String(err);
@@ -22305,7 +22348,7 @@ var package_default;
22305
22348
  var init_package = __esm(() => {
22306
22349
  package_default = {
22307
22350
  name: "@nathapp/nax",
22308
- version: "0.54.2",
22351
+ version: "0.54.4",
22309
22352
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22310
22353
  type: "module",
22311
22354
  bin: {
@@ -22382,8 +22425,8 @@ var init_version = __esm(() => {
22382
22425
  NAX_VERSION = package_default.version;
22383
22426
  NAX_COMMIT = (() => {
22384
22427
  try {
22385
- if (/^[0-9a-f]{6,10}$/.test("18dd8fc"))
22386
- return "18dd8fc";
22428
+ if (/^[0-9a-f]{6,10}$/.test("d0da600"))
22429
+ return "d0da600";
22387
22430
  } catch {}
22388
22431
  try {
22389
22432
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -23101,28 +23144,38 @@ class TelegramInteractionPlugin {
23101
23144
  if (!this.botToken || !this.chatId) {
23102
23145
  throw new Error("Telegram plugin not initialized");
23103
23146
  }
23104
- const text = this.formatMessage(request);
23147
+ const header = this.buildHeader(request);
23105
23148
  const keyboard = this.buildKeyboard(request);
23149
+ const body = this.buildBody(request);
23150
+ const chunks = this.splitText(body, MAX_MESSAGE_CHARS - header.length - 10);
23106
23151
  try {
23107
- const response = await fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, {
23108
- method: "POST",
23109
- headers: { "Content-Type": "application/json" },
23110
- body: JSON.stringify({
23111
- chat_id: this.chatId,
23112
- text,
23113
- reply_markup: keyboard ? { inline_keyboard: keyboard } : undefined,
23114
- parse_mode: "Markdown"
23115
- })
23116
- });
23117
- if (!response.ok) {
23118
- const errorBody = await response.text().catch(() => "");
23119
- throw new Error(`Telegram API error (${response.status}): ${errorBody || response.statusText}`);
23120
- }
23121
- const data = await response.json();
23122
- if (!data.ok) {
23123
- throw new Error(`Telegram API returned ok=false: ${JSON.stringify(data)}`);
23152
+ const sentIds = [];
23153
+ for (let i = 0;i < chunks.length; i++) {
23154
+ const isLast = i === chunks.length - 1;
23155
+ const partLabel = chunks.length > 1 ? `[${i + 1}/${chunks.length}] ` : "";
23156
+ const text = `${header}
23157
+ ${partLabel}${chunks[i]}`;
23158
+ const response = await fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, {
23159
+ method: "POST",
23160
+ headers: { "Content-Type": "application/json" },
23161
+ body: JSON.stringify({
23162
+ chat_id: this.chatId,
23163
+ text,
23164
+ reply_markup: isLast && keyboard ? { inline_keyboard: keyboard } : undefined,
23165
+ parse_mode: "Markdown"
23166
+ })
23167
+ });
23168
+ if (!response.ok) {
23169
+ const errorBody = await response.text().catch(() => "");
23170
+ throw new Error(`Telegram API error (${response.status}): ${errorBody || response.statusText}`);
23171
+ }
23172
+ const data = await response.json();
23173
+ if (!data.ok) {
23174
+ throw new Error(`Telegram API returned ok=false: ${JSON.stringify(data)}`);
23175
+ }
23176
+ sentIds.push(data.result.message_id);
23124
23177
  }
23125
- this.pendingMessages.set(request.id, data.result.message_id);
23178
+ this.pendingMessages.set(request.id, sentIds);
23126
23179
  } catch (err) {
23127
23180
  const msg = err instanceof Error ? err.message : String(err);
23128
23181
  throw new Error(`Failed to send Telegram message: ${msg}`);
@@ -23159,10 +23212,9 @@ class TelegramInteractionPlugin {
23159
23212
  await this.sendTimeoutMessage(requestId);
23160
23213
  this.pendingMessages.delete(requestId);
23161
23214
  }
23162
- formatMessage(request) {
23215
+ buildHeader(request) {
23163
23216
  const emoji3 = this.getStageEmoji(request.stage);
23164
23217
  let text = `${emoji3} *${request.stage.toUpperCase()}*
23165
-
23166
23218
  `;
23167
23219
  text += `*Feature:* ${request.featureName}
23168
23220
  `;
@@ -23171,11 +23223,15 @@ class TelegramInteractionPlugin {
23171
23223
  `;
23172
23224
  }
23173
23225
  text += `
23174
- ${request.summary}
23226
+ `;
23227
+ return text;
23228
+ }
23229
+ buildBody(request) {
23230
+ let text = `${this.sanitizeMarkdown(request.summary)}
23175
23231
  `;
23176
23232
  if (request.detail) {
23177
23233
  text += `
23178
- ${request.detail}
23234
+ ${this.sanitizeMarkdown(request.detail)}
23179
23235
  `;
23180
23236
  }
23181
23237
  if (request.options && request.options.length > 0) {
@@ -23183,8 +23239,8 @@ ${request.detail}
23183
23239
  *Options:*
23184
23240
  `;
23185
23241
  for (const opt of request.options) {
23186
- const desc = opt.description ? ` \u2014 ${opt.description}` : "";
23187
- text += ` \u2022 ${opt.label}${desc}
23242
+ const desc = opt.description ? ` - ${this.sanitizeMarkdown(opt.description)}` : "";
23243
+ text += ` - ${opt.label}${desc}
23188
23244
  `;
23189
23245
  }
23190
23246
  }
@@ -23195,6 +23251,30 @@ ${request.detail}
23195
23251
  }
23196
23252
  return text;
23197
23253
  }
23254
+ sanitizeMarkdown(text) {
23255
+ return text.replace(/\\(?=[_*`\[])/g, "\\\\").replace(/_/g, "\\_").replace(/`/g, "\\`").replace(/\*/g, "\\*").replace(/\[/g, "\\[");
23256
+ }
23257
+ splitText(text, maxChars) {
23258
+ if (text.length <= maxChars)
23259
+ return [text];
23260
+ const chunks = [];
23261
+ let remaining = text;
23262
+ while (remaining.length > maxChars) {
23263
+ const slice = remaining.slice(0, maxChars);
23264
+ const lastNewline = slice.lastIndexOf(`
23265
+ `);
23266
+ if (lastNewline > maxChars * 0.5) {
23267
+ chunks.push(remaining.slice(0, lastNewline));
23268
+ remaining = remaining.slice(lastNewline + 1);
23269
+ } else {
23270
+ chunks.push(slice);
23271
+ remaining = remaining.slice(maxChars);
23272
+ }
23273
+ }
23274
+ if (remaining.length > 0)
23275
+ chunks.push(remaining);
23276
+ return chunks;
23277
+ }
23198
23278
  buildKeyboard(request) {
23199
23279
  switch (request.type) {
23200
23280
  case "confirm":
@@ -23302,8 +23382,11 @@ ${request.detail}
23302
23382
  };
23303
23383
  }
23304
23384
  if (update.message?.text) {
23305
- const messageId = this.pendingMessages.get(requestId);
23306
- if (!messageId)
23385
+ const messageIds = this.pendingMessages.get(requestId);
23386
+ if (!messageIds)
23387
+ return null;
23388
+ const replyToId = update.message.reply_to_message?.message_id;
23389
+ if (replyToId !== undefined && !messageIds.includes(replyToId))
23307
23390
  return null;
23308
23391
  return {
23309
23392
  requestId,
@@ -23329,20 +23412,20 @@ ${request.detail}
23329
23412
  } catch {}
23330
23413
  }
23331
23414
  async sendTimeoutMessage(requestId) {
23332
- const messageId = this.pendingMessages.get(requestId);
23333
- if (!messageId || !this.botToken || !this.chatId) {
23415
+ const messageIds = this.pendingMessages.get(requestId);
23416
+ if (!messageIds || !this.botToken || !this.chatId) {
23334
23417
  this.pendingMessages.delete(requestId);
23335
23418
  return;
23336
23419
  }
23420
+ const lastId = messageIds[messageIds.length - 1];
23337
23421
  try {
23338
23422
  await fetch(`https://api.telegram.org/bot${this.botToken}/editMessageText`, {
23339
23423
  method: "POST",
23340
23424
  headers: { "Content-Type": "application/json" },
23341
23425
  body: JSON.stringify({
23342
23426
  chat_id: this.chatId,
23343
- message_id: messageId,
23344
- text: "\u23F1 *EXPIRED* \u2014 Interaction timed out",
23345
- parse_mode: "Markdown"
23427
+ message_id: lastId,
23428
+ text: "\u23F1 EXPIRED \u2014 Interaction timed out"
23346
23429
  })
23347
23430
  });
23348
23431
  } catch {} finally {
@@ -23350,7 +23433,7 @@ ${request.detail}
23350
23433
  }
23351
23434
  }
23352
23435
  }
23353
- var TelegramConfigSchema;
23436
+ var MAX_MESSAGE_CHARS = 4000, TelegramConfigSchema;
23354
23437
  var init_telegram = __esm(() => {
23355
23438
  init_zod();
23356
23439
  TelegramConfigSchema = exports_external.object({
@@ -23627,7 +23710,10 @@ class AutoInteractionPlugin {
23627
23710
  const output = await adapter.complete(prompt, {
23628
23711
  ...modelArg && { model: modelArg },
23629
23712
  jsonMode: true,
23630
- ...this.config.naxConfig && { config: this.config.naxConfig }
23713
+ ...this.config.naxConfig && { config: this.config.naxConfig },
23714
+ featureName: request.featureName,
23715
+ storyId: request.storyId,
23716
+ sessionRole: "auto"
23631
23717
  });
23632
23718
  return this.parseResponse(output);
23633
23719
  }
@@ -24368,31 +24454,33 @@ ${stderr}` };
24368
24454
  }
24369
24455
  let totalCriteria = 0;
24370
24456
  let testableCount = 0;
24371
- const existsResults = await Promise.all(testPaths.map(({ testPath }) => _acceptanceSetupDeps.fileExists(testPath)));
24372
- const anyFileMissing = existsResults.some((exists) => !exists);
24373
- let shouldGenerate = anyFileMissing;
24374
- if (!anyFileMissing) {
24375
- const fingerprint = computeACFingerprint(allCriteria);
24376
- const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
24377
- getSafeLogger()?.debug("acceptance-setup", "Fingerprint check", {
24378
- currentFingerprint: fingerprint,
24379
- storedFingerprint: meta3?.acFingerprint ?? "none",
24380
- match: meta3?.acFingerprint === fingerprint
24381
- });
24382
- if (!meta3 || meta3.acFingerprint !== fingerprint) {
24457
+ const fingerprint = computeACFingerprint(allCriteria);
24458
+ const meta3 = await _acceptanceSetupDeps.readMeta(metaPath);
24459
+ getSafeLogger()?.debug("acceptance-setup", "Fingerprint check", {
24460
+ currentFingerprint: fingerprint,
24461
+ storedFingerprint: meta3?.acFingerprint ?? "none",
24462
+ match: meta3?.acFingerprint === fingerprint
24463
+ });
24464
+ let shouldGenerate = false;
24465
+ if (!meta3 || meta3.acFingerprint !== fingerprint) {
24466
+ if (!meta3) {
24467
+ getSafeLogger()?.info("acceptance-setup", "No acceptance meta \u2014 generating acceptance tests");
24468
+ } else {
24383
24469
  getSafeLogger()?.info("acceptance-setup", "ACs changed \u2014 regenerating acceptance tests", {
24384
- reason: !meta3 ? "no meta file" : "fingerprint mismatch"
24470
+ reason: "fingerprint mismatch",
24471
+ currentFingerprint: fingerprint,
24472
+ storedFingerprint: meta3.acFingerprint
24385
24473
  });
24386
- for (const { testPath } of testPaths) {
24387
- if (await _acceptanceSetupDeps.fileExists(testPath)) {
24388
- await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
24389
- await _acceptanceSetupDeps.deleteFile(testPath);
24390
- }
24474
+ }
24475
+ for (const { testPath } of testPaths) {
24476
+ if (await _acceptanceSetupDeps.fileExists(testPath)) {
24477
+ await _acceptanceSetupDeps.copyFile(testPath, `${testPath}.bak`);
24478
+ await _acceptanceSetupDeps.deleteFile(testPath);
24391
24479
  }
24392
- shouldGenerate = true;
24393
- } else {
24394
- getSafeLogger()?.info("acceptance-setup", "Reusing existing acceptance tests (fingerprint match)");
24395
24480
  }
24481
+ shouldGenerate = true;
24482
+ } else {
24483
+ getSafeLogger()?.info("acceptance-setup", "Reusing existing acceptance tests (fingerprint match)");
24396
24484
  }
24397
24485
  if (shouldGenerate) {
24398
24486
  totalCriteria = allCriteria.length;
@@ -24404,6 +24492,8 @@ ${stderr}` };
24404
24492
  for (const story of nonFixStories) {
24405
24493
  const storyRefined = await _acceptanceSetupDeps.refine(story.acceptanceCriteria, {
24406
24494
  storyId: story.id,
24495
+ featureName: ctx.prd.feature,
24496
+ workdir: ctx.workdir,
24407
24497
  codebaseContext: "",
24408
24498
  config: ctx.config,
24409
24499
  testStrategy: ctx.config.acceptance.testStrategy,
@@ -24439,10 +24529,10 @@ ${stderr}` };
24439
24529
  });
24440
24530
  await _acceptanceSetupDeps.writeFile(testPath, result.testCode);
24441
24531
  }
24442
- const fingerprint = computeACFingerprint(allCriteria);
24532
+ const fingerprint2 = computeACFingerprint(allCriteria);
24443
24533
  await _acceptanceSetupDeps.writeMeta(metaPath, {
24444
24534
  generatedAt: new Date().toISOString(),
24445
- acFingerprint: fingerprint,
24535
+ acFingerprint: fingerprint2,
24446
24536
  storyCount: ctx.prd.userStories.length,
24447
24537
  acCount: totalCriteria,
24448
24538
  generator: "nax"
@@ -24907,6 +24997,16 @@ async function runSemanticReview(workdir, storyGitRef, story, semanticConfig, mo
24907
24997
  storyId: story.id,
24908
24998
  durationMs: durationMs2
24909
24999
  });
25000
+ logger?.debug("review", "Semantic review findings", {
25001
+ storyId: story.id,
25002
+ findings: parsed.findings.map((f) => ({
25003
+ severity: f.severity,
25004
+ file: f.file,
25005
+ line: f.line,
25006
+ issue: f.issue,
25007
+ suggestion: f.suggestion
25008
+ }))
25009
+ });
24910
25010
  const output = `Semantic review failed:
24911
25011
 
24912
25012
  ${formatFindings(parsed.findings)}`;
@@ -32828,36 +32928,32 @@ import { appendFileSync as appendFileSync2 } from "fs";
32828
32928
  function startHeartbeat(statusWriter, getTotalCost, getIterations, jsonlFilePath) {
32829
32929
  const logger = getSafeLogger();
32830
32930
  stopHeartbeat();
32831
- heartbeatTimer = setInterval(async () => {
32832
- logger?.debug("crash-recovery", "Heartbeat");
32833
- if (jsonlFilePath) {
32931
+ heartbeatTimer = setInterval(() => {
32932
+ (async () => {
32834
32933
  try {
32835
- const heartbeatEntry = {
32836
- timestamp: new Date().toISOString(),
32837
- level: "debug",
32838
- stage: "heartbeat",
32839
- message: "Process alive",
32840
- data: {
32841
- pid: process.pid,
32842
- memoryUsageMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024)
32843
- }
32844
- };
32845
- const line = `${JSON.stringify(heartbeatEntry)}
32934
+ logger?.debug("crash-recovery", "Heartbeat");
32935
+ if (jsonlFilePath) {
32936
+ const heartbeatEntry = {
32937
+ timestamp: new Date().toISOString(),
32938
+ level: "debug",
32939
+ stage: "heartbeat",
32940
+ message: "Process alive",
32941
+ data: {
32942
+ pid: process.pid,
32943
+ memoryUsageMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024)
32944
+ }
32945
+ };
32946
+ const line = `${JSON.stringify(heartbeatEntry)}
32846
32947
  `;
32847
- appendFileSync2(jsonlFilePath, line);
32948
+ appendFileSync2(jsonlFilePath, line);
32949
+ }
32950
+ await statusWriter.update(getTotalCost(), getIterations(), {
32951
+ lastHeartbeat: new Date().toISOString()
32952
+ });
32848
32953
  } catch (err) {
32849
- logger?.warn("crash-recovery", "Failed to write heartbeat", { error: err.message });
32954
+ logger?.warn("crash-recovery", "Failed during heartbeat", { error: err.message });
32850
32955
  }
32851
- }
32852
- try {
32853
- await statusWriter.update(getTotalCost(), getIterations(), {
32854
- lastHeartbeat: new Date().toISOString()
32855
- });
32856
- } catch (err) {
32857
- logger?.warn("crash-recovery", "Failed to update status during heartbeat", {
32858
- error: err.message
32859
- });
32860
- }
32956
+ })().catch(() => {});
32861
32957
  }, 60000);
32862
32958
  logger?.debug("crash-recovery", "Heartbeat started (60s interval)");
32863
32959
  }
@@ -33016,6 +33112,10 @@ function createSignalHandler(ctx) {
33016
33112
  }
33017
33113
  function createUncaughtExceptionHandler(ctx) {
33018
33114
  return async (error48) => {
33115
+ process.stderr.write(`
33116
+ [nax crash] Uncaught exception: ${error48.message}
33117
+ ${error48.stack ?? ""}
33118
+ `);
33019
33119
  const logger = getSafeLogger();
33020
33120
  logger?.error("crash-recovery", "Uncaught exception", {
33021
33121
  error: error48.message,
@@ -33036,6 +33136,10 @@ function createUncaughtExceptionHandler(ctx) {
33036
33136
  function createUnhandledRejectionHandler(ctx) {
33037
33137
  return async (reason) => {
33038
33138
  const error48 = reason instanceof Error ? reason : new Error(String(reason));
33139
+ process.stderr.write(`
33140
+ [nax crash] Unhandled rejection: ${error48.message}
33141
+ ${error48.stack ?? ""}
33142
+ `);
33039
33143
  const logger = getSafeLogger();
33040
33144
  logger?.error("crash-recovery", "Unhandled promise rejection", {
33041
33145
  error: error48.message,
@@ -34417,7 +34521,7 @@ __export(exports_parallel_executor_rectify, {
34417
34521
  });
34418
34522
  import path15 from "path";
34419
34523
  async function rectifyConflictedStory(options) {
34420
- const { storyId, workdir, config: config2, hooks, pluginRegistry, prd, eventEmitter } = options;
34524
+ const { storyId, workdir, config: config2, hooks, pluginRegistry, prd, eventEmitter, agentGetFn } = options;
34421
34525
  const logger = getSafeLogger();
34422
34526
  logger?.info("parallel", "Rectifying story on updated base", { storyId, attempt: "rectification" });
34423
34527
  try {
@@ -34449,7 +34553,8 @@ async function rectifyConflictedStory(options) {
34449
34553
  hooks,
34450
34554
  plugins: pluginRegistry,
34451
34555
  storyStartTime: new Date().toISOString(),
34452
- routing
34556
+ routing,
34557
+ agentGetFn
34453
34558
  };
34454
34559
  const pipelineResult = await runPipeline2(defaultPipeline2, pipelineContext, eventEmitter);
34455
34560
  const cost = pipelineResult.context.agentResult?.estimatedCost ?? 0;
@@ -34485,7 +34590,7 @@ var init_parallel_executor_rectify = __esm(() => {
34485
34590
  // src/execution/parallel-executor-rectification-pass.ts
34486
34591
  async function runRectificationPass(conflictedStories, options, prd, rectifyConflictedStory2) {
34487
34592
  const logger = getSafeLogger();
34488
- const { workdir, config: config2, hooks, pluginRegistry, eventEmitter } = options;
34593
+ const { workdir, config: config2, hooks, pluginRegistry, eventEmitter, agentGetFn } = options;
34489
34594
  const rectify = rectifyConflictedStory2 || (async (opts) => {
34490
34595
  const { rectifyConflictedStory: importedRectify } = await Promise.resolve().then(() => (init_parallel_executor_rectify(), exports_parallel_executor_rectify));
34491
34596
  return importedRectify(opts);
@@ -34506,7 +34611,8 @@ async function runRectificationPass(conflictedStories, options, prd, rectifyConf
34506
34611
  hooks,
34507
34612
  pluginRegistry,
34508
34613
  prd,
34509
- eventEmitter
34614
+ eventEmitter,
34615
+ agentGetFn
34510
34616
  });
34511
34617
  additionalCost += result.cost;
34512
34618
  if (result.success) {
@@ -34829,7 +34935,7 @@ function wireEventsWriter(bus, feature, runId, workdir) {
34829
34935
  const eventsFile = join49(eventsDir, "events.jsonl");
34830
34936
  let dirReady = false;
34831
34937
  const write = (line) => {
34832
- (async () => {
34938
+ return (async () => {
34833
34939
  try {
34834
34940
  if (!dirReady) {
34835
34941
  await mkdir2(eventsDir, { recursive: true });
@@ -34847,16 +34953,30 @@ function wireEventsWriter(bus, feature, runId, workdir) {
34847
34953
  };
34848
34954
  const unsubs = [];
34849
34955
  unsubs.push(bus.on("run:started", (_ev) => {
34850
- write({ ts: new Date().toISOString(), event: "run:started", runId, feature, project });
34956
+ return write({ ts: new Date().toISOString(), event: "run:started", runId, feature, project });
34851
34957
  }));
34852
34958
  unsubs.push(bus.on("story:started", (ev) => {
34853
- write({ ts: new Date().toISOString(), event: "story:started", runId, feature, project, storyId: ev.storyId });
34959
+ return write({
34960
+ ts: new Date().toISOString(),
34961
+ event: "story:started",
34962
+ runId,
34963
+ feature,
34964
+ project,
34965
+ storyId: ev.storyId
34966
+ });
34854
34967
  }));
34855
34968
  unsubs.push(bus.on("story:completed", (ev) => {
34856
- write({ ts: new Date().toISOString(), event: "story:completed", runId, feature, project, storyId: ev.storyId });
34969
+ return write({
34970
+ ts: new Date().toISOString(),
34971
+ event: "story:completed",
34972
+ runId,
34973
+ feature,
34974
+ project,
34975
+ storyId: ev.storyId
34976
+ });
34857
34977
  }));
34858
34978
  unsubs.push(bus.on("story:decomposed", (ev) => {
34859
- write({
34979
+ return write({
34860
34980
  ts: new Date().toISOString(),
34861
34981
  event: "story:decomposed",
34862
34982
  runId,
@@ -34867,13 +34987,20 @@ function wireEventsWriter(bus, feature, runId, workdir) {
34867
34987
  });
34868
34988
  }));
34869
34989
  unsubs.push(bus.on("story:failed", (ev) => {
34870
- write({ ts: new Date().toISOString(), event: "story:failed", runId, feature, project, storyId: ev.storyId });
34990
+ return write({
34991
+ ts: new Date().toISOString(),
34992
+ event: "story:failed",
34993
+ runId,
34994
+ feature,
34995
+ project,
34996
+ storyId: ev.storyId
34997
+ });
34871
34998
  }));
34872
34999
  unsubs.push(bus.on("run:completed", (_ev) => {
34873
- write({ ts: new Date().toISOString(), event: "on-complete", runId, feature, project });
35000
+ return write({ ts: new Date().toISOString(), event: "on-complete", runId, feature, project });
34874
35001
  }));
34875
35002
  unsubs.push(bus.on("run:paused", (ev) => {
34876
- write({
35003
+ return write({
34877
35004
  ts: new Date().toISOString(),
34878
35005
  event: "run:paused",
34879
35006
  runId,
@@ -34895,44 +35022,44 @@ var init_events_writer = __esm(() => {
34895
35022
  function wireHooks(bus, hooks, workdir, feature) {
34896
35023
  const logger = getSafeLogger();
34897
35024
  const safe = (name, fn) => {
34898
- fn().catch((err) => logger?.warn("hooks-subscriber", `Hook "${name}" failed`, { error: String(err) }));
35025
+ return fn().catch((err) => logger?.warn("hooks-subscriber", `Hook "${name}" failed`, { error: String(err) })).catch(() => {});
34899
35026
  };
34900
35027
  const unsubs = [];
34901
35028
  unsubs.push(bus.on("run:started", (ev) => {
34902
- safe("on-start", () => fireHook(hooks, "on-start", hookCtx(feature, { status: "running" }), workdir));
35029
+ return safe("on-start", () => fireHook(hooks, "on-start", hookCtx(feature, { status: "running" }), workdir));
34903
35030
  }));
34904
35031
  unsubs.push(bus.on("story:started", (ev) => {
34905
- safe("on-story-start", () => fireHook(hooks, "on-story-start", hookCtx(feature, { storyId: ev.storyId, model: ev.modelTier, agent: ev.agent }), workdir));
35032
+ return safe("on-story-start", () => fireHook(hooks, "on-story-start", hookCtx(feature, { storyId: ev.storyId, model: ev.modelTier, agent: ev.agent }), workdir));
34906
35033
  }));
34907
35034
  unsubs.push(bus.on("story:completed", (ev) => {
34908
- safe("on-story-complete", () => fireHook(hooks, "on-story-complete", hookCtx(feature, { storyId: ev.storyId, status: "passed", cost: ev.cost }), workdir));
35035
+ return safe("on-story-complete", () => fireHook(hooks, "on-story-complete", hookCtx(feature, { storyId: ev.storyId, status: "passed", cost: ev.cost }), workdir));
34909
35036
  }));
34910
35037
  unsubs.push(bus.on("story:decomposed", (ev) => {
34911
- safe("on-story-complete (decomposed)", () => fireHook(hooks, "on-story-complete", hookCtx(feature, { storyId: ev.storyId, status: "decomposed", subStoryCount: ev.subStoryCount }), workdir));
35038
+ return safe("on-story-complete (decomposed)", () => fireHook(hooks, "on-story-complete", hookCtx(feature, { storyId: ev.storyId, status: "decomposed", subStoryCount: ev.subStoryCount }), workdir));
34912
35039
  }));
34913
35040
  unsubs.push(bus.on("story:failed", (ev) => {
34914
- safe("on-story-fail", () => fireHook(hooks, "on-story-fail", hookCtx(feature, { storyId: ev.storyId, status: "failed", reason: ev.reason }), workdir));
35041
+ return safe("on-story-fail", () => fireHook(hooks, "on-story-fail", hookCtx(feature, { storyId: ev.storyId, status: "failed", reason: ev.reason }), workdir));
34915
35042
  }));
34916
35043
  unsubs.push(bus.on("story:paused", (ev) => {
34917
- safe("on-pause (story)", () => fireHook(hooks, "on-pause", hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }), workdir));
35044
+ return safe("on-pause (story)", () => fireHook(hooks, "on-pause", hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }), workdir));
34918
35045
  }));
34919
35046
  unsubs.push(bus.on("run:paused", (ev) => {
34920
- safe("on-pause (run)", () => fireHook(hooks, "on-pause", hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }), workdir));
35047
+ return safe("on-pause (run)", () => fireHook(hooks, "on-pause", hookCtx(feature, { storyId: ev.storyId, reason: ev.reason, cost: ev.cost }), workdir));
34921
35048
  }));
34922
35049
  unsubs.push(bus.on("run:completed", (ev) => {
34923
- safe("on-complete", () => fireHook(hooks, "on-complete", hookCtx(feature, { status: "complete", cost: ev.totalCost ?? 0 }), workdir));
35050
+ return safe("on-complete", () => fireHook(hooks, "on-complete", hookCtx(feature, { status: "complete", cost: ev.totalCost ?? 0 }), workdir));
34924
35051
  }));
34925
35052
  unsubs.push(bus.on("run:resumed", (ev) => {
34926
- safe("on-resume", () => fireHook(hooks, "on-resume", hookCtx(feature, { status: "running" }), workdir));
35053
+ return safe("on-resume", () => fireHook(hooks, "on-resume", hookCtx(feature, { status: "running" }), workdir));
34927
35054
  }));
34928
35055
  unsubs.push(bus.on("story:completed", (ev) => {
34929
- safe("on-session-end (completed)", () => fireHook(hooks, "on-session-end", hookCtx(feature, { storyId: ev.storyId, status: "passed" }), workdir));
35056
+ return safe("on-session-end (completed)", () => fireHook(hooks, "on-session-end", hookCtx(feature, { storyId: ev.storyId, status: "passed" }), workdir));
34930
35057
  }));
34931
35058
  unsubs.push(bus.on("story:failed", (ev) => {
34932
- safe("on-session-end (failed)", () => fireHook(hooks, "on-session-end", hookCtx(feature, { storyId: ev.storyId, status: "failed" }), workdir));
35059
+ return safe("on-session-end (failed)", () => fireHook(hooks, "on-session-end", hookCtx(feature, { storyId: ev.storyId, status: "failed" }), workdir));
34933
35060
  }));
34934
35061
  unsubs.push(bus.on("run:errored", (ev) => {
34935
- safe("on-error", () => fireHook(hooks, "on-error", hookCtx(feature, { reason: ev.reason }), workdir));
35062
+ return safe("on-error", () => fireHook(hooks, "on-error", hookCtx(feature, { reason: ev.reason }), workdir));
34936
35063
  }));
34937
35064
  return () => {
34938
35065
  for (const u of unsubs)
@@ -35007,7 +35134,7 @@ function wireRegistry(bus, feature, runId, workdir) {
35007
35134
  const runDir = join50(homedir6(), ".nax", "runs", `${project}-${feature}-${runId}`);
35008
35135
  const metaFile = join50(runDir, "meta.json");
35009
35136
  const unsub = bus.on("run:started", (_ev) => {
35010
- (async () => {
35137
+ return (async () => {
35011
35138
  try {
35012
35139
  await mkdir3(runDir, { recursive: true });
35013
35140
  const meta3 = {
@@ -35038,11 +35165,11 @@ var init_registry3 = __esm(() => {
35038
35165
  function wireReporters(bus, pluginRegistry, runId, startTime) {
35039
35166
  const logger = getSafeLogger();
35040
35167
  const safe = (name, fn) => {
35041
- fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) }));
35168
+ return fn().catch((err) => logger?.warn("reporters-subscriber", `Reporter "${name}" error`, { error: String(err) })).catch(() => {});
35042
35169
  };
35043
35170
  const unsubs = [];
35044
35171
  unsubs.push(bus.on("run:started", (ev) => {
35045
- safe("onRunStart", async () => {
35172
+ return safe("onRunStart", async () => {
35046
35173
  const reporters = pluginRegistry.getReporters();
35047
35174
  for (const r of reporters) {
35048
35175
  if (r.onRunStart) {
@@ -35061,7 +35188,7 @@ function wireReporters(bus, pluginRegistry, runId, startTime) {
35061
35188
  });
35062
35189
  }));
35063
35190
  unsubs.push(bus.on("story:completed", (ev) => {
35064
- safe("onStoryComplete(completed)", async () => {
35191
+ return safe("onStoryComplete(completed)", async () => {
35065
35192
  const reporters = pluginRegistry.getReporters();
35066
35193
  for (const r of reporters) {
35067
35194
  if (r.onStoryComplete) {
@@ -35083,7 +35210,7 @@ function wireReporters(bus, pluginRegistry, runId, startTime) {
35083
35210
  });
35084
35211
  }));
35085
35212
  unsubs.push(bus.on("story:failed", (ev) => {
35086
- safe("onStoryComplete(failed)", async () => {
35213
+ return safe("onStoryComplete(failed)", async () => {
35087
35214
  const reporters = pluginRegistry.getReporters();
35088
35215
  for (const r of reporters) {
35089
35216
  if (r.onStoryComplete) {
@@ -35105,7 +35232,7 @@ function wireReporters(bus, pluginRegistry, runId, startTime) {
35105
35232
  });
35106
35233
  }));
35107
35234
  unsubs.push(bus.on("story:paused", (ev) => {
35108
- safe("onStoryComplete(paused)", async () => {
35235
+ return safe("onStoryComplete(paused)", async () => {
35109
35236
  const reporters = pluginRegistry.getReporters();
35110
35237
  for (const r of reporters) {
35111
35238
  if (r.onStoryComplete) {
@@ -35127,7 +35254,7 @@ function wireReporters(bus, pluginRegistry, runId, startTime) {
35127
35254
  });
35128
35255
  }));
35129
35256
  unsubs.push(bus.on("run:completed", (ev) => {
35130
- safe("onRunEnd", async () => {
35257
+ return safe("onRunEnd", async () => {
35131
35258
  const reporters = pluginRegistry.getReporters();
35132
35259
  for (const r of reporters) {
35133
35260
  if (r.onRunEnd) {
@@ -68822,11 +68949,19 @@ function validateStory(raw, index, allIds) {
68822
68949
  ...contextFiles.length > 0 ? { contextFiles } : {}
68823
68950
  };
68824
68951
  }
68952
+ function sanitizeInvalidEscapes(text) {
68953
+ let result = text.replace(/\\x([0-9a-fA-F]{1,2})/g, (_, hex3) => `\\u00${hex3.padStart(2, "0")}`);
68954
+ result = result.replace(/\\u([0-9a-fA-F]{1,3})(?![0-9a-fA-F])/g, (_, digits) => `\\u${digits.padStart(4, "0")}`);
68955
+ result = result.replace(/\\u(?![0-9a-fA-F])/g, "\\");
68956
+ result = result.replace(/\\([^"\\\/bfnrtu])/g, "$1");
68957
+ return result;
68958
+ }
68825
68959
  function parseRawString(text) {
68826
68960
  const extracted = extractJsonFromMarkdown(text);
68827
68961
  const cleaned = stripTrailingCommas(extracted);
68962
+ const sanitized = sanitizeInvalidEscapes(cleaned);
68828
68963
  try {
68829
- return JSON.parse(cleaned);
68964
+ return JSON.parse(sanitized);
68830
68965
  } catch (err) {
68831
68966
  const parseErr = err;
68832
68967
  throw new Error(`[schema] Failed to parse JSON: ${parseErr.message}`, { cause: parseErr });
@@ -68924,7 +69059,14 @@ async function planCommand(workdir, config2, options) {
68924
69059
  if (entry)
68925
69060
  autoModel = resolveModel2(entry).model;
68926
69061
  } catch {}
68927
- rawResponse = await cliAdapter.complete(prompt, { model: autoModel, jsonMode: true, workdir, config: config2 });
69062
+ rawResponse = await cliAdapter.complete(prompt, {
69063
+ model: autoModel,
69064
+ jsonMode: true,
69065
+ workdir,
69066
+ config: config2,
69067
+ featureName: options.feature,
69068
+ sessionRole: "plan"
69069
+ });
68928
69070
  try {
68929
69071
  const envelope = JSON.parse(rawResponse);
68930
69072
  if (envelope?.type === "result" && typeof envelope?.result === "string") {
@@ -71349,7 +71491,7 @@ Runs:
71349
71491
  const summary = await extractRunSummary(filePath);
71350
71492
  const timestamp = file2.replace(".jsonl", "");
71351
71493
  const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
71352
- const duration3 = summary ? formatDuration2(summary.durationMs) : "?";
71494
+ const duration3 = summary ? formatDuration(summary.durationMs) : "?";
71353
71495
  const cost = summary ? `$${summary.totalCost.toFixed(4)}` : "$?.????";
71354
71496
  const status = summary ? summary.failed === 0 ? source_default.green("\u2713") : source_default.red("\u2717") : "?";
71355
71497
  console.log(` ${timestamp} ${stories.padEnd(7)} ${duration3.padEnd(8)} ${cost.padEnd(8)} ${status}`);
@@ -71443,17 +71585,6 @@ function shouldDisplayEntry(entry, options) {
71443
71585
  }
71444
71586
  return true;
71445
71587
  }
71446
- function formatDuration2(ms) {
71447
- if (ms < 1000) {
71448
- return `${ms}ms`;
71449
- }
71450
- if (ms < 60000) {
71451
- return `${(ms / 1000).toFixed(1)}s`;
71452
- }
71453
- const minutes = Math.floor(ms / 60000);
71454
- const seconds = Math.floor(ms % 60000 / 1000);
71455
- return `${minutes}m${seconds}s`;
71456
- }
71457
71588
 
71458
71589
  // src/commands/logs.ts
71459
71590
  async function logsCommand(options) {
@@ -71555,7 +71686,7 @@ var DEFAULT_LIMIT = 20;
71555
71686
  var _runsCmdDeps = {
71556
71687
  getRunsDir
71557
71688
  };
71558
- function formatDuration3(ms) {
71689
+ function formatDuration2(ms) {
71559
71690
  if (ms <= 0)
71560
71691
  return "-";
71561
71692
  const minutes = Math.floor(ms / 60000);
@@ -71668,7 +71799,7 @@ async function runsCommand(options = {}) {
71668
71799
  pad3(row.feature, COL.feature),
71669
71800
  pad3(colored, COL.status + (colored.length - visibleLength(colored))),
71670
71801
  pad3(`${row.passed}/${row.total}`, COL.stories),
71671
- pad3(formatDuration3(row.durationMs), COL.duration),
71802
+ pad3(formatDuration2(row.durationMs), COL.duration),
71672
71803
  formatDate(row.registeredAt)
71673
71804
  ].join(" ");
71674
71805
  console.log(line);
@@ -71955,7 +72086,8 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
71955
72086
  allStoryMetrics,
71956
72087
  pluginRegistry,
71957
72088
  formatterMode: options.formatterMode,
71958
- headless: options.headless
72089
+ headless: options.headless,
72090
+ agentGetFn: options.agentGetFn
71959
72091
  }, prd);
71960
72092
  prd = parallelResult.prd;
71961
72093
  totalCost = parallelResult.totalCost;
@@ -71991,7 +72123,8 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
71991
72123
  startTime: options.startTime,
71992
72124
  batchPlan,
71993
72125
  agentGetFn: options.agentGetFn,
71994
- pidRegistry: options.pidRegistry
72126
+ pidRegistry: options.pidRegistry,
72127
+ interactionChain: options.interactionChain
71995
72128
  }, prd);
71996
72129
  prd = sequentialResult.prd;
71997
72130
  iterations = sequentialResult.iterations;
@@ -72117,7 +72250,8 @@ async function run(options) {
72117
72250
  parallel,
72118
72251
  runParallelExecution: _runnerDeps.runParallelExecution ?? undefined,
72119
72252
  agentGetFn,
72120
- pidRegistry
72253
+ pidRegistry,
72254
+ interactionChain
72121
72255
  }, prd, pluginRegistry);
72122
72256
  prd = executionResult.prd;
72123
72257
  iterations = executionResult.iterations;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.54.2",
3
+ "version": "0.54.4",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {