@nathapp/nax 0.54.12 → 0.55.0

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 +368 -219
  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.0",
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("e16efa0"))
22487
+ return "e16efa0";
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))
@@ -25732,6 +25798,7 @@ var init_autofix = __esm(() => {
25732
25798
  init_config();
25733
25799
  init_loader();
25734
25800
  init_logger2();
25801
+ init_quality();
25735
25802
  init_event_bus();
25736
25803
  autofixStage = {
25737
25804
  name: "autofix",
@@ -25768,7 +25835,12 @@ var init_autofix = __esm(() => {
25768
25835
  if (hasLintFailure && (lintFixCmd || formatFixCmd)) {
25769
25836
  if (lintFixCmd) {
25770
25837
  pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: lintFixCmd });
25771
- const lintResult = await _autofixDeps.runCommand(lintFixCmd, effectiveWorkdir);
25838
+ const lintResult = await _autofixDeps.runQualityCommand({
25839
+ commandName: "lintFix",
25840
+ command: lintFixCmd,
25841
+ workdir: effectiveWorkdir,
25842
+ storyId: ctx.story.id
25843
+ });
25772
25844
  logger.debug("autofix", `lintFix exit=${lintResult.exitCode}`, { storyId: ctx.story.id, command: lintFixCmd });
25773
25845
  if (lintResult.exitCode !== 0) {
25774
25846
  logger.warn("autofix", "lintFix command failed \u2014 may not have fixed all issues", {
@@ -25779,7 +25851,12 @@ var init_autofix = __esm(() => {
25779
25851
  }
25780
25852
  if (formatFixCmd) {
25781
25853
  pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: formatFixCmd });
25782
- const fmtResult = await _autofixDeps.runCommand(formatFixCmd, effectiveWorkdir);
25854
+ const fmtResult = await _autofixDeps.runQualityCommand({
25855
+ commandName: "formatFix",
25856
+ command: formatFixCmd,
25857
+ workdir: effectiveWorkdir,
25858
+ storyId: ctx.story.id
25859
+ });
25783
25860
  logger.debug("autofix", `formatFix exit=${fmtResult.exitCode}`, {
25784
25861
  storyId: ctx.story.id,
25785
25862
  command: formatFixCmd
@@ -25805,6 +25882,14 @@ var init_autofix = __esm(() => {
25805
25882
  if (agentFixed) {
25806
25883
  if (ctx.reviewResult)
25807
25884
  ctx.reviewResult = { ...ctx.reviewResult, success: true };
25885
+ const passedChecks = (ctx.reviewResult?.checks ?? []).filter((c) => c.success).map((c) => c.check);
25886
+ if (passedChecks.length > 0) {
25887
+ ctx.retrySkipChecks = new Set(passedChecks);
25888
+ logger.debug("autofix", "Skipping already-passed checks on retry", {
25889
+ storyId: ctx.story.id,
25890
+ skippedChecks: passedChecks
25891
+ });
25892
+ }
25808
25893
  logger.info("autofix", "Agent rectification succeeded \u2014 retrying review", { storyId: ctx.story.id });
25809
25894
  return { action: "retry", fromStage: "review" };
25810
25895
  }
@@ -25814,7 +25899,7 @@ var init_autofix = __esm(() => {
25814
25899
  };
25815
25900
  _autofixDeps = {
25816
25901
  getAgent,
25817
- runCommand,
25902
+ runQualityCommand,
25818
25903
  recheckReview,
25819
25904
  runAgentRectification,
25820
25905
  loadConfigForWorkdir
@@ -25830,8 +25915,8 @@ async function appendProgress(featureDir, storyId, status, message) {
25830
25915
  const timestamp = new Date().toISOString();
25831
25916
  const entry = `[${timestamp}] ${storyId} \u2014 ${status.toUpperCase()} \u2014 ${message}
25832
25917
  `;
25833
- const file2 = Bun.file(progressPath);
25834
- const existing = await file2.exists() ? await file2.text() : "";
25918
+ const file3 = Bun.file(progressPath);
25919
+ const existing = await file3.exists() ? await file3.text() : "";
25835
25920
  await Bun.write(progressPath, existing + entry);
25836
25921
  }
25837
25922
  var init_progress = () => {};
@@ -25890,6 +25975,7 @@ var init_completion = __esm(() => {
25890
25975
  await savePRD(ctx.prd, prdPath);
25891
25976
  const updatedCounts = countStories(ctx.prd);
25892
25977
  logger.info("completion", "Progress update", {
25978
+ storyId: ctx.story.id,
25893
25979
  completed: updatedCounts.passed + updatedCounts.failed,
25894
25980
  total: updatedCounts.total,
25895
25981
  passed: updatedCounts.passed,
@@ -26347,7 +26433,7 @@ function deriveTestPatterns(contextFiles) {
26347
26433
  async function detectTestDir(workdir) {
26348
26434
  for (const dir of COMMON_TEST_DIRS) {
26349
26435
  const fullPath = path6.join(workdir, dir);
26350
- const file2 = Bun.file(path6.join(fullPath, "."));
26436
+ const file3 = Bun.file(path6.join(fullPath, "."));
26351
26437
  try {
26352
26438
  const dirStat = await Bun.file(fullPath).exists();
26353
26439
  const proc = Bun.spawn(["test", "-d", fullPath], { stdout: "pipe", stderr: "pipe" });
@@ -26410,21 +26496,21 @@ function formatTestSummary(files, detail) {
26410
26496
  lines.push("The following tests already exist. DO NOT duplicate this coverage.");
26411
26497
  lines.push("Focus only on testing NEW behavior introduced by this story.");
26412
26498
  lines.push("");
26413
- for (const file2 of files) {
26499
+ for (const file3 of files) {
26414
26500
  switch (detail) {
26415
26501
  case "names-only":
26416
- lines.push(`- **${file2.relativePath}** (${file2.testCount} tests)`);
26502
+ lines.push(`- **${file3.relativePath}** (${file3.testCount} tests)`);
26417
26503
  break;
26418
26504
  case "names-and-counts":
26419
- lines.push(`### ${file2.relativePath} (${file2.testCount} tests)`);
26420
- for (const desc of file2.describes) {
26505
+ lines.push(`### ${file3.relativePath} (${file3.testCount} tests)`);
26506
+ for (const desc of file3.describes) {
26421
26507
  lines.push(`- ${desc.name} (${desc.tests.length} tests)`);
26422
26508
  }
26423
26509
  lines.push("");
26424
26510
  break;
26425
26511
  case "describe-blocks":
26426
- lines.push(`### ${file2.relativePath} (${file2.testCount} tests)`);
26427
- for (const desc of file2.describes) {
26512
+ lines.push(`### ${file3.relativePath} (${file3.testCount} tests)`);
26513
+ for (const desc of file3.describes) {
26428
26514
  lines.push(`- **${desc.name}** (${desc.tests.length} tests)`);
26429
26515
  for (const test of desc.tests) {
26430
26516
  lines.push(` - ${test}`);
@@ -26553,8 +26639,8 @@ function renderErrorSection(sections, byType) {
26553
26639
  const fileList = match[1].split(",").map((f) => f.trim());
26554
26640
  sections.push(`**Required files:**
26555
26641
  `);
26556
- for (const file2 of fileList) {
26557
- sections.push(`- \`${file2}\``);
26642
+ for (const file3 of fileList) {
26643
+ sections.push(`- \`${file3}\``);
26558
26644
  }
26559
26645
  sections.push(`
26560
26646
  `);
@@ -26748,24 +26834,24 @@ async function addFileElements(elements, storyContext, story) {
26748
26834
  for (const relativeFilePath of filesToLoad) {
26749
26835
  try {
26750
26836
  const absolutePath = path7.resolve(workdir, relativeFilePath);
26751
- const file2 = Bun.file(absolutePath);
26752
- if (!await file2.exists()) {
26837
+ const file3 = Bun.file(absolutePath);
26838
+ if (!await file3.exists()) {
26753
26839
  const logger = getLogger();
26754
26840
  logger.warn("context", "Relevant file not found", { filePath: relativeFilePath, storyId: story.id });
26755
26841
  continue;
26756
26842
  }
26757
- if (file2.size > MAX_FILE_SIZE_BYTES) {
26843
+ if (file3.size > MAX_FILE_SIZE_BYTES) {
26758
26844
  const logger = getLogger();
26759
26845
  logger.warn("context", "File too large for inline \u2014 using path-only", {
26760
26846
  filePath: relativeFilePath,
26761
- sizeKB: Math.round(file2.size / 1024),
26847
+ sizeKB: Math.round(file3.size / 1024),
26762
26848
  maxKB: 10,
26763
26849
  storyId: story.id
26764
26850
  });
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));
26851
+ 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
26852
  continue;
26767
26853
  }
26768
- const content = await file2.text();
26854
+ const content = await file3.text();
26769
26855
  const ext = path7.extname(relativeFilePath).slice(1) || "txt";
26770
26856
  elements.push(createFileContext(relativeFilePath, `\`\`\`${ext}
26771
26857
  // File: ${relativeFilePath}
@@ -26818,10 +26904,10 @@ function hookCtx(feature, opts) {
26818
26904
  }
26819
26905
  async function loadPackageContextMd(packageWorkdir) {
26820
26906
  const contextPath = `${packageWorkdir}/.nax/context.md`;
26821
- const file2 = Bun.file(contextPath);
26822
- if (!await file2.exists())
26907
+ const file3 = Bun.file(contextPath);
26908
+ if (!await file3.exists())
26823
26909
  return null;
26824
- return file2.text();
26910
+ return file3.text();
26825
26911
  }
26826
26912
  async function buildStoryContextFull(prd, story, config2, packageWorkdir) {
26827
26913
  try {
@@ -27073,11 +27159,11 @@ async function verifyTestWriterIsolation(workdir, beforeRef, allowedPaths = ["sr
27073
27159
  const sourceFiles = changed.filter((f) => isSourceFile(f) && !isTestFile(f));
27074
27160
  const softViolations = [];
27075
27161
  const violations = [];
27076
- for (const file2 of sourceFiles) {
27077
- if (matchesAllowedPath(file2, allowedPaths)) {
27078
- softViolations.push(file2);
27162
+ for (const file3 of sourceFiles) {
27163
+ if (matchesAllowedPath(file3, allowedPaths)) {
27164
+ softViolations.push(file3);
27079
27165
  } else {
27080
- violations.push(file2);
27166
+ violations.push(file3);
27081
27167
  }
27082
27168
  }
27083
27169
  return {
@@ -27493,9 +27579,9 @@ async function verifyAssets(workingDirectory, expectedFiles) {
27493
27579
  if (!expectedFiles || expectedFiles.length === 0)
27494
27580
  return { success: true, missingFiles: [] };
27495
27581
  const missingFiles = [];
27496
- for (const file2 of expectedFiles) {
27497
- if (!existsSync16(join22(workingDirectory, file2)))
27498
- missingFiles.push(file2);
27582
+ for (const file3 of expectedFiles) {
27583
+ if (!existsSync16(join22(workingDirectory, file3)))
27584
+ missingFiles.push(file3);
27499
27585
  }
27500
27586
  if (missingFiles.length > 0) {
27501
27587
  return {
@@ -27601,7 +27687,7 @@ function createRectificationPrompt(failures, story, config2) {
27601
27687
  const maxChars = config2?.maxFailureSummaryChars ?? 2000;
27602
27688
  const failureSummary = formatFailureSummary(failures, maxChars);
27603
27689
  const failingFiles = Array.from(new Set(failures.map((f) => f.file)));
27604
- const testCommands = failingFiles.map((file2) => ` bun test ${file2}`).join(`
27690
+ const testCommands = failingFiles.map((file3) => ` bun test ${file3}`).join(`
27605
27691
  `);
27606
27692
  return `# Rectification Required
27607
27693
 
@@ -27648,7 +27734,7 @@ function createEscalatedRectificationPrompt(failures, story, priorAttempts, orig
27648
27734
  const maxChars = config2?.maxFailureSummaryChars ?? 2000;
27649
27735
  const failureSummary = formatFailureSummary(failures, maxChars);
27650
27736
  const failingFiles = Array.from(new Set(failures.map((f) => f.file)));
27651
- const testCommands = failingFiles.map((file2) => ` bun test ${file2}`).join(`
27737
+ const testCommands = failingFiles.map((file3) => ` bun test ${file3}`).join(`
27652
27738
  `);
27653
27739
  const failingTestNames = failures.map((f) => f.testName);
27654
27740
  let failingTestsSection = "";
@@ -28359,12 +28445,12 @@ async function loadOverride(role, workdir, config2) {
28359
28445
  return null;
28360
28446
  }
28361
28447
  const absolutePath = join23(workdir, overridePath);
28362
- const file2 = Bun.file(absolutePath);
28363
- if (!await file2.exists()) {
28448
+ const file3 = Bun.file(absolutePath);
28449
+ if (!await file3.exists()) {
28364
28450
  return null;
28365
28451
  }
28366
28452
  try {
28367
- return await file2.text();
28453
+ return await file3.text();
28368
28454
  } catch (err) {
28369
28455
  throw new Error(`Cannot read prompt override for role "${role}" at "${absolutePath}": ${err instanceof Error ? err.message : String(err)}`);
28370
28456
  }
@@ -28486,9 +28572,9 @@ ${this._contextMd}
28486
28572
  }
28487
28573
  if (this._overridePath) {
28488
28574
  try {
28489
- const file2 = Bun.file(this._overridePath);
28490
- if (await file2.exists()) {
28491
- return await file2.text();
28575
+ const file3 = Bun.file(this._overridePath);
28576
+ if (await file3.exists()) {
28577
+ return await file3.text();
28492
28578
  }
28493
28579
  } catch {}
28494
28580
  }
@@ -28824,14 +28910,14 @@ async function readVerdict(workdir) {
28824
28910
  const logger = getLogger();
28825
28911
  const verdictPath = path9.join(workdir, VERDICT_FILE);
28826
28912
  try {
28827
- const file2 = Bun.file(verdictPath);
28828
- const exists = await file2.exists();
28913
+ const file3 = Bun.file(verdictPath);
28914
+ const exists = await file3.exists();
28829
28915
  if (!exists) {
28830
28916
  return null;
28831
28917
  }
28832
28918
  let rawText;
28833
28919
  try {
28834
- rawText = await file2.text();
28920
+ rawText = await file3.text();
28835
28921
  } catch (readErr) {
28836
28922
  logger.warn("tdd", "Failed to read verifier verdict file", {
28837
28923
  path: verdictPath,
@@ -29756,8 +29842,8 @@ async function readQueueFile(workdir) {
29756
29842
  const processingPath = path10.join(workdir, ".queue.txt.processing");
29757
29843
  const logger = getSafeLogger4();
29758
29844
  try {
29759
- const file2 = Bun.file(queuePath);
29760
- const exists = await file2.exists();
29845
+ const file3 = Bun.file(queuePath);
29846
+ const exists = await file3.exists();
29761
29847
  if (!exists) {
29762
29848
  return [];
29763
29849
  }
@@ -29781,8 +29867,8 @@ async function clearQueueFile(workdir) {
29781
29867
  const processingPath = path10.join(workdir, ".queue.txt.processing");
29782
29868
  const logger = getSafeLogger4();
29783
29869
  try {
29784
- const file2 = Bun.file(processingPath);
29785
- const exists = await file2.exists();
29870
+ const file3 = Bun.file(processingPath);
29871
+ const exists = await file3.exists();
29786
29872
  if (exists) {
29787
29873
  await Bun.spawn(["rm", processingPath], { stdout: "pipe" }).exited;
29788
29874
  }
@@ -30378,8 +30464,8 @@ async function importGrepFallback(sourceFiles, workdir, testFilePatterns) {
30378
30464
  const testFilePaths = [];
30379
30465
  for (const pattern of testFilePatterns) {
30380
30466
  const glob = _bunDeps.glob(pattern);
30381
- for await (const file2 of glob.scan(workdir)) {
30382
- testFilePaths.push(`${workdir}/${file2}`);
30467
+ for await (const file3 of glob.scan(workdir)) {
30468
+ testFilePaths.push(`${workdir}/${file3}`);
30383
30469
  }
30384
30470
  }
30385
30471
  const matched = [];
@@ -31289,8 +31375,8 @@ function generateContextTemplate(scan) {
31289
31375
  lines.push(`## Project Structure
31290
31376
  `);
31291
31377
  lines.push("```");
31292
- for (const file2 of scan.fileTree.slice(0, 20)) {
31293
- lines.push(file2);
31378
+ for (const file3 of scan.fileTree.slice(0, 20)) {
31379
+ lines.push(file3);
31294
31380
  }
31295
31381
  if (scan.fileTree.length > 20) {
31296
31382
  lines.push(`... and ${scan.fileTree.length - 20} more files`);
@@ -32109,8 +32195,8 @@ async function checkStaleLock(workdir) {
32109
32195
  };
32110
32196
  }
32111
32197
  try {
32112
- const file2 = Bun.file(lockPath);
32113
- const content = await file2.text();
32198
+ const file3 = Bun.file(lockPath);
32199
+ const content = await file3.text();
32114
32200
  const lockData = JSON.parse(content);
32115
32201
  let lockTimeMs;
32116
32202
  if (lockData.timestamp) {
@@ -32396,8 +32482,8 @@ async function checkGitignoreCoversNax(workdir) {
32396
32482
  message: ".gitignore not found"
32397
32483
  };
32398
32484
  }
32399
- const file2 = Bun.file(gitignorePath);
32400
- const content = await file2.text();
32485
+ const file3 = Bun.file(gitignorePath);
32486
+ const content = await file3.text();
32401
32487
  const patterns = [
32402
32488
  "nax.lock",
32403
32489
  ".nax/**/runs/",
@@ -33081,14 +33167,14 @@ async function fireHook(config2, event, ctx, workdir) {
33081
33167
  }
33082
33168
  }
33083
33169
  var DEFAULT_TIMEOUT = 5000;
33084
- var init_runner3 = __esm(() => {
33170
+ var init_runner4 = __esm(() => {
33085
33171
  init_logger2();
33086
33172
  init_json_file();
33087
33173
  });
33088
33174
 
33089
33175
  // src/hooks/index.ts
33090
33176
  var init_hooks = __esm(() => {
33091
- init_runner3();
33177
+ init_runner4();
33092
33178
  });
33093
33179
 
33094
33180
  // src/execution/crash-heartbeat.ts
@@ -33982,7 +34068,7 @@ async function handleRunCompletion(options) {
33982
34068
  }
33983
34069
  var _runCompletionDeps;
33984
34070
  var init_run_completion = __esm(() => {
33985
- init_runner3();
34071
+ init_runner4();
33986
34072
  init_logger2();
33987
34073
  init_metrics();
33988
34074
  init_event_bus();
@@ -35101,13 +35187,23 @@ __export(exports_parallel_worker, {
35101
35187
  async function executeStoryInWorktree(story, worktreePath, context, routing, eventEmitter) {
35102
35188
  const logger = getSafeLogger();
35103
35189
  try {
35190
+ let storyGitRef;
35191
+ if (story.storyGitRef && await isGitRefValid(worktreePath, story.storyGitRef)) {
35192
+ storyGitRef = story.storyGitRef;
35193
+ } else {
35194
+ storyGitRef = await captureGitRef(worktreePath);
35195
+ if (storyGitRef) {
35196
+ story.storyGitRef = storyGitRef;
35197
+ }
35198
+ }
35104
35199
  const pipelineContext = {
35105
35200
  ...context,
35106
35201
  effectiveConfig: context.effectiveConfig ?? context.config,
35107
35202
  story,
35108
35203
  stories: [story],
35109
35204
  workdir: worktreePath,
35110
- routing
35205
+ routing,
35206
+ storyGitRef: storyGitRef ?? undefined
35111
35207
  };
35112
35208
  logger?.debug("parallel", "Executing story in worktree", {
35113
35209
  storyId: story.id,
@@ -35183,6 +35279,7 @@ var init_parallel_worker = __esm(() => {
35183
35279
  init_runner();
35184
35280
  init_stages();
35185
35281
  init_routing();
35282
+ init_git();
35186
35283
  });
35187
35284
 
35188
35285
  // src/worktree/manager.ts
@@ -35613,6 +35710,16 @@ __export(exports_merge_conflict_rectify, {
35613
35710
  rectifyConflictedStory: () => rectifyConflictedStory
35614
35711
  });
35615
35712
  import path15 from "path";
35713
+ async function closeStaleAcpSession(worktreePath, sessionName) {
35714
+ const logger = getSafeLogger();
35715
+ try {
35716
+ const { typedSpawn: typedSpawn2 } = await Promise.resolve().then(() => (init_bun_deps(), exports_bun_deps));
35717
+ const cmd = ["acpx", "--cwd", worktreePath, "claude", "sessions", "close", sessionName];
35718
+ logger?.debug("parallel", "Closing stale ACP session before rectification", { sessionName });
35719
+ const proc = typedSpawn2(cmd, { stdout: "pipe", stderr: "pipe" });
35720
+ await proc.exited;
35721
+ } catch {}
35722
+ }
35616
35723
  async function rectifyConflictedStory(options) {
35617
35724
  const { storyId, workdir, config: config2, hooks, pluginRegistry, prd, eventEmitter, agentGetFn } = options;
35618
35725
  const logger = getSafeLogger();
@@ -35630,6 +35737,9 @@ async function rectifyConflictedStory(options) {
35630
35737
  } catch {}
35631
35738
  await worktreeManager.create(workdir, storyId);
35632
35739
  const worktreePath = path15.join(workdir, ".nax-wt", storyId);
35740
+ const { buildSessionName: buildSessionName2 } = await Promise.resolve().then(() => (init_adapter2(), exports_adapter));
35741
+ const staleSessionName = buildSessionName2(worktreePath, prd.feature, storyId);
35742
+ await closeStaleAcpSession(worktreePath, staleSessionName);
35633
35743
  const story = prd.userStories.find((s) => s.id === storyId);
35634
35744
  if (!story) {
35635
35745
  return { success: false, storyId, cost: 0, finalConflict: false, pipelineFailure: true };
@@ -35707,9 +35817,47 @@ async function runParallelBatch(options) {
35707
35817
  }
35708
35818
  worktreePaths.set(story.id, path16.join(workdir, ".nax-wt", story.id));
35709
35819
  }
35710
- const workerResult = await _parallelBatchDeps.executeParallelBatch(stories, workdir, config2, pipelineContext, worktreePaths, maxConcurrency, eventEmitter);
35820
+ const rootConfigPath = path16.join(workdir, ".nax", "config.json");
35821
+ const storyEffectiveConfigs = new Map;
35822
+ for (const story of stories) {
35823
+ if (story.workdir) {
35824
+ const effectiveConfig = await loadConfigForWorkdir(rootConfigPath, story.workdir);
35825
+ storyEffectiveConfigs.set(story.id, effectiveConfig);
35826
+ }
35827
+ }
35828
+ const workerResult = await _parallelBatchDeps.executeParallelBatch(stories, workdir, config2, pipelineContext, worktreePaths, maxConcurrency, eventEmitter, storyEffectiveConfigs.size > 0 ? storyEffectiveConfigs : undefined);
35711
35829
  const batchEndMs = Date.now();
35712
- const completed = workerResult.merged;
35830
+ const completed = [];
35831
+ if (workerResult.pipelinePassed.length > 0) {
35832
+ const mergeEngine = await _parallelBatchDeps.createMergeEngine(worktreeManager);
35833
+ const successfulIds = workerResult.pipelinePassed.map((s) => s.id);
35834
+ const deps = {};
35835
+ for (const s of stories)
35836
+ deps[s.id] = s.dependencies ?? [];
35837
+ const mergeResults = await mergeEngine.mergeAll(workdir, successfulIds, deps);
35838
+ for (const mergeResult of mergeResults) {
35839
+ const story = workerResult.pipelinePassed.find((s) => s.id === mergeResult.storyId);
35840
+ if (!story)
35841
+ continue;
35842
+ if (mergeResult.success) {
35843
+ completed.push(story);
35844
+ workerResult.merged.push(story);
35845
+ logger?.info("parallel-batch", "Story merged successfully", {
35846
+ storyId: mergeResult.storyId
35847
+ });
35848
+ } else {
35849
+ workerResult.mergeConflicts.push({
35850
+ storyId: mergeResult.storyId,
35851
+ conflictFiles: mergeResult.conflictFiles || [],
35852
+ originalCost: workerResult.storyCosts.get(mergeResult.storyId) ?? 0
35853
+ });
35854
+ logger?.warn("parallel-batch", "Merge conflict \u2014 will attempt rectification", {
35855
+ storyId: mergeResult.storyId,
35856
+ conflictFiles: mergeResult.conflictFiles
35857
+ });
35858
+ }
35859
+ }
35860
+ }
35713
35861
  const failed = workerResult.failed.map((f) => ({
35714
35862
  story: f.story,
35715
35863
  pipelineResult: f.pipelineResult ?? {
@@ -35767,11 +35915,12 @@ async function runParallelBatch(options) {
35767
35915
  }
35768
35916
  var _parallelBatchDeps;
35769
35917
  var init_parallel_batch = __esm(() => {
35918
+ init_loader();
35770
35919
  init_logger2();
35771
35920
  _parallelBatchDeps = {
35772
- executeParallelBatch: async (_stories, _projectRoot, _config, _context, _worktreePaths, _maxConcurrency, _eventEmitter) => {
35921
+ executeParallelBatch: async (_stories, _projectRoot, _config, _context, _worktreePaths, _maxConcurrency, _eventEmitter, _storyEffectiveConfigs) => {
35773
35922
  const { executeParallelBatch: executeParallelBatch2 } = await Promise.resolve().then(() => (init_parallel_worker(), exports_parallel_worker));
35774
- return executeParallelBatch2(_stories, _projectRoot, _config, _context, _worktreePaths, _maxConcurrency, _eventEmitter);
35923
+ return executeParallelBatch2(_stories, _projectRoot, _config, _context, _worktreePaths, _maxConcurrency, _eventEmitter, _storyEffectiveConfigs);
35775
35924
  },
35776
35925
  createWorktreeManager: async () => {
35777
35926
  const { WorktreeManager: WorktreeManager2 } = await Promise.resolve().then(() => (init_manager(), exports_manager));
@@ -36260,15 +36409,15 @@ var _detectorDeps, WEB_DEPS, API_DEPS;
36260
36409
  var init_detector = __esm(() => {
36261
36410
  _detectorDeps = {
36262
36411
  async fileExists(path17) {
36263
- const file2 = Bun.file(path17);
36264
- return file2.exists();
36412
+ const file3 = Bun.file(path17);
36413
+ return file3.exists();
36265
36414
  },
36266
36415
  async readJson(path17) {
36267
36416
  try {
36268
- const file2 = Bun.file(path17);
36269
- if (!await file2.exists())
36417
+ const file3 = Bun.file(path17);
36418
+ if (!await file3.exists())
36270
36419
  return null;
36271
- const text = await file2.text();
36420
+ const text = await file3.text();
36272
36421
  return JSON.parse(text);
36273
36422
  } catch {
36274
36423
  return null;
@@ -36723,7 +36872,7 @@ var init_run_initialization = __esm(() => {
36723
36872
  init_errors3();
36724
36873
  init_logger2();
36725
36874
  init_prd();
36726
- init_runner2();
36875
+ init_runner3();
36727
36876
  init_git();
36728
36877
  _reconcileDeps = {
36729
36878
  getAgent,
@@ -65896,7 +66045,7 @@ var require_stack_utils = __commonJS((exports, module) => {
65896
66045
  const evalFile = match[4];
65897
66046
  const evalLine = Number(match[5]);
65898
66047
  const evalCol = Number(match[6]);
65899
- let file2 = match[7];
66048
+ let file3 = match[7];
65900
66049
  const lnum = match[8];
65901
66050
  const col = match[9];
65902
66051
  const native = match[10] === "native";
@@ -65909,17 +66058,17 @@ var require_stack_utils = __commonJS((exports, module) => {
65909
66058
  if (col) {
65910
66059
  res.column = Number(col);
65911
66060
  }
65912
- if (closeParen && file2) {
66061
+ if (closeParen && file3) {
65913
66062
  let closes = 0;
65914
- for (let i = file2.length - 1;i > 0; i--) {
65915
- if (file2.charAt(i) === ")") {
66063
+ for (let i = file3.length - 1;i > 0; i--) {
66064
+ if (file3.charAt(i) === ")") {
65916
66065
  closes++;
65917
- } else if (file2.charAt(i) === "(" && file2.charAt(i - 1) === " ") {
66066
+ } else if (file3.charAt(i) === "(" && file3.charAt(i - 1) === " ") {
65918
66067
  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;
66068
+ if (closes === -1 && file3.charAt(i - 1) === " ") {
66069
+ const before2 = file3.slice(0, i - 1);
66070
+ const after2 = file3.slice(i + 1);
66071
+ file3 = after2;
65923
66072
  fname += ` (${before2}`;
65924
66073
  break;
65925
66074
  }
@@ -65933,7 +66082,7 @@ var require_stack_utils = __commonJS((exports, module) => {
65933
66082
  method2 = methodMatch[2];
65934
66083
  }
65935
66084
  }
65936
- setFile(res, file2, this._cwd);
66085
+ setFile(res, file3, this._cwd);
65937
66086
  if (ctor) {
65938
66087
  Object.defineProperty(res, "constructor", {
65939
66088
  value: true,
@@ -68377,8 +68526,8 @@ async function detectNode(workdir) {
68377
68526
  if (!existsSync9(pkgPath))
68378
68527
  return null;
68379
68528
  try {
68380
- const file2 = Bun.file(pkgPath);
68381
- const pkg = await file2.json();
68529
+ const file3 = Bun.file(pkgPath);
68530
+ const pkg = await file3.json();
68382
68531
  const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
68383
68532
  const notable = [
68384
68533
  ...new Set(Object.keys(allDeps).filter((dep) => NOTABLE_NODE_DEPS.some((kw) => dep === kw || dep.startsWith(`${kw}/`) || dep.includes(kw))))
@@ -68454,8 +68603,8 @@ async function detectPhp(workdir) {
68454
68603
  if (!existsSync9(composerPath))
68455
68604
  return null;
68456
68605
  try {
68457
- const file2 = Bun.file(composerPath);
68458
- const composer = await file2.json();
68606
+ const file3 = Bun.file(composerPath);
68607
+ const composer = await file3.json();
68459
68608
  const deps = Object.keys({ ...composer.require ?? {}, ...composer["require-dev"] ?? {} }).filter((d) => d !== "php").map((d) => d.split("/").pop() ?? d).slice(0, 10);
68460
68609
  return { name: composer.name, lang: "PHP", dependencies: deps };
68461
68610
  } catch {
@@ -69970,16 +70119,16 @@ async function runsListCommand(options) {
69970
70119
  return;
69971
70120
  }
69972
70121
  logger.info("cli", `Runs for ${feature}`, { count: files.length });
69973
- for (const file2 of files.sort().reverse()) {
69974
- const logPath = join15(runsDir, file2);
70122
+ for (const file3 of files.sort().reverse()) {
70123
+ const logPath = join15(runsDir, file3);
69975
70124
  const entries = await parseRunLog(logPath);
69976
70125
  const startEvent = entries.find((e) => e.message === "run.start");
69977
70126
  const completeEvent = entries.find((e) => e.message === "run.complete");
69978
70127
  if (!startEvent) {
69979
- logger.warn("cli", "Run log missing run.start event", { file: file2 });
70128
+ logger.warn("cli", "Run log missing run.start event", { file: file3 });
69980
70129
  continue;
69981
70130
  }
69982
- const runId = startEvent.data?.runId || file2.replace(".jsonl", "");
70131
+ const runId = startEvent.data?.runId || file3.replace(".jsonl", "");
69983
70132
  const startedAt = startEvent.timestamp;
69984
70133
  const status = completeEvent ? "completed" : "in-progress";
69985
70134
  const totalCost = completeEvent?.data?.totalCost || 0;
@@ -71500,8 +71649,8 @@ async function selectRunFile(runsDir) {
71500
71649
  return join39(runsDir, files[0]);
71501
71650
  }
71502
71651
  async function extractRunSummary(filePath) {
71503
- const file2 = Bun.file(filePath);
71504
- const content = await file2.text();
71652
+ const file3 = Bun.file(filePath);
71653
+ const content = await file3.text();
71505
71654
  const lines = content.trim().split(`
71506
71655
  `);
71507
71656
  let total = 0;
@@ -71581,10 +71730,10 @@ Runs:
71581
71730
  `));
71582
71731
  console.log(source_default.gray(" Timestamp Stories Duration Cost Status"));
71583
71732
  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);
71733
+ for (const file3 of files) {
71734
+ const filePath = join40(runsDir, file3);
71586
71735
  const summary = await extractRunSummary(filePath);
71587
- const timestamp = file2.replace(".jsonl", "");
71736
+ const timestamp = file3.replace(".jsonl", "");
71588
71737
  const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
71589
71738
  const duration3 = summary ? formatDuration(summary.durationMs) : "?";
71590
71739
  const cost = summary ? `$${summary.totalCost.toFixed(4)}` : "$?.????";
@@ -71594,8 +71743,8 @@ Runs:
71594
71743
  console.log();
71595
71744
  }
71596
71745
  async function displayLogs(filePath, options) {
71597
- const file2 = Bun.file(filePath);
71598
- const content = await file2.text();
71746
+ const file3 = Bun.file(filePath);
71747
+ const content = await file3.text();
71599
71748
  const lines = content.trim().split(`
71600
71749
  `);
71601
71750
  const mode = options.json ? "json" : "normal";
@@ -71622,8 +71771,8 @@ async function displayLogs(filePath, options) {
71622
71771
  }
71623
71772
  async function followLogs(filePath, options) {
71624
71773
  const mode = options.json ? "json" : "normal";
71625
- const file2 = Bun.file(filePath);
71626
- const content = await file2.text();
71774
+ const file3 = Bun.file(filePath);
71775
+ const content = await file3.text();
71627
71776
  const lines = content.trim().split(`
71628
71777
  `);
71629
71778
  for (const line of lines) {
@@ -78552,8 +78701,8 @@ async function writeQueueCommand(queueFilePath, command) {
78552
78701
  throw new Error(`Unhandled queue command: ${_exhaustive}`);
78553
78702
  }
78554
78703
  }
78555
- const file2 = Bun.file(queueFilePath);
78556
- const existingContent = await file2.text().catch(() => "");
78704
+ const file3 = Bun.file(queueFilePath);
78705
+ const existingContent = await file3.text().catch(() => "");
78557
78706
  const newContent = existingContent ? `${existingContent.trimEnd()}
78558
78707
  ${commandLine}
78559
78708
  ` : `${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.0",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {