@nathapp/nax 0.54.12 → 0.55.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 +386 -222
  2. package/package.json +1 -1
package/dist/nax.js CHANGED
@@ -3259,6 +3259,14 @@ async function withProcessTimeout(proc, timeoutMs, opts) {
3259
3259
  }
3260
3260
 
3261
3261
  // src/utils/bun-deps.ts
3262
+ var exports_bun_deps = {};
3263
+ __export(exports_bun_deps, {
3264
+ which: () => which,
3265
+ typedSpawn: () => typedSpawn,
3266
+ spawn: () => spawn,
3267
+ sleep: () => sleep,
3268
+ file: () => file
3269
+ });
3262
3270
  function typedSpawn(cmd, opts) {
3263
3271
  return Bun.spawn(cmd, opts);
3264
3272
  }
@@ -3268,6 +3276,9 @@ function which(name) {
3268
3276
  function sleep(ms) {
3269
3277
  return Bun.sleep(ms);
3270
3278
  }
3279
+ function file(path) {
3280
+ return Bun.file(path);
3281
+ }
3271
3282
  var spawn;
3272
3283
  var init_bun_deps = __esm(() => {
3273
3284
  spawn = Bun.spawn;
@@ -14942,26 +14953,26 @@ var formatMap, stringProcessor = (schema, ctx, _json, _params) => {
14942
14953
  _json.pattern = pattern.source;
14943
14954
  }, fileProcessor = (schema, _ctx, json, _params) => {
14944
14955
  const _json = json;
14945
- const file = {
14956
+ const file2 = {
14946
14957
  type: "string",
14947
14958
  format: "binary",
14948
14959
  contentEncoding: "binary"
14949
14960
  };
14950
14961
  const { minimum, maximum, mime } = schema._zod.bag;
14951
14962
  if (minimum !== undefined)
14952
- file.minLength = minimum;
14963
+ file2.minLength = minimum;
14953
14964
  if (maximum !== undefined)
14954
- file.maxLength = maximum;
14965
+ file2.maxLength = maximum;
14955
14966
  if (mime) {
14956
14967
  if (mime.length === 1) {
14957
- file.contentMediaType = mime[0];
14958
- Object.assign(_json, file);
14968
+ file2.contentMediaType = mime[0];
14969
+ Object.assign(_json, file2);
14959
14970
  } else {
14960
- Object.assign(_json, file);
14971
+ Object.assign(_json, file2);
14961
14972
  _json.anyOf = mime.map((m) => ({ contentMediaType: m }));
14962
14973
  }
14963
14974
  } else {
14964
- Object.assign(_json, file);
14975
+ Object.assign(_json, file2);
14965
14976
  }
14966
14977
  }, successProcessor = (_schema, _ctx, json, _params) => {
14967
14978
  json.type = "boolean";
@@ -15833,7 +15844,7 @@ __export(exports_schemas2, {
15833
15844
  function: () => _function,
15834
15845
  float64: () => float64,
15835
15846
  float32: () => float32,
15836
- file: () => file,
15847
+ file: () => file2,
15837
15848
  exactOptional: () => exactOptional,
15838
15849
  enum: () => _enum2,
15839
15850
  emoji: () => emoji2,
@@ -16214,7 +16225,7 @@ function literal(value, params) {
16214
16225
  ...exports_util.normalizeParams(params)
16215
16226
  });
16216
16227
  }
16217
- function file(params) {
16228
+ function file2(params) {
16218
16229
  return _file(ZodFile, params);
16219
16230
  }
16220
16231
  function transform(fn) {
@@ -17662,7 +17673,7 @@ __export(exports_external, {
17662
17673
  float64: () => float64,
17663
17674
  float32: () => float32,
17664
17675
  flattenError: () => flattenError,
17665
- file: () => file,
17676
+ file: () => file2,
17666
17677
  exactOptional: () => exactOptional,
17667
17678
  enum: () => _enum2,
17668
17679
  endsWith: () => _endsWith,
@@ -19586,7 +19597,8 @@ class SpawnAcpSession {
19586
19597
  const stderr = await new Response(proc.stderr).text();
19587
19598
  if (exitCode !== 0) {
19588
19599
  getSafeLogger()?.warn("acp-adapter", `Session prompt exited with code ${exitCode}`, {
19589
- stderr: stderr.slice(0, 200)
19600
+ exitCode,
19601
+ stderr: stderr.slice(0, 500)
19590
19602
  });
19591
19603
  return {
19592
19604
  messages: [{ role: "assistant", content: stderr || `Exit code ${exitCode}` }],
@@ -19912,10 +19924,10 @@ async function sweepFeatureSessions(workdir, featureName) {
19912
19924
  }
19913
19925
  async function sweepStaleFeatureSessions(workdir, featureName, maxAgeMs = MAX_SESSION_AGE_MS) {
19914
19926
  const path = acpSessionsPath(workdir, featureName);
19915
- const file2 = Bun.file(path);
19916
- if (!await file2.exists())
19927
+ const file3 = Bun.file(path);
19928
+ if (!await file3.exists())
19917
19929
  return;
19918
- const ageMs = Date.now() - file2.lastModified;
19930
+ const ageMs = Date.now() - file3.lastModified;
19919
19931
  if (ageMs < maxAgeMs)
19920
19932
  return;
19921
19933
  getSafeLogger()?.info("acp-adapter", `[sweep] Sidecar is ${Math.round(ageMs / 60000)}m old \u2014 sweeping stale sessions`, {
@@ -19994,11 +20006,23 @@ class AcpAgentAdapter {
19994
20006
  storyId: options.storyId,
19995
20007
  sessionRole: options.sessionRole
19996
20008
  });
20009
+ let sessionErrorRetried = false;
19997
20010
  for (let attempt = 0;attempt < MAX_RATE_LIMIT_RETRIES; attempt++) {
19998
20011
  try {
19999
20012
  const result = await this._runWithClient(options, startTime);
20000
20013
  if (!result.success) {
20001
20014
  getSafeLogger()?.warn("acp-adapter", `Run failed for ${this.name}`, { exitCode: result.exitCode });
20015
+ if (result.sessionError && _acpAdapterDeps.shouldRetrySessionError && !sessionErrorRetried) {
20016
+ sessionErrorRetried = true;
20017
+ getSafeLogger()?.warn("acp-adapter", "Session error \u2014 retrying with fresh session", {
20018
+ storyId: options.storyId,
20019
+ featureName: options.featureName
20020
+ });
20021
+ if (options.featureName && options.storyId) {
20022
+ await clearAcpSession(options.workdir, options.featureName, options.storyId);
20023
+ }
20024
+ continue;
20025
+ }
20002
20026
  }
20003
20027
  return result;
20004
20028
  } catch (err) {
@@ -20127,6 +20151,7 @@ class AcpAgentAdapter {
20127
20151
  };
20128
20152
  }
20129
20153
  const success2 = lastResponse?.stopReason === "end_turn";
20154
+ const isSessionError = lastResponse?.stopReason === "error";
20130
20155
  const output = extractOutput(lastResponse);
20131
20156
  const estimatedCost = totalExactCostUsd ?? (totalTokenUsage.input_tokens > 0 || totalTokenUsage.output_tokens > 0 ? estimateCostFromTokenUsage(totalTokenUsage, options.modelDef.model) : 0);
20132
20157
  return {
@@ -20134,6 +20159,7 @@ class AcpAgentAdapter {
20134
20159
  exitCode: success2 ? 0 : 1,
20135
20160
  output: output.slice(-MAX_AGENT_OUTPUT_CHARS2),
20136
20161
  rateLimited: false,
20162
+ sessionError: isSessionError,
20137
20163
  durationMs,
20138
20164
  estimatedCost
20139
20165
  };
@@ -20328,6 +20354,7 @@ var init_adapter2 = __esm(() => {
20328
20354
  _acpAdapterDeps = {
20329
20355
  which,
20330
20356
  sleep,
20357
+ shouldRetrySessionError: true,
20331
20358
  createClient(cmdStr, cwd, timeoutSeconds, pidRegistry) {
20332
20359
  return createSpawnAcpClient(cmdStr, cwd, timeoutSeconds, pidRegistry);
20333
20360
  }
@@ -21190,17 +21217,24 @@ ${errors3.join(`
21190
21217
  return result.data;
21191
21218
  }
21192
21219
  async function loadConfigForWorkdir(rootConfigPath, packageDir) {
21220
+ const logger = getLogger();
21193
21221
  const rootNaxDir = dirname(rootConfigPath);
21194
21222
  const rootConfig = await loadConfig(rootNaxDir);
21195
21223
  if (!packageDir) {
21224
+ logger.debug("config", "No packageDir \u2014 using root config");
21196
21225
  return rootConfig;
21197
21226
  }
21198
21227
  const repoRoot = dirname(rootNaxDir);
21199
21228
  const packageConfigPath = join6(repoRoot, PROJECT_NAX_DIR, "mono", packageDir, "config.json");
21200
21229
  const packageOverride = await loadJsonFile(packageConfigPath, "config");
21201
21230
  if (!packageOverride) {
21231
+ logger.debug("config", "Per-package config not found \u2014 falling back to root config", {
21232
+ packageConfigPath,
21233
+ packageDir
21234
+ });
21202
21235
  return rootConfig;
21203
21236
  }
21237
+ logger.debug("config", "Per-package config loaded", { packageConfigPath, packageDir });
21204
21238
  return mergePackageConfig(rootConfig, packageOverride);
21205
21239
  }
21206
21240
  var init_loader = __esm(() => {
@@ -22370,7 +22404,7 @@ var package_default;
22370
22404
  var init_package = __esm(() => {
22371
22405
  package_default = {
22372
22406
  name: "@nathapp/nax",
22373
- version: "0.54.12",
22407
+ version: "0.55.1",
22374
22408
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22375
22409
  type: "module",
22376
22410
  bin: {
@@ -22449,8 +22483,8 @@ var init_version = __esm(() => {
22449
22483
  NAX_VERSION = package_default.version;
22450
22484
  NAX_COMMIT = (() => {
22451
22485
  try {
22452
- if (/^[0-9a-f]{6,10}$/.test("cbd14dd"))
22453
- return "cbd14dd";
22486
+ if (/^[0-9a-f]{6,10}$/.test("a51fce0b"))
22487
+ return "a51fce0b";
22454
22488
  } catch {}
22455
22489
  try {
22456
22490
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -23886,12 +23920,12 @@ async function loadPendingInteraction(requestId, featureDir) {
23886
23920
  const filename = `${requestId}.json`;
23887
23921
  const filePath = path3.join(interactionsDir, filename);
23888
23922
  try {
23889
- const file2 = Bun.file(filePath);
23890
- const exists = await file2.exists();
23923
+ const file3 = Bun.file(filePath);
23924
+ const exists = await file3.exists();
23891
23925
  if (!exists) {
23892
23926
  return null;
23893
23927
  }
23894
- const json2 = await file2.text();
23928
+ const json2 = await file3.text();
23895
23929
  const request = JSON.parse(json2);
23896
23930
  return request;
23897
23931
  } catch {
@@ -24220,9 +24254,9 @@ var init_acceptance2 = __esm(() => {
24220
24254
  async execute(ctx) {
24221
24255
  const logger = getLogger();
24222
24256
  const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
24223
- logger.info("acceptance", "Running acceptance tests");
24257
+ logger.info("acceptance", "Running acceptance tests", { storyId: ctx.story.id });
24224
24258
  if (!ctx.featureDir) {
24225
- logger.warn("acceptance", "No feature directory \u2014 skipping acceptance tests");
24259
+ logger.warn("acceptance", "No feature directory \u2014 skipping acceptance tests", { storyId: ctx.story.id });
24226
24260
  return { action: "continue" };
24227
24261
  }
24228
24262
  const testGroups = ctx.acceptanceTestPaths ?? [
@@ -24239,11 +24273,12 @@ var init_acceptance2 = __esm(() => {
24239
24273
  const testFile = Bun.file(testPath);
24240
24274
  const exists = await testFile.exists();
24241
24275
  if (!exists) {
24242
- logger.warn("acceptance", "Acceptance test file not found \u2014 skipping", { testPath });
24276
+ logger.warn("acceptance", "Acceptance test file not found \u2014 skipping", { storyId: ctx.story.id, testPath });
24243
24277
  continue;
24244
24278
  }
24245
24279
  const testCmdParts = buildAcceptanceRunCommand(testPath, effectiveConfig.project?.testFramework, effectiveConfig.acceptance.command);
24246
24280
  logger.info("acceptance", "Running acceptance command", {
24281
+ storyId: ctx.story.id,
24247
24282
  cmd: testCmdParts.join(" "),
24248
24283
  packageDir
24249
24284
  });
@@ -24266,12 +24301,14 @@ ${stderr}`;
24266
24301
  const overriddenFailures = failedACs.filter((acId) => overrides[acId]);
24267
24302
  if (overriddenFailures.length > 0) {
24268
24303
  logger.warn("acceptance", "Skipped failures (overridden)", {
24304
+ storyId: ctx.story.id,
24269
24305
  overriddenFailures,
24270
24306
  overrides: overriddenFailures.map((acId) => ({ acId, reason: overrides[acId] }))
24271
24307
  });
24272
24308
  }
24273
24309
  if (failedACs.length === 0 && exitCode !== 0) {
24274
24310
  logger.error("acceptance", "Tests errored with no AC failures parsed", {
24311
+ storyId: ctx.story.id,
24275
24312
  exitCode,
24276
24313
  packageDir
24277
24314
  });
@@ -24288,18 +24325,19 @@ ${stderr}`;
24288
24325
  }
24289
24326
  if (actualFailures.length > 0) {
24290
24327
  logger.error("acceptance", "Acceptance tests failed", {
24328
+ storyId: ctx.story.id,
24291
24329
  failedACs: actualFailures,
24292
24330
  packageDir
24293
24331
  });
24294
24332
  logTestOutput(logger, "acceptance", output);
24295
24333
  } else if (exitCode === 0) {
24296
- logger.info("acceptance", "Package acceptance tests passed", { packageDir });
24334
+ logger.info("acceptance", "Package acceptance tests passed", { storyId: ctx.story.id, packageDir });
24297
24335
  }
24298
24336
  }
24299
24337
  const combinedOutput = allOutputParts.join(`
24300
24338
  `);
24301
24339
  if (allFailedACs.length === 0) {
24302
- logger.info("acceptance", "All acceptance tests passed");
24340
+ logger.info("acceptance", "All acceptance tests passed", { storyId: ctx.story.id });
24303
24341
  return { action: "continue" };
24304
24342
  }
24305
24343
  ctx.acceptanceFailures = {
@@ -24639,6 +24677,99 @@ ${stderr}` };
24639
24677
  };
24640
24678
  });
24641
24679
 
24680
+ // src/quality/runner.ts
24681
+ var {spawn: spawn2 } = globalThis.Bun;
24682
+ async function runQualityCommand(opts) {
24683
+ const { commandName, command, workdir, storyId, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
24684
+ const startTime = Date.now();
24685
+ const logger = getSafeLogger();
24686
+ logger?.info("quality", `Running ${commandName}`, { storyId, commandName, command, workdir });
24687
+ try {
24688
+ const parts = command.split(/\s+/);
24689
+ const [executable, ...args] = parts;
24690
+ const proc = _qualityRunnerDeps.spawn({
24691
+ cmd: [executable, ...args],
24692
+ cwd: workdir,
24693
+ stdout: "pipe",
24694
+ stderr: "pipe"
24695
+ });
24696
+ let timedOut = false;
24697
+ const killTimer = setTimeout(() => {
24698
+ timedOut = true;
24699
+ try {
24700
+ proc.kill("SIGTERM");
24701
+ } catch {}
24702
+ setTimeout(() => {
24703
+ try {
24704
+ proc.kill("SIGKILL");
24705
+ } catch {}
24706
+ }, SIGKILL_GRACE_PERIOD_MS2);
24707
+ }, timeoutMs);
24708
+ const [exitCode, stdout, stderr] = await Promise.all([
24709
+ proc.exited,
24710
+ new Response(proc.stdout).text(),
24711
+ new Response(proc.stderr).text()
24712
+ ]);
24713
+ clearTimeout(killTimer);
24714
+ const durationMs = Date.now() - startTime;
24715
+ if (timedOut) {
24716
+ logger?.warn("quality", `${commandName} timed out`, {
24717
+ storyId,
24718
+ commandName,
24719
+ command,
24720
+ workdir,
24721
+ durationMs,
24722
+ timedOut: true
24723
+ });
24724
+ return {
24725
+ commandName,
24726
+ command,
24727
+ success: false,
24728
+ exitCode: -1,
24729
+ output: `[nax] ${commandName} timed out after ${timeoutMs / 1000}s`,
24730
+ durationMs,
24731
+ timedOut: true
24732
+ };
24733
+ }
24734
+ const output = [stdout, stderr].filter(Boolean).join(`
24735
+ `);
24736
+ const success2 = exitCode === 0;
24737
+ logger?.info("quality", `${commandName} completed`, {
24738
+ storyId,
24739
+ commandName,
24740
+ command,
24741
+ workdir,
24742
+ exitCode,
24743
+ durationMs,
24744
+ timedOut: false
24745
+ });
24746
+ return { commandName, command, success: success2, exitCode, output, durationMs, timedOut: false };
24747
+ } catch (error48) {
24748
+ const durationMs = Date.now() - startTime;
24749
+ return {
24750
+ commandName,
24751
+ command,
24752
+ success: false,
24753
+ exitCode: -1,
24754
+ output: errorMessage(error48),
24755
+ durationMs,
24756
+ timedOut: false
24757
+ };
24758
+ }
24759
+ }
24760
+ var DEFAULT_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS2 = 5000, _qualityRunnerDeps;
24761
+ var init_runner2 = __esm(() => {
24762
+ init_logger2();
24763
+ _qualityRunnerDeps = {
24764
+ spawn: spawn2
24765
+ };
24766
+ });
24767
+
24768
+ // src/quality/index.ts
24769
+ var init_quality = __esm(() => {
24770
+ init_runner2();
24771
+ });
24772
+
24642
24773
  // src/pipeline/event-bus.ts
24643
24774
  class PipelineEventBus {
24644
24775
  subscribers = new Map;
@@ -24913,7 +25044,7 @@ var init_language_commands = __esm(() => {
24913
25044
  });
24914
25045
 
24915
25046
  // src/review/semantic.ts
24916
- var {spawn: spawn2 } = globalThis.Bun;
25047
+ var {spawn: spawn3 } = globalThis.Bun;
24917
25048
  async function collectDiff(workdir, storyGitRef, excludePatterns) {
24918
25049
  const cmd = ["git", "diff", "--unified=3", `${storyGitRef}..HEAD`];
24919
25050
  if (excludePatterns.length > 0) {
@@ -25016,7 +25147,7 @@ If all ACs are correctly implemented, respond with { "passed": true, "findings":
25016
25147
  function parseLLMResponse(raw) {
25017
25148
  try {
25018
25149
  let cleaned = raw.trim();
25019
- const fenceMatch = cleaned.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?\s*```$/);
25150
+ const fenceMatch = cleaned.match(/^```(?:json)?\s*\n([\s\S]*?)\n```/);
25020
25151
  if (fenceMatch)
25021
25152
  cleaned = fenceMatch[1].trim();
25022
25153
  const parsed = JSON.parse(cleaned);
@@ -25198,18 +25329,17 @@ var init_semantic = __esm(() => {
25198
25329
  init_logger2();
25199
25330
  init_git();
25200
25331
  _semanticDeps = {
25201
- spawn: spawn2,
25332
+ spawn: spawn3,
25202
25333
  isGitRefValid,
25203
25334
  getMergeBase
25204
25335
  };
25205
25336
  });
25206
25337
 
25207
25338
  // src/review/runner.ts
25208
- var {spawn: spawn3 } = globalThis.Bun;
25209
25339
  async function loadPackageJson(workdir) {
25210
25340
  try {
25211
- const file2 = _reviewRunnerDeps.file(`${workdir}/package.json`);
25212
- const content = await file2.text();
25341
+ const file3 = _reviewRunnerDeps.file(`${workdir}/package.json`);
25342
+ const content = await file3.text();
25213
25343
  return JSON.parse(content);
25214
25344
  } catch {
25215
25345
  return null;
@@ -25257,81 +25387,20 @@ async function resolveCommand(check2, config2, executionConfig, workdir, quality
25257
25387
  }
25258
25388
  return null;
25259
25389
  }
25260
- async function runCheck(check2, command, workdir) {
25261
- const startTime = Date.now();
25262
- const logger = getSafeLogger();
25263
- logger?.info("review", `Running ${check2} check`, { check: check2, command, workdir });
25264
- try {
25265
- const parts = command.split(/\s+/);
25266
- const executable = parts[0];
25267
- const args = parts.slice(1);
25268
- const proc = _reviewRunnerDeps.spawn({
25269
- cmd: [executable, ...args],
25270
- cwd: workdir,
25271
- stdout: "pipe",
25272
- stderr: "pipe"
25273
- });
25274
- let timedOut = false;
25275
- const timerId = setTimeout(() => {
25276
- timedOut = true;
25277
- try {
25278
- proc.kill("SIGTERM");
25279
- } catch {}
25280
- setTimeout(() => {
25281
- try {
25282
- proc.kill("SIGKILL");
25283
- } catch {}
25284
- }, SIGKILL_GRACE_PERIOD_MS2);
25285
- }, REVIEW_CHECK_TIMEOUT_MS);
25286
- const exitCode = await proc.exited;
25287
- clearTimeout(timerId);
25288
- if (timedOut) {
25289
- return {
25290
- check: check2,
25291
- command,
25292
- success: false,
25293
- exitCode: -1,
25294
- output: `[nax] ${check2} timed out after ${REVIEW_CHECK_TIMEOUT_MS / 1000}s`,
25295
- durationMs: Date.now() - startTime
25296
- };
25297
- }
25298
- const stdout = await new Response(proc.stdout).text();
25299
- const stderr = await new Response(proc.stderr).text();
25300
- const output = [stdout, stderr].filter(Boolean).join(`
25301
- `);
25302
- if (exitCode !== 0) {
25303
- logger?.warn("review", `${check2} check failed`, {
25304
- check: check2,
25305
- command,
25306
- workdir,
25307
- exitCode,
25308
- output: output.slice(0, 2000)
25309
- });
25310
- } else {
25311
- logger?.debug("review", `${check2} check passed`, { check: check2, command, durationMs: Date.now() - startTime });
25312
- }
25313
- return {
25314
- check: check2,
25315
- command,
25316
- success: exitCode === 0,
25317
- exitCode,
25318
- output,
25319
- durationMs: Date.now() - startTime
25320
- };
25321
- } catch (error48) {
25322
- return {
25323
- check: check2,
25324
- command,
25325
- success: false,
25326
- exitCode: -1,
25327
- output: errorMessage(error48),
25328
- durationMs: Date.now() - startTime
25329
- };
25330
- }
25390
+ async function runCheck(check2, command, workdir, storyId) {
25391
+ const result = await runQualityCommand({ commandName: check2, command, workdir, storyId });
25392
+ return {
25393
+ check: check2,
25394
+ command: result.command,
25395
+ success: result.success,
25396
+ exitCode: result.exitCode,
25397
+ output: result.output,
25398
+ durationMs: result.durationMs
25399
+ };
25331
25400
  }
25332
25401
  async function getUncommittedFilesImpl(workdir) {
25333
25402
  try {
25334
- const proc = _reviewRunnerDeps.spawn({
25403
+ const proc = Bun.spawn({
25335
25404
  cmd: ["git", "diff", "--name-only", "HEAD"],
25336
25405
  cwd: workdir,
25337
25406
  stdout: "pipe",
@@ -25348,7 +25417,7 @@ async function getUncommittedFilesImpl(workdir) {
25348
25417
  return [];
25349
25418
  }
25350
25419
  }
25351
- async function runReview(config2, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig) {
25420
+ async function runReview(config2, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig, retrySkipChecks) {
25352
25421
  const startTime = Date.now();
25353
25422
  const logger = getSafeLogger();
25354
25423
  const checks3 = [];
@@ -25388,6 +25457,12 @@ Stage and commit these files before running review.`
25388
25457
  };
25389
25458
  }
25390
25459
  for (const checkName of config2.checks) {
25460
+ if (retrySkipChecks?.has(checkName)) {
25461
+ getSafeLogger()?.debug("review", `Skipping ${checkName} check (already passed in previous review pass)`, {
25462
+ storyId
25463
+ });
25464
+ continue;
25465
+ }
25391
25466
  if (checkName === "semantic") {
25392
25467
  const semanticStory = {
25393
25468
  id: storyId ?? "",
@@ -25416,7 +25491,7 @@ Stage and commit these files before running review.`
25416
25491
  getSafeLogger()?.warn("review", `Skipping ${checkName} check (command not configured or disabled)`);
25417
25492
  continue;
25418
25493
  }
25419
- const result = await runCheck(checkName, command, workdir);
25494
+ const result = await runCheck(checkName, command, workdir, storyId);
25420
25495
  checks3.push(result);
25421
25496
  if (!result.success && !firstFailure) {
25422
25497
  firstFailure = `${checkName} failed (exit code ${result.exitCode})`;
@@ -25433,9 +25508,10 @@ Stage and commit these files before running review.`
25433
25508
  failureReason: firstFailure
25434
25509
  };
25435
25510
  }
25436
- var _reviewSemanticDeps, _reviewRunnerDeps, REVIEW_CHECK_TIMEOUT_MS = 120000, SIGKILL_GRACE_PERIOD_MS2 = 5000, _reviewGitDeps;
25437
- var init_runner2 = __esm(() => {
25511
+ var _reviewSemanticDeps, _reviewRunnerDeps, _reviewGitDeps;
25512
+ var init_runner3 = __esm(() => {
25438
25513
  init_logger2();
25514
+ init_quality();
25439
25515
  init_git();
25440
25516
  init_language_commands();
25441
25517
  init_semantic();
@@ -25443,7 +25519,6 @@ var init_runner2 = __esm(() => {
25443
25519
  runSemanticReview
25444
25520
  };
25445
25521
  _reviewRunnerDeps = {
25446
- spawn: spawn3,
25447
25522
  file: Bun.file,
25448
25523
  which: Bun.which
25449
25524
  };
@@ -25483,9 +25558,9 @@ async function getChangedFiles(workdir, baseRef) {
25483
25558
  }
25484
25559
 
25485
25560
  class ReviewOrchestrator {
25486
- async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef, scopePrefix, qualityCommands, storyId, story, modelResolver, naxConfig) {
25561
+ async review(reviewConfig, workdir, executionConfig, plugins, storyGitRef, scopePrefix, qualityCommands, storyId, story, modelResolver, naxConfig, retrySkipChecks) {
25487
25562
  const logger = getSafeLogger();
25488
- const builtIn = await runReview(reviewConfig, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig);
25563
+ const builtIn = await runReview(reviewConfig, workdir, executionConfig, qualityCommands, storyId, storyGitRef, story, modelResolver, naxConfig, retrySkipChecks);
25489
25564
  if (!builtIn.success) {
25490
25565
  return { builtIn, success: false, failureReason: builtIn.failureReason, pluginFailed: false };
25491
25566
  }
@@ -25550,7 +25625,7 @@ class ReviewOrchestrator {
25550
25625
  var _orchestratorDeps, reviewOrchestrator;
25551
25626
  var init_orchestrator = __esm(() => {
25552
25627
  init_logger2();
25553
- init_runner2();
25628
+ init_runner3();
25554
25629
  _orchestratorDeps = { spawn: spawn4 };
25555
25630
  reviewOrchestrator = new ReviewOrchestrator;
25556
25631
  });
@@ -25579,12 +25654,14 @@ var init_review = __esm(() => {
25579
25654
  const agentResolver = ctx.agentGetFn ?? getAgent;
25580
25655
  const agentName = effectiveConfig.autoMode?.defaultAgent;
25581
25656
  const modelResolver = (_tier) => agentName ? agentResolver(agentName) ?? null : null;
25657
+ const retrySkipChecks = ctx.retrySkipChecks;
25658
+ ctx.retrySkipChecks = undefined;
25582
25659
  const result = await reviewOrchestrator.review(effectiveConfig.review, effectiveWorkdir, effectiveConfig.execution, ctx.plugins, ctx.storyGitRef, ctx.story.workdir, effectiveConfig.quality?.commands, ctx.story.id, {
25583
25660
  id: ctx.story.id,
25584
25661
  title: ctx.story.title,
25585
25662
  description: ctx.story.description,
25586
25663
  acceptanceCriteria: ctx.story.acceptanceCriteria
25587
- }, modelResolver, ctx.config);
25664
+ }, modelResolver, ctx.config, retrySkipChecks);
25588
25665
  ctx.reviewResult = result.builtIn;
25589
25666
  if (!result.success) {
25590
25667
  const pluginFindings = result.builtIn.pluginReviewers?.flatMap((pr) => pr.findings ?? []) ?? [];
@@ -25626,17 +25703,6 @@ var init_review = __esm(() => {
25626
25703
 
25627
25704
  // src/pipeline/stages/autofix.ts
25628
25705
  import { join as join17 } from "path";
25629
- async function runCommand(cmd, cwd) {
25630
- const parts = cmd.split(/\s+/);
25631
- const proc = Bun.spawn(parts, { cwd, stdout: "pipe", stderr: "pipe" });
25632
- const [exitCode, stdout, stderr] = await Promise.all([
25633
- proc.exited,
25634
- new Response(proc.stdout).text(),
25635
- new Response(proc.stderr).text()
25636
- ]);
25637
- return { exitCode, output: `${stdout}
25638
- ${stderr}` };
25639
- }
25640
25706
  async function recheckReview(ctx) {
25641
25707
  const { reviewStage: reviewStage2 } = await Promise.resolve().then(() => (init_review(), exports_review));
25642
25708
  if (!reviewStage2.enabled(ctx))
@@ -25672,20 +25738,35 @@ Commit your fixes when done.${scopeConstraint}`;
25672
25738
  async function runAgentRectification(ctx) {
25673
25739
  const logger = getLogger();
25674
25740
  const effectiveConfig = ctx.effectiveConfig ?? ctx.config;
25675
- const maxAttempts = effectiveConfig.quality.autofix?.maxAttempts ?? 2;
25741
+ const maxPerCycle = effectiveConfig.quality.autofix?.maxAttempts ?? 2;
25742
+ const maxTotal = effectiveConfig.quality.autofix?.maxTotalAttempts ?? 10;
25743
+ const consumed = ctx.autofixAttempt ?? 0;
25676
25744
  const failedChecks = collectFailedChecks(ctx);
25677
25745
  if (failedChecks.length === 0) {
25678
25746
  logger.debug("autofix", "No failed checks found \u2014 skipping agent rectification", { storyId: ctx.story.id });
25679
25747
  return false;
25680
25748
  }
25749
+ if (consumed >= maxTotal) {
25750
+ logger.warn("autofix", "Global autofix budget exhausted \u2014 escalating", {
25751
+ storyId: ctx.story.id,
25752
+ totalAttempts: consumed,
25753
+ maxTotalAttempts: maxTotal
25754
+ });
25755
+ return false;
25756
+ }
25757
+ const remainingBudget = maxTotal - consumed;
25758
+ const maxAttempts = Math.min(maxPerCycle, remainingBudget);
25681
25759
  logger.info("autofix", "Starting agent rectification for review failures", {
25682
25760
  storyId: ctx.story.id,
25683
25761
  failedChecks: failedChecks.map((c) => c.check),
25684
- maxAttempts
25762
+ maxAttempts,
25763
+ totalUsed: consumed,
25764
+ maxTotalAttempts: maxTotal
25685
25765
  });
25686
25766
  const agentGetFn = ctx.agentGetFn ?? _autofixDeps.getAgent;
25687
25767
  for (let attempt = 1;attempt <= maxAttempts; attempt++) {
25688
- logger.info("autofix", `Agent rectification attempt ${attempt}/${maxAttempts}`, { storyId: ctx.story.id });
25768
+ ctx.autofixAttempt = consumed + attempt;
25769
+ logger.info("autofix", `Agent rectification attempt ${ctx.autofixAttempt}/${maxTotal}`, { storyId: ctx.story.id });
25689
25770
  const agent = agentGetFn(ctx.config.autoMode.defaultAgent);
25690
25771
  if (!agent) {
25691
25772
  logger.error("autofix", "Agent not found \u2014 cannot run agent rectification", { storyId: ctx.story.id });
@@ -25732,6 +25813,7 @@ var init_autofix = __esm(() => {
25732
25813
  init_config();
25733
25814
  init_loader();
25734
25815
  init_logger2();
25816
+ init_quality();
25735
25817
  init_event_bus();
25736
25818
  autofixStage = {
25737
25819
  name: "autofix",
@@ -25768,7 +25850,12 @@ var init_autofix = __esm(() => {
25768
25850
  if (hasLintFailure && (lintFixCmd || formatFixCmd)) {
25769
25851
  if (lintFixCmd) {
25770
25852
  pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: lintFixCmd });
25771
- const lintResult = await _autofixDeps.runCommand(lintFixCmd, effectiveWorkdir);
25853
+ const lintResult = await _autofixDeps.runQualityCommand({
25854
+ commandName: "lintFix",
25855
+ command: lintFixCmd,
25856
+ workdir: effectiveWorkdir,
25857
+ storyId: ctx.story.id
25858
+ });
25772
25859
  logger.debug("autofix", `lintFix exit=${lintResult.exitCode}`, { storyId: ctx.story.id, command: lintFixCmd });
25773
25860
  if (lintResult.exitCode !== 0) {
25774
25861
  logger.warn("autofix", "lintFix command failed \u2014 may not have fixed all issues", {
@@ -25779,7 +25866,12 @@ var init_autofix = __esm(() => {
25779
25866
  }
25780
25867
  if (formatFixCmd) {
25781
25868
  pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: formatFixCmd });
25782
- const fmtResult = await _autofixDeps.runCommand(formatFixCmd, effectiveWorkdir);
25869
+ const fmtResult = await _autofixDeps.runQualityCommand({
25870
+ commandName: "formatFix",
25871
+ command: formatFixCmd,
25872
+ workdir: effectiveWorkdir,
25873
+ storyId: ctx.story.id
25874
+ });
25783
25875
  logger.debug("autofix", `formatFix exit=${fmtResult.exitCode}`, {
25784
25876
  storyId: ctx.story.id,
25785
25877
  command: formatFixCmd
@@ -25805,6 +25897,14 @@ var init_autofix = __esm(() => {
25805
25897
  if (agentFixed) {
25806
25898
  if (ctx.reviewResult)
25807
25899
  ctx.reviewResult = { ...ctx.reviewResult, success: true };
25900
+ const passedChecks = (ctx.reviewResult?.checks ?? []).filter((c) => c.success).map((c) => c.check);
25901
+ if (passedChecks.length > 0) {
25902
+ ctx.retrySkipChecks = new Set(passedChecks);
25903
+ logger.debug("autofix", "Skipping already-passed checks on retry", {
25904
+ storyId: ctx.story.id,
25905
+ skippedChecks: passedChecks
25906
+ });
25907
+ }
25808
25908
  logger.info("autofix", "Agent rectification succeeded \u2014 retrying review", { storyId: ctx.story.id });
25809
25909
  return { action: "retry", fromStage: "review" };
25810
25910
  }
@@ -25814,7 +25914,7 @@ var init_autofix = __esm(() => {
25814
25914
  };
25815
25915
  _autofixDeps = {
25816
25916
  getAgent,
25817
- runCommand,
25917
+ runQualityCommand,
25818
25918
  recheckReview,
25819
25919
  runAgentRectification,
25820
25920
  loadConfigForWorkdir
@@ -25830,8 +25930,8 @@ async function appendProgress(featureDir, storyId, status, message) {
25830
25930
  const timestamp = new Date().toISOString();
25831
25931
  const entry = `[${timestamp}] ${storyId} \u2014 ${status.toUpperCase()} \u2014 ${message}
25832
25932
  `;
25833
- const file2 = Bun.file(progressPath);
25834
- const existing = await file2.exists() ? await file2.text() : "";
25933
+ const file3 = Bun.file(progressPath);
25934
+ const existing = await file3.exists() ? await file3.text() : "";
25835
25935
  await Bun.write(progressPath, existing + entry);
25836
25936
  }
25837
25937
  var init_progress = () => {};
@@ -25890,6 +25990,7 @@ var init_completion = __esm(() => {
25890
25990
  await savePRD(ctx.prd, prdPath);
25891
25991
  const updatedCounts = countStories(ctx.prd);
25892
25992
  logger.info("completion", "Progress update", {
25993
+ storyId: ctx.story.id,
25893
25994
  completed: updatedCounts.passed + updatedCounts.failed,
25894
25995
  total: updatedCounts.total,
25895
25996
  passed: updatedCounts.passed,
@@ -26347,7 +26448,7 @@ function deriveTestPatterns(contextFiles) {
26347
26448
  async function detectTestDir(workdir) {
26348
26449
  for (const dir of COMMON_TEST_DIRS) {
26349
26450
  const fullPath = path6.join(workdir, dir);
26350
- const file2 = Bun.file(path6.join(fullPath, "."));
26451
+ const file3 = Bun.file(path6.join(fullPath, "."));
26351
26452
  try {
26352
26453
  const dirStat = await Bun.file(fullPath).exists();
26353
26454
  const proc = Bun.spawn(["test", "-d", fullPath], { stdout: "pipe", stderr: "pipe" });
@@ -26410,21 +26511,21 @@ function formatTestSummary(files, detail) {
26410
26511
  lines.push("The following tests already exist. DO NOT duplicate this coverage.");
26411
26512
  lines.push("Focus only on testing NEW behavior introduced by this story.");
26412
26513
  lines.push("");
26413
- for (const file2 of files) {
26514
+ for (const file3 of files) {
26414
26515
  switch (detail) {
26415
26516
  case "names-only":
26416
- lines.push(`- **${file2.relativePath}** (${file2.testCount} tests)`);
26517
+ lines.push(`- **${file3.relativePath}** (${file3.testCount} tests)`);
26417
26518
  break;
26418
26519
  case "names-and-counts":
26419
- lines.push(`### ${file2.relativePath} (${file2.testCount} tests)`);
26420
- for (const desc of file2.describes) {
26520
+ lines.push(`### ${file3.relativePath} (${file3.testCount} tests)`);
26521
+ for (const desc of file3.describes) {
26421
26522
  lines.push(`- ${desc.name} (${desc.tests.length} tests)`);
26422
26523
  }
26423
26524
  lines.push("");
26424
26525
  break;
26425
26526
  case "describe-blocks":
26426
- lines.push(`### ${file2.relativePath} (${file2.testCount} tests)`);
26427
- for (const desc of file2.describes) {
26527
+ lines.push(`### ${file3.relativePath} (${file3.testCount} tests)`);
26528
+ for (const desc of file3.describes) {
26428
26529
  lines.push(`- **${desc.name}** (${desc.tests.length} tests)`);
26429
26530
  for (const test of desc.tests) {
26430
26531
  lines.push(` - ${test}`);
@@ -26553,8 +26654,8 @@ function renderErrorSection(sections, byType) {
26553
26654
  const fileList = match[1].split(",").map((f) => f.trim());
26554
26655
  sections.push(`**Required files:**
26555
26656
  `);
26556
- for (const file2 of fileList) {
26557
- sections.push(`- \`${file2}\``);
26657
+ for (const file3 of fileList) {
26658
+ sections.push(`- \`${file3}\``);
26558
26659
  }
26559
26660
  sections.push(`
26560
26661
  `);
@@ -26748,24 +26849,24 @@ async function addFileElements(elements, storyContext, story) {
26748
26849
  for (const relativeFilePath of filesToLoad) {
26749
26850
  try {
26750
26851
  const absolutePath = path7.resolve(workdir, relativeFilePath);
26751
- const file2 = Bun.file(absolutePath);
26752
- if (!await file2.exists()) {
26852
+ const file3 = Bun.file(absolutePath);
26853
+ if (!await file3.exists()) {
26753
26854
  const logger = getLogger();
26754
26855
  logger.warn("context", "Relevant file not found", { filePath: relativeFilePath, storyId: story.id });
26755
26856
  continue;
26756
26857
  }
26757
- if (file2.size > MAX_FILE_SIZE_BYTES) {
26858
+ if (file3.size > MAX_FILE_SIZE_BYTES) {
26758
26859
  const logger = getLogger();
26759
26860
  logger.warn("context", "File too large for inline \u2014 using path-only", {
26760
26861
  filePath: relativeFilePath,
26761
- sizeKB: Math.round(file2.size / 1024),
26862
+ sizeKB: Math.round(file3.size / 1024),
26762
26863
  maxKB: 10,
26763
26864
  storyId: story.id
26764
26865
  });
26765
- elements.push(createFileContext(relativeFilePath, `_File too large to inline (${Math.round(file2.size / 1024)}KB). Path: \`${relativeFilePath}\` \u2014 read it directly if needed._`, 5));
26866
+ elements.push(createFileContext(relativeFilePath, `_File too large to inline (${Math.round(file3.size / 1024)}KB). Path: \`${relativeFilePath}\` \u2014 read it directly if needed._`, 5));
26766
26867
  continue;
26767
26868
  }
26768
- const content = await file2.text();
26869
+ const content = await file3.text();
26769
26870
  const ext = path7.extname(relativeFilePath).slice(1) || "txt";
26770
26871
  elements.push(createFileContext(relativeFilePath, `\`\`\`${ext}
26771
26872
  // File: ${relativeFilePath}
@@ -26818,10 +26919,10 @@ function hookCtx(feature, opts) {
26818
26919
  }
26819
26920
  async function loadPackageContextMd(packageWorkdir) {
26820
26921
  const contextPath = `${packageWorkdir}/.nax/context.md`;
26821
- const file2 = Bun.file(contextPath);
26822
- if (!await file2.exists())
26922
+ const file3 = Bun.file(contextPath);
26923
+ if (!await file3.exists())
26823
26924
  return null;
26824
- return file2.text();
26925
+ return file3.text();
26825
26926
  }
26826
26927
  async function buildStoryContextFull(prd, story, config2, packageWorkdir) {
26827
26928
  try {
@@ -27073,11 +27174,11 @@ async function verifyTestWriterIsolation(workdir, beforeRef, allowedPaths = ["sr
27073
27174
  const sourceFiles = changed.filter((f) => isSourceFile(f) && !isTestFile(f));
27074
27175
  const softViolations = [];
27075
27176
  const violations = [];
27076
- for (const file2 of sourceFiles) {
27077
- if (matchesAllowedPath(file2, allowedPaths)) {
27078
- softViolations.push(file2);
27177
+ for (const file3 of sourceFiles) {
27178
+ if (matchesAllowedPath(file3, allowedPaths)) {
27179
+ softViolations.push(file3);
27079
27180
  } else {
27080
- violations.push(file2);
27181
+ violations.push(file3);
27081
27182
  }
27082
27183
  }
27083
27184
  return {
@@ -27493,9 +27594,9 @@ async function verifyAssets(workingDirectory, expectedFiles) {
27493
27594
  if (!expectedFiles || expectedFiles.length === 0)
27494
27595
  return { success: true, missingFiles: [] };
27495
27596
  const missingFiles = [];
27496
- for (const file2 of expectedFiles) {
27497
- if (!existsSync16(join22(workingDirectory, file2)))
27498
- missingFiles.push(file2);
27597
+ for (const file3 of expectedFiles) {
27598
+ if (!existsSync16(join22(workingDirectory, file3)))
27599
+ missingFiles.push(file3);
27499
27600
  }
27500
27601
  if (missingFiles.length > 0) {
27501
27602
  return {
@@ -27601,7 +27702,7 @@ function createRectificationPrompt(failures, story, config2) {
27601
27702
  const maxChars = config2?.maxFailureSummaryChars ?? 2000;
27602
27703
  const failureSummary = formatFailureSummary(failures, maxChars);
27603
27704
  const failingFiles = Array.from(new Set(failures.map((f) => f.file)));
27604
- const testCommands = failingFiles.map((file2) => ` bun test ${file2}`).join(`
27705
+ const testCommands = failingFiles.map((file3) => ` bun test ${file3}`).join(`
27605
27706
  `);
27606
27707
  return `# Rectification Required
27607
27708
 
@@ -27648,7 +27749,7 @@ function createEscalatedRectificationPrompt(failures, story, priorAttempts, orig
27648
27749
  const maxChars = config2?.maxFailureSummaryChars ?? 2000;
27649
27750
  const failureSummary = formatFailureSummary(failures, maxChars);
27650
27751
  const failingFiles = Array.from(new Set(failures.map((f) => f.file)));
27651
- const testCommands = failingFiles.map((file2) => ` bun test ${file2}`).join(`
27752
+ const testCommands = failingFiles.map((file3) => ` bun test ${file3}`).join(`
27652
27753
  `);
27653
27754
  const failingTestNames = failures.map((f) => f.testName);
27654
27755
  let failingTestsSection = "";
@@ -28359,12 +28460,12 @@ async function loadOverride(role, workdir, config2) {
28359
28460
  return null;
28360
28461
  }
28361
28462
  const absolutePath = join23(workdir, overridePath);
28362
- const file2 = Bun.file(absolutePath);
28363
- if (!await file2.exists()) {
28463
+ const file3 = Bun.file(absolutePath);
28464
+ if (!await file3.exists()) {
28364
28465
  return null;
28365
28466
  }
28366
28467
  try {
28367
- return await file2.text();
28468
+ return await file3.text();
28368
28469
  } catch (err) {
28369
28470
  throw new Error(`Cannot read prompt override for role "${role}" at "${absolutePath}": ${err instanceof Error ? err.message : String(err)}`);
28370
28471
  }
@@ -28486,9 +28587,9 @@ ${this._contextMd}
28486
28587
  }
28487
28588
  if (this._overridePath) {
28488
28589
  try {
28489
- const file2 = Bun.file(this._overridePath);
28490
- if (await file2.exists()) {
28491
- return await file2.text();
28590
+ const file3 = Bun.file(this._overridePath);
28591
+ if (await file3.exists()) {
28592
+ return await file3.text();
28492
28593
  }
28493
28594
  } catch {}
28494
28595
  }
@@ -28824,14 +28925,14 @@ async function readVerdict(workdir) {
28824
28925
  const logger = getLogger();
28825
28926
  const verdictPath = path9.join(workdir, VERDICT_FILE);
28826
28927
  try {
28827
- const file2 = Bun.file(verdictPath);
28828
- const exists = await file2.exists();
28928
+ const file3 = Bun.file(verdictPath);
28929
+ const exists = await file3.exists();
28829
28930
  if (!exists) {
28830
28931
  return null;
28831
28932
  }
28832
28933
  let rawText;
28833
28934
  try {
28834
- rawText = await file2.text();
28935
+ rawText = await file3.text();
28835
28936
  } catch (readErr) {
28836
28937
  logger.warn("tdd", "Failed to read verifier verdict file", {
28837
28938
  path: verdictPath,
@@ -29756,8 +29857,8 @@ async function readQueueFile(workdir) {
29756
29857
  const processingPath = path10.join(workdir, ".queue.txt.processing");
29757
29858
  const logger = getSafeLogger4();
29758
29859
  try {
29759
- const file2 = Bun.file(queuePath);
29760
- const exists = await file2.exists();
29860
+ const file3 = Bun.file(queuePath);
29861
+ const exists = await file3.exists();
29761
29862
  if (!exists) {
29762
29863
  return [];
29763
29864
  }
@@ -29781,8 +29882,8 @@ async function clearQueueFile(workdir) {
29781
29882
  const processingPath = path10.join(workdir, ".queue.txt.processing");
29782
29883
  const logger = getSafeLogger4();
29783
29884
  try {
29784
- const file2 = Bun.file(processingPath);
29785
- const exists = await file2.exists();
29885
+ const file3 = Bun.file(processingPath);
29886
+ const exists = await file3.exists();
29786
29887
  if (exists) {
29787
29888
  await Bun.spawn(["rm", processingPath], { stdout: "pipe" }).exited;
29788
29889
  }
@@ -30378,8 +30479,8 @@ async function importGrepFallback(sourceFiles, workdir, testFilePatterns) {
30378
30479
  const testFilePaths = [];
30379
30480
  for (const pattern of testFilePatterns) {
30380
30481
  const glob = _bunDeps.glob(pattern);
30381
- for await (const file2 of glob.scan(workdir)) {
30382
- testFilePaths.push(`${workdir}/${file2}`);
30482
+ for await (const file3 of glob.scan(workdir)) {
30483
+ testFilePaths.push(`${workdir}/${file3}`);
30383
30484
  }
30384
30485
  }
30385
30486
  const matched = [];
@@ -31289,8 +31390,8 @@ function generateContextTemplate(scan) {
31289
31390
  lines.push(`## Project Structure
31290
31391
  `);
31291
31392
  lines.push("```");
31292
- for (const file2 of scan.fileTree.slice(0, 20)) {
31293
- lines.push(file2);
31393
+ for (const file3 of scan.fileTree.slice(0, 20)) {
31394
+ lines.push(file3);
31294
31395
  }
31295
31396
  if (scan.fileTree.length > 20) {
31296
31397
  lines.push(`... and ${scan.fileTree.length - 20} more files`);
@@ -32109,8 +32210,8 @@ async function checkStaleLock(workdir) {
32109
32210
  };
32110
32211
  }
32111
32212
  try {
32112
- const file2 = Bun.file(lockPath);
32113
- const content = await file2.text();
32213
+ const file3 = Bun.file(lockPath);
32214
+ const content = await file3.text();
32114
32215
  const lockData = JSON.parse(content);
32115
32216
  let lockTimeMs;
32116
32217
  if (lockData.timestamp) {
@@ -32396,8 +32497,8 @@ async function checkGitignoreCoversNax(workdir) {
32396
32497
  message: ".gitignore not found"
32397
32498
  };
32398
32499
  }
32399
- const file2 = Bun.file(gitignorePath);
32400
- const content = await file2.text();
32500
+ const file3 = Bun.file(gitignorePath);
32501
+ const content = await file3.text();
32401
32502
  const patterns = [
32402
32503
  "nax.lock",
32403
32504
  ".nax/**/runs/",
@@ -33081,14 +33182,14 @@ async function fireHook(config2, event, ctx, workdir) {
33081
33182
  }
33082
33183
  }
33083
33184
  var DEFAULT_TIMEOUT = 5000;
33084
- var init_runner3 = __esm(() => {
33185
+ var init_runner4 = __esm(() => {
33085
33186
  init_logger2();
33086
33187
  init_json_file();
33087
33188
  });
33088
33189
 
33089
33190
  // src/hooks/index.ts
33090
33191
  var init_hooks = __esm(() => {
33091
- init_runner3();
33192
+ init_runner4();
33092
33193
  });
33093
33194
 
33094
33195
  // src/execution/crash-heartbeat.ts
@@ -33982,7 +34083,7 @@ async function handleRunCompletion(options) {
33982
34083
  }
33983
34084
  var _runCompletionDeps;
33984
34085
  var init_run_completion = __esm(() => {
33985
- init_runner3();
34086
+ init_runner4();
33986
34087
  init_logger2();
33987
34088
  init_metrics();
33988
34089
  init_event_bus();
@@ -35101,13 +35202,23 @@ __export(exports_parallel_worker, {
35101
35202
  async function executeStoryInWorktree(story, worktreePath, context, routing, eventEmitter) {
35102
35203
  const logger = getSafeLogger();
35103
35204
  try {
35205
+ let storyGitRef;
35206
+ if (story.storyGitRef && await isGitRefValid(worktreePath, story.storyGitRef)) {
35207
+ storyGitRef = story.storyGitRef;
35208
+ } else {
35209
+ storyGitRef = await captureGitRef(worktreePath);
35210
+ if (storyGitRef) {
35211
+ story.storyGitRef = storyGitRef;
35212
+ }
35213
+ }
35104
35214
  const pipelineContext = {
35105
35215
  ...context,
35106
35216
  effectiveConfig: context.effectiveConfig ?? context.config,
35107
35217
  story,
35108
35218
  stories: [story],
35109
35219
  workdir: worktreePath,
35110
- routing
35220
+ routing,
35221
+ storyGitRef: storyGitRef ?? undefined
35111
35222
  };
35112
35223
  logger?.debug("parallel", "Executing story in worktree", {
35113
35224
  storyId: story.id,
@@ -35183,6 +35294,7 @@ var init_parallel_worker = __esm(() => {
35183
35294
  init_runner();
35184
35295
  init_stages();
35185
35296
  init_routing();
35297
+ init_git();
35186
35298
  });
35187
35299
 
35188
35300
  // src/worktree/manager.ts
@@ -35613,6 +35725,16 @@ __export(exports_merge_conflict_rectify, {
35613
35725
  rectifyConflictedStory: () => rectifyConflictedStory
35614
35726
  });
35615
35727
  import path15 from "path";
35728
+ async function closeStaleAcpSession(worktreePath, sessionName) {
35729
+ const logger = getSafeLogger();
35730
+ try {
35731
+ const { typedSpawn: typedSpawn2 } = await Promise.resolve().then(() => (init_bun_deps(), exports_bun_deps));
35732
+ const cmd = ["acpx", "--cwd", worktreePath, "claude", "sessions", "close", sessionName];
35733
+ logger?.debug("parallel", "Closing stale ACP session before rectification", { sessionName });
35734
+ const proc = typedSpawn2(cmd, { stdout: "pipe", stderr: "pipe" });
35735
+ await proc.exited;
35736
+ } catch {}
35737
+ }
35616
35738
  async function rectifyConflictedStory(options) {
35617
35739
  const { storyId, workdir, config: config2, hooks, pluginRegistry, prd, eventEmitter, agentGetFn } = options;
35618
35740
  const logger = getSafeLogger();
@@ -35630,6 +35752,9 @@ async function rectifyConflictedStory(options) {
35630
35752
  } catch {}
35631
35753
  await worktreeManager.create(workdir, storyId);
35632
35754
  const worktreePath = path15.join(workdir, ".nax-wt", storyId);
35755
+ const { buildSessionName: buildSessionName2 } = await Promise.resolve().then(() => (init_adapter2(), exports_adapter));
35756
+ const staleSessionName = buildSessionName2(worktreePath, prd.feature, storyId);
35757
+ await closeStaleAcpSession(worktreePath, staleSessionName);
35633
35758
  const story = prd.userStories.find((s) => s.id === storyId);
35634
35759
  if (!story) {
35635
35760
  return { success: false, storyId, cost: 0, finalConflict: false, pipelineFailure: true };
@@ -35707,9 +35832,47 @@ async function runParallelBatch(options) {
35707
35832
  }
35708
35833
  worktreePaths.set(story.id, path16.join(workdir, ".nax-wt", story.id));
35709
35834
  }
35710
- const workerResult = await _parallelBatchDeps.executeParallelBatch(stories, workdir, config2, pipelineContext, worktreePaths, maxConcurrency, eventEmitter);
35835
+ const rootConfigPath = path16.join(workdir, ".nax", "config.json");
35836
+ const storyEffectiveConfigs = new Map;
35837
+ for (const story of stories) {
35838
+ if (story.workdir) {
35839
+ const effectiveConfig = await loadConfigForWorkdir(rootConfigPath, story.workdir);
35840
+ storyEffectiveConfigs.set(story.id, effectiveConfig);
35841
+ }
35842
+ }
35843
+ const workerResult = await _parallelBatchDeps.executeParallelBatch(stories, workdir, config2, pipelineContext, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs.size > 0 ? storyEffectiveConfigs : undefined);
35711
35844
  const batchEndMs = Date.now();
35712
- const completed = workerResult.merged;
35845
+ const completed = [];
35846
+ if (workerResult.pipelinePassed.length > 0) {
35847
+ const mergeEngine = await _parallelBatchDeps.createMergeEngine(worktreeManager);
35848
+ const successfulIds = workerResult.pipelinePassed.map((s) => s.id);
35849
+ const deps = {};
35850
+ for (const s of stories)
35851
+ deps[s.id] = s.dependencies ?? [];
35852
+ const mergeResults = await mergeEngine.mergeAll(workdir, successfulIds, deps);
35853
+ for (const mergeResult of mergeResults) {
35854
+ const story = workerResult.pipelinePassed.find((s) => s.id === mergeResult.storyId);
35855
+ if (!story)
35856
+ continue;
35857
+ if (mergeResult.success) {
35858
+ completed.push(story);
35859
+ workerResult.merged.push(story);
35860
+ logger?.info("parallel-batch", "Story merged successfully", {
35861
+ storyId: mergeResult.storyId
35862
+ });
35863
+ } else {
35864
+ workerResult.mergeConflicts.push({
35865
+ storyId: mergeResult.storyId,
35866
+ conflictFiles: mergeResult.conflictFiles || [],
35867
+ originalCost: workerResult.storyCosts.get(mergeResult.storyId) ?? 0
35868
+ });
35869
+ logger?.warn("parallel-batch", "Merge conflict \u2014 will attempt rectification", {
35870
+ storyId: mergeResult.storyId,
35871
+ conflictFiles: mergeResult.conflictFiles
35872
+ });
35873
+ }
35874
+ }
35875
+ }
35713
35876
  const failed = workerResult.failed.map((f) => ({
35714
35877
  story: f.story,
35715
35878
  pipelineResult: f.pipelineResult ?? {
@@ -35767,11 +35930,12 @@ async function runParallelBatch(options) {
35767
35930
  }
35768
35931
  var _parallelBatchDeps;
35769
35932
  var init_parallel_batch = __esm(() => {
35933
+ init_loader();
35770
35934
  init_logger2();
35771
35935
  _parallelBatchDeps = {
35772
- executeParallelBatch: async (_stories, _projectRoot, _config, _context, _worktreePaths, _maxConcurrency, _eventEmitter) => {
35936
+ executeParallelBatch: async (_stories, _projectRoot, _config, _context, _worktreePaths, _maxConcurrency, _eventEmitter, _storyEffectiveConfigs) => {
35773
35937
  const { executeParallelBatch: executeParallelBatch2 } = await Promise.resolve().then(() => (init_parallel_worker(), exports_parallel_worker));
35774
- return executeParallelBatch2(_stories, _projectRoot, _config, _context, _worktreePaths, _maxConcurrency, _eventEmitter);
35938
+ return executeParallelBatch2(_stories, _projectRoot, _config, _context, _worktreePaths, _maxConcurrency, _eventEmitter, _storyEffectiveConfigs);
35775
35939
  },
35776
35940
  createWorktreeManager: async () => {
35777
35941
  const { WorktreeManager: WorktreeManager2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
@@ -36260,15 +36424,15 @@ var _detectorDeps, WEB_DEPS, API_DEPS;
36260
36424
  var init_detector = __esm(() => {
36261
36425
  _detectorDeps = {
36262
36426
  async fileExists(path17) {
36263
- const file2 = Bun.file(path17);
36264
- return file2.exists();
36427
+ const file3 = Bun.file(path17);
36428
+ return file3.exists();
36265
36429
  },
36266
36430
  async readJson(path17) {
36267
36431
  try {
36268
- const file2 = Bun.file(path17);
36269
- if (!await file2.exists())
36432
+ const file3 = Bun.file(path17);
36433
+ if (!await file3.exists())
36270
36434
  return null;
36271
- const text = await file2.text();
36435
+ const text = await file3.text();
36272
36436
  return JSON.parse(text);
36273
36437
  } catch {
36274
36438
  return null;
@@ -36723,7 +36887,7 @@ var init_run_initialization = __esm(() => {
36723
36887
  init_errors3();
36724
36888
  init_logger2();
36725
36889
  init_prd();
36726
- init_runner2();
36890
+ init_runner3();
36727
36891
  init_git();
36728
36892
  _reconcileDeps = {
36729
36893
  getAgent,
@@ -65896,7 +66060,7 @@ var require_stack_utils = __commonJS((exports, module) => {
65896
66060
  const evalFile = match[4];
65897
66061
  const evalLine = Number(match[5]);
65898
66062
  const evalCol = Number(match[6]);
65899
- let file2 = match[7];
66063
+ let file3 = match[7];
65900
66064
  const lnum = match[8];
65901
66065
  const col = match[9];
65902
66066
  const native = match[10] === "native";
@@ -65909,17 +66073,17 @@ var require_stack_utils = __commonJS((exports, module) => {
65909
66073
  if (col) {
65910
66074
  res.column = Number(col);
65911
66075
  }
65912
- if (closeParen && file2) {
66076
+ if (closeParen && file3) {
65913
66077
  let closes = 0;
65914
- for (let i = file2.length - 1;i > 0; i--) {
65915
- if (file2.charAt(i) === ")") {
66078
+ for (let i = file3.length - 1;i > 0; i--) {
66079
+ if (file3.charAt(i) === ")") {
65916
66080
  closes++;
65917
- } else if (file2.charAt(i) === "(" && file2.charAt(i - 1) === " ") {
66081
+ } else if (file3.charAt(i) === "(" && file3.charAt(i - 1) === " ") {
65918
66082
  closes--;
65919
- if (closes === -1 && file2.charAt(i - 1) === " ") {
65920
- const before2 = file2.slice(0, i - 1);
65921
- const after2 = file2.slice(i + 1);
65922
- file2 = after2;
66083
+ if (closes === -1 && file3.charAt(i - 1) === " ") {
66084
+ const before2 = file3.slice(0, i - 1);
66085
+ const after2 = file3.slice(i + 1);
66086
+ file3 = after2;
65923
66087
  fname += ` (${before2}`;
65924
66088
  break;
65925
66089
  }
@@ -65933,7 +66097,7 @@ var require_stack_utils = __commonJS((exports, module) => {
65933
66097
  method2 = methodMatch[2];
65934
66098
  }
65935
66099
  }
65936
- setFile(res, file2, this._cwd);
66100
+ setFile(res, file3, this._cwd);
65937
66101
  if (ctor) {
65938
66102
  Object.defineProperty(res, "constructor", {
65939
66103
  value: true,
@@ -68377,8 +68541,8 @@ async function detectNode(workdir) {
68377
68541
  if (!existsSync9(pkgPath))
68378
68542
  return null;
68379
68543
  try {
68380
- const file2 = Bun.file(pkgPath);
68381
- const pkg = await file2.json();
68544
+ const file3 = Bun.file(pkgPath);
68545
+ const pkg = await file3.json();
68382
68546
  const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
68383
68547
  const notable = [
68384
68548
  ...new Set(Object.keys(allDeps).filter((dep) => NOTABLE_NODE_DEPS.some((kw) => dep === kw || dep.startsWith(`${kw}/`) || dep.includes(kw))))
@@ -68454,8 +68618,8 @@ async function detectPhp(workdir) {
68454
68618
  if (!existsSync9(composerPath))
68455
68619
  return null;
68456
68620
  try {
68457
- const file2 = Bun.file(composerPath);
68458
- const composer = await file2.json();
68621
+ const file3 = Bun.file(composerPath);
68622
+ const composer = await file3.json();
68459
68623
  const deps = Object.keys({ ...composer.require ?? {}, ...composer["require-dev"] ?? {} }).filter((d) => d !== "php").map((d) => d.split("/").pop() ?? d).slice(0, 10);
68460
68624
  return { name: composer.name, lang: "PHP", dependencies: deps };
68461
68625
  } catch {
@@ -69970,16 +70134,16 @@ async function runsListCommand(options) {
69970
70134
  return;
69971
70135
  }
69972
70136
  logger.info("cli", `Runs for ${feature}`, { count: files.length });
69973
- for (const file2 of files.sort().reverse()) {
69974
- const logPath = join15(runsDir, file2);
70137
+ for (const file3 of files.sort().reverse()) {
70138
+ const logPath = join15(runsDir, file3);
69975
70139
  const entries = await parseRunLog(logPath);
69976
70140
  const startEvent = entries.find((e) => e.message === "run.start");
69977
70141
  const completeEvent = entries.find((e) => e.message === "run.complete");
69978
70142
  if (!startEvent) {
69979
- logger.warn("cli", "Run log missing run.start event", { file: file2 });
70143
+ logger.warn("cli", "Run log missing run.start event", { file: file3 });
69980
70144
  continue;
69981
70145
  }
69982
- const runId = startEvent.data?.runId || file2.replace(".jsonl", "");
70146
+ const runId = startEvent.data?.runId || file3.replace(".jsonl", "");
69983
70147
  const startedAt = startEvent.timestamp;
69984
70148
  const status = completeEvent ? "completed" : "in-progress";
69985
70149
  const totalCost = completeEvent?.data?.totalCost || 0;
@@ -71500,8 +71664,8 @@ async function selectRunFile(runsDir) {
71500
71664
  return join39(runsDir, files[0]);
71501
71665
  }
71502
71666
  async function extractRunSummary(filePath) {
71503
- const file2 = Bun.file(filePath);
71504
- const content = await file2.text();
71667
+ const file3 = Bun.file(filePath);
71668
+ const content = await file3.text();
71505
71669
  const lines = content.trim().split(`
71506
71670
  `);
71507
71671
  let total = 0;
@@ -71581,10 +71745,10 @@ Runs:
71581
71745
  `));
71582
71746
  console.log(source_default.gray(" Timestamp Stories Duration Cost Status"));
71583
71747
  console.log(source_default.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
71584
- for (const file2 of files) {
71585
- const filePath = join40(runsDir, file2);
71748
+ for (const file3 of files) {
71749
+ const filePath = join40(runsDir, file3);
71586
71750
  const summary = await extractRunSummary(filePath);
71587
- const timestamp = file2.replace(".jsonl", "");
71751
+ const timestamp = file3.replace(".jsonl", "");
71588
71752
  const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
71589
71753
  const duration3 = summary ? formatDuration(summary.durationMs) : "?";
71590
71754
  const cost = summary ? `$${summary.totalCost.toFixed(4)}` : "$?.????";
@@ -71594,8 +71758,8 @@ Runs:
71594
71758
  console.log();
71595
71759
  }
71596
71760
  async function displayLogs(filePath, options) {
71597
- const file2 = Bun.file(filePath);
71598
- const content = await file2.text();
71761
+ const file3 = Bun.file(filePath);
71762
+ const content = await file3.text();
71599
71763
  const lines = content.trim().split(`
71600
71764
  `);
71601
71765
  const mode = options.json ? "json" : "normal";
@@ -71622,8 +71786,8 @@ async function displayLogs(filePath, options) {
71622
71786
  }
71623
71787
  async function followLogs(filePath, options) {
71624
71788
  const mode = options.json ? "json" : "normal";
71625
- const file2 = Bun.file(filePath);
71626
- const content = await file2.text();
71789
+ const file3 = Bun.file(filePath);
71790
+ const content = await file3.text();
71627
71791
  const lines = content.trim().split(`
71628
71792
  `);
71629
71793
  for (const line of lines) {
@@ -78552,8 +78716,8 @@ async function writeQueueCommand(queueFilePath, command) {
78552
78716
  throw new Error(`Unhandled queue command: ${_exhaustive}`);
78553
78717
  }
78554
78718
  }
78555
- const file2 = Bun.file(queueFilePath);
78556
- const existingContent = await file2.text().catch(() => "");
78719
+ const file3 = Bun.file(queueFilePath);
78720
+ const existingContent = await file3.text().catch(() => "");
78557
78721
  const newContent = existingContent ? `${existingContent.trimEnd()}
78558
78722
  ${commandLine}
78559
78723
  ` : `${commandLine}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.54.12",
3
+ "version": "0.55.1",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {