@nathapp/nax 0.49.3 → 0.49.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/nax.js CHANGED
@@ -3667,6 +3667,16 @@ function buildAllowedEnv(options) {
3667
3667
  async function executeOnce(binary, options, pidRegistry) {
3668
3668
  const cmd = _runOnceDeps.buildCmd(binary, options);
3669
3669
  const startTime = Date.now();
3670
+ if (options.sessionRole || options.acpSessionName || options.keepSessionOpen) {
3671
+ const logger2 = getLogger();
3672
+ logger2.debug("agent", "CLI mode: session options received (unused)", {
3673
+ sessionRole: options.sessionRole,
3674
+ acpSessionName: options.acpSessionName,
3675
+ keepSessionOpen: options.keepSessionOpen,
3676
+ featureName: options.featureName,
3677
+ storyId: options.storyId
3678
+ });
3679
+ }
3670
3680
  const proc = Bun.spawn(cmd, {
3671
3681
  cwd: options.workdir,
3672
3682
  stdout: "pipe",
@@ -19276,7 +19286,6 @@ class SpawnAcpClient {
19276
19286
  model;
19277
19287
  cwd;
19278
19288
  timeoutSeconds;
19279
- permissionMode;
19280
19289
  env;
19281
19290
  pidRegistry;
19282
19291
  constructor(cmdStr, cwd, timeoutSeconds, pidRegistry) {
@@ -19290,7 +19299,6 @@ class SpawnAcpClient {
19290
19299
  this.agentName = lastToken;
19291
19300
  this.cwd = cwd || process.cwd();
19292
19301
  this.timeoutSeconds = timeoutSeconds || 1800;
19293
- this.permissionMode = "approve-reads";
19294
19302
  this.env = buildAllowedEnv2();
19295
19303
  this.pidRegistry = pidRegistry;
19296
19304
  }
@@ -19432,7 +19440,13 @@ async function closeAcpSession(session) {
19432
19440
  function acpSessionsPath(workdir, featureName) {
19433
19441
  return join3(workdir, "nax", "features", featureName, "acp-sessions.json");
19434
19442
  }
19435
- async function saveAcpSession(workdir, featureName, storyId, sessionName) {
19443
+ function sidecarSessionName(entry) {
19444
+ return typeof entry === "string" ? entry : entry.sessionName;
19445
+ }
19446
+ function sidecarAgentName(entry) {
19447
+ return typeof entry === "string" ? "claude" : entry.agentName;
19448
+ }
19449
+ async function saveAcpSession(workdir, featureName, storyId, sessionName, agentName = "claude") {
19436
19450
  try {
19437
19451
  const path = acpSessionsPath(workdir, featureName);
19438
19452
  let data = {};
@@ -19440,7 +19454,7 @@ async function saveAcpSession(workdir, featureName, storyId, sessionName) {
19440
19454
  const existing = await Bun.file(path).text();
19441
19455
  data = JSON.parse(existing);
19442
19456
  } catch {}
19443
- data[storyId] = sessionName;
19457
+ data[storyId] = { sessionName, agentName };
19444
19458
  await Bun.write(path, JSON.stringify(data, null, 2));
19445
19459
  } catch (err) {
19446
19460
  getSafeLogger()?.warn("acp-adapter", "Failed to save session to sidecar", { error: String(err) });
@@ -19467,7 +19481,8 @@ async function readAcpSession(workdir, featureName, storyId) {
19467
19481
  const path = acpSessionsPath(workdir, featureName);
19468
19482
  const existing = await Bun.file(path).text();
19469
19483
  const data = JSON.parse(existing);
19470
- return data[storyId] ?? null;
19484
+ const entry = data[storyId];
19485
+ return entry ? sidecarSessionName(entry) : null;
19471
19486
  } catch {
19472
19487
  return null;
19473
19488
  }
@@ -19486,24 +19501,34 @@ async function sweepFeatureSessions(workdir, featureName) {
19486
19501
  return;
19487
19502
  const logger = getSafeLogger();
19488
19503
  logger?.info("acp-adapter", `[sweep] Closing ${entries.length} open sessions for feature: ${featureName}`);
19489
- const cmdStr = "acpx claude";
19490
- const client = _acpAdapterDeps.createClient(cmdStr, workdir);
19491
- try {
19492
- await client.start();
19493
- for (const [, sessionName] of entries) {
19494
- try {
19495
- if (client.loadSession) {
19496
- const session = await client.loadSession(sessionName, "claude", "approve-reads");
19497
- if (session) {
19498
- await session.close().catch(() => {});
19504
+ const byAgent = new Map;
19505
+ for (const [, entry] of entries) {
19506
+ const agent = sidecarAgentName(entry);
19507
+ const name = sidecarSessionName(entry);
19508
+ if (!byAgent.has(agent))
19509
+ byAgent.set(agent, []);
19510
+ byAgent.get(agent)?.push(name);
19511
+ }
19512
+ for (const [agentName, sessionNames] of byAgent) {
19513
+ const cmdStr = `acpx ${agentName}`;
19514
+ const client = _acpAdapterDeps.createClient(cmdStr, workdir);
19515
+ try {
19516
+ await client.start();
19517
+ for (const sessionName of sessionNames) {
19518
+ try {
19519
+ if (client.loadSession) {
19520
+ const session = await client.loadSession(sessionName, agentName, "approve-reads");
19521
+ if (session) {
19522
+ await session.close().catch(() => {});
19523
+ }
19499
19524
  }
19525
+ } catch (err) {
19526
+ logger?.warn("acp-adapter", `[sweep] Failed to close session ${sessionName}`, { error: String(err) });
19500
19527
  }
19501
- } catch (err) {
19502
- logger?.warn("acp-adapter", `[sweep] Failed to close session ${sessionName}`, { error: String(err) });
19503
19528
  }
19529
+ } finally {
19530
+ await client.close().catch(() => {});
19504
19531
  }
19505
- } finally {
19506
- await client.close().catch(() => {});
19507
19532
  }
19508
19533
  try {
19509
19534
  await Bun.write(path, JSON.stringify({}, null, 2));
@@ -19644,7 +19669,7 @@ class AcpAgentAdapter {
19644
19669
  });
19645
19670
  const session = await ensureAcpSession(client, sessionName, this.name, permissionMode);
19646
19671
  if (options.featureName && options.storyId) {
19647
- await saveAcpSession(options.workdir, options.featureName, options.storyId, sessionName);
19672
+ await saveAcpSession(options.workdir, options.featureName, options.storyId, sessionName, this.name);
19648
19673
  }
19649
19674
  let lastResponse = null;
19650
19675
  let timedOut = false;
@@ -19702,13 +19727,15 @@ class AcpAgentAdapter {
19702
19727
  }
19703
19728
  runState.succeeded = !timedOut && lastResponse?.stopReason === "end_turn";
19704
19729
  } finally {
19705
- if (runState.succeeded) {
19730
+ if (runState.succeeded && !options.keepSessionOpen) {
19706
19731
  await closeAcpSession(session);
19707
19732
  if (options.featureName && options.storyId) {
19708
19733
  await clearAcpSession(options.workdir, options.featureName, options.storyId);
19709
19734
  }
19710
- } else {
19735
+ } else if (!runState.succeeded) {
19711
19736
  getSafeLogger()?.info("acp-adapter", "Keeping session open for retry", { sessionName });
19737
+ } else {
19738
+ getSafeLogger()?.debug("acp-adapter", "Keeping session open (keepSessionOpen=true)", { sessionName });
19712
19739
  }
19713
19740
  await client.close().catch(() => {});
19714
19741
  }
@@ -20726,6 +20753,18 @@ function mergePackageConfig(root, packageOverride) {
20726
20753
  ...packageOverride.review,
20727
20754
  commands: {
20728
20755
  ...root.review.commands,
20756
+ ...packageOverride.quality?.commands?.lint !== undefined && {
20757
+ lint: packageOverride.quality.commands.lint
20758
+ },
20759
+ ...packageOverride.quality?.commands?.lintFix !== undefined && {
20760
+ lintFix: packageOverride.quality.commands.lintFix
20761
+ },
20762
+ ...packageOverride.quality?.commands?.typecheck !== undefined && {
20763
+ typecheck: packageOverride.quality.commands.typecheck
20764
+ },
20765
+ ...packageOverride.quality?.commands?.test !== undefined && {
20766
+ test: packageOverride.quality.commands.test
20767
+ },
20729
20768
  ...packageOverride.review?.commands
20730
20769
  }
20731
20770
  },
@@ -22250,7 +22289,7 @@ var package_default;
22250
22289
  var init_package = __esm(() => {
22251
22290
  package_default = {
22252
22291
  name: "@nathapp/nax",
22253
- version: "0.49.3",
22292
+ version: "0.49.6",
22254
22293
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22255
22294
  type: "module",
22256
22295
  bin: {
@@ -22323,8 +22362,8 @@ var init_version = __esm(() => {
22323
22362
  NAX_VERSION = package_default.version;
22324
22363
  NAX_COMMIT = (() => {
22325
22364
  try {
22326
- if (/^[0-9a-f]{6,10}$/.test("30ff375"))
22327
- return "30ff375";
22365
+ if (/^[0-9a-f]{6,10}$/.test("a1f7e2d"))
22366
+ return "a1f7e2d";
22328
22367
  } catch {}
22329
22368
  try {
22330
22369
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -23309,6 +23348,7 @@ class WebhookInteractionPlugin {
23309
23348
  server = null;
23310
23349
  serverStartPromise = null;
23311
23350
  pendingResponses = new Map;
23351
+ receiveCallbacks = new Map;
23312
23352
  async init(config2) {
23313
23353
  const cfg = WebhookConfigSchema.parse(config2);
23314
23354
  this.config = {
@@ -23358,27 +23398,39 @@ class WebhookInteractionPlugin {
23358
23398
  }
23359
23399
  async receive(requestId, timeout = 60000) {
23360
23400
  await this.startServer();
23361
- const startTime = Date.now();
23362
- let backoffMs = 100;
23363
- const maxBackoffMs = 2000;
23364
- while (Date.now() - startTime < timeout) {
23365
- const response = this.pendingResponses.get(requestId);
23366
- if (response) {
23367
- this.pendingResponses.delete(requestId);
23368
- return response;
23369
- }
23370
- await _webhookPluginDeps.sleep(backoffMs);
23371
- backoffMs = Math.min(backoffMs * 2, maxBackoffMs);
23401
+ const early = this.pendingResponses.get(requestId);
23402
+ if (early) {
23403
+ this.pendingResponses.delete(requestId);
23404
+ return early;
23372
23405
  }
23373
- return {
23374
- requestId,
23375
- action: "skip",
23376
- respondedBy: "timeout",
23377
- respondedAt: Date.now()
23378
- };
23406
+ return new Promise((resolve7) => {
23407
+ const timer = setTimeout(() => {
23408
+ this.receiveCallbacks.delete(requestId);
23409
+ resolve7({
23410
+ requestId,
23411
+ action: "skip",
23412
+ respondedBy: "timeout",
23413
+ respondedAt: Date.now()
23414
+ });
23415
+ }, timeout);
23416
+ this.receiveCallbacks.set(requestId, (response) => {
23417
+ clearTimeout(timer);
23418
+ this.receiveCallbacks.delete(requestId);
23419
+ resolve7(response);
23420
+ });
23421
+ });
23379
23422
  }
23380
23423
  async cancel(requestId) {
23381
23424
  this.pendingResponses.delete(requestId);
23425
+ this.receiveCallbacks.delete(requestId);
23426
+ }
23427
+ deliverResponse(requestId, response) {
23428
+ const cb = this.receiveCallbacks.get(requestId);
23429
+ if (cb) {
23430
+ cb(response);
23431
+ } else {
23432
+ this.pendingResponses.set(requestId, response);
23433
+ }
23382
23434
  }
23383
23435
  async startServer() {
23384
23436
  if (this.server)
@@ -23431,7 +23483,7 @@ class WebhookInteractionPlugin {
23431
23483
  try {
23432
23484
  const parsed = JSON.parse(body);
23433
23485
  const response = InteractionResponseSchema.parse(parsed);
23434
- this.pendingResponses.set(requestId, response);
23486
+ this.deliverResponse(requestId, response);
23435
23487
  } catch {
23436
23488
  return new Response("Bad Request: Invalid response format", { status: 400 });
23437
23489
  }
@@ -23439,7 +23491,7 @@ class WebhookInteractionPlugin {
23439
23491
  try {
23440
23492
  const parsed = await req.json();
23441
23493
  const response = InteractionResponseSchema.parse(parsed);
23442
- this.pendingResponses.set(requestId, response);
23494
+ this.deliverResponse(requestId, response);
23443
23495
  } catch {
23444
23496
  return new Response("Bad Request: Invalid response format", { status: 400 });
23445
23497
  }
@@ -23466,12 +23518,9 @@ class WebhookInteractionPlugin {
23466
23518
  }
23467
23519
  }
23468
23520
  }
23469
- var _webhookPluginDeps, WebhookConfigSchema, InteractionResponseSchema;
23521
+ var WebhookConfigSchema, InteractionResponseSchema;
23470
23522
  var init_webhook = __esm(() => {
23471
23523
  init_zod();
23472
- _webhookPluginDeps = {
23473
- sleep: (ms) => Bun.sleep(ms)
23474
- };
23475
23524
  WebhookConfigSchema = exports_external.object({
23476
23525
  url: exports_external.string().url().optional(),
23477
23526
  callbackPort: exports_external.number().int().min(1024).max(65535).optional(),
@@ -26037,13 +26086,13 @@ function isSourceFile(filePath) {
26037
26086
  return SRC_PATTERNS.some((pattern) => pattern.test(filePath));
26038
26087
  }
26039
26088
  async function getChangedFiles2(workdir, fromRef = "HEAD") {
26040
- const proc = Bun.spawn(["git", "diff", "--name-only", fromRef], {
26089
+ const proc = _isolationDeps.spawn(["git", "diff", "--name-only", fromRef], {
26041
26090
  cwd: workdir,
26042
26091
  stdout: "pipe",
26043
26092
  stderr: "pipe"
26044
26093
  });
26094
+ const output = await Bun.readableStreamToText(proc.stdout);
26045
26095
  await proc.exited;
26046
- const output = await new Response(proc.stdout).text();
26047
26096
  return output.trim().split(`
26048
26097
  `).filter(Boolean);
26049
26098
  }
@@ -26090,8 +26139,9 @@ async function verifyImplementerIsolation(workdir, beforeRef) {
26090
26139
  description: "Implementer should not modify test files"
26091
26140
  };
26092
26141
  }
26093
- var TEST_PATTERNS, SRC_PATTERNS;
26142
+ var _isolationDeps, TEST_PATTERNS, SRC_PATTERNS;
26094
26143
  var init_isolation = __esm(() => {
26144
+ _isolationDeps = { spawn: Bun.spawn };
26095
26145
  TEST_PATTERNS = [/^test\//, /^tests\//, /^__tests__\//, /\.spec\.\w+$/, /\.test\.\w+$/, /\.e2e-spec\.\w+$/];
26096
26146
  SRC_PATTERNS = [/^src\//, /^lib\//, /^packages\//];
26097
26147
  });
@@ -26289,7 +26339,7 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
26289
26339
  const shell = options?.shell ?? "/bin/sh";
26290
26340
  const gracePeriodMs = options?.gracePeriodMs ?? 5000;
26291
26341
  const drainTimeoutMs = options?.drainTimeoutMs ?? 2000;
26292
- const proc = Bun.spawn([shell, "-c", command], {
26342
+ const proc = _executorDeps.spawn([shell, "-c", command], {
26293
26343
  stdout: "pipe",
26294
26344
  stderr: "pipe",
26295
26345
  env: env2 || normalizeEnvironment(process.env),
@@ -26383,8 +26433,9 @@ function buildTestCommand(baseCommand, options) {
26383
26433
  }
26384
26434
  return command;
26385
26435
  }
26386
- var DEFAULT_STRIP_ENV_VARS;
26436
+ var _executorDeps, DEFAULT_STRIP_ENV_VARS;
26387
26437
  var init_executor = __esm(() => {
26438
+ _executorDeps = { spawn: Bun.spawn };
26388
26439
  DEFAULT_STRIP_ENV_VARS = ["CLAUDECODE", "REPL_ID", "AGENT"];
26389
26440
  });
26390
26441
 
@@ -26684,15 +26735,15 @@ var init_verification = __esm(() => {
26684
26735
  // src/tdd/cleanup.ts
26685
26736
  async function getPgid(pid) {
26686
26737
  try {
26687
- const proc = Bun.spawn(["ps", "-o", "pgid=", "-p", String(pid)], {
26738
+ const proc = _cleanupDeps.spawn(["ps", "-o", "pgid=", "-p", String(pid)], {
26688
26739
  stdout: "pipe",
26689
26740
  stderr: "pipe"
26690
26741
  });
26742
+ const output = await Bun.readableStreamToText(proc.stdout);
26691
26743
  const exitCode = await proc.exited;
26692
26744
  if (exitCode !== 0) {
26693
26745
  return null;
26694
26746
  }
26695
- const output = await new Response(proc.stdout).text();
26696
26747
  const pgid = Number.parseInt(output.trim(), 10);
26697
26748
  return Number.isNaN(pgid) ? null : pgid;
26698
26749
  } catch {
@@ -26706,7 +26757,7 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
26706
26757
  return;
26707
26758
  }
26708
26759
  try {
26709
- process.kill(-pgid, "SIGTERM");
26760
+ _cleanupDeps.kill(-pgid, "SIGTERM");
26710
26761
  } catch (error48) {
26711
26762
  const err = error48;
26712
26763
  if (err.code !== "ESRCH") {
@@ -26714,11 +26765,11 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
26714
26765
  }
26715
26766
  return;
26716
26767
  }
26717
- await Bun.sleep(gracePeriodMs);
26768
+ await _cleanupDeps.sleep(gracePeriodMs);
26718
26769
  const pgidAfterWait = await getPgid(pid);
26719
26770
  if (pgidAfterWait && pgidAfterWait === pgid) {
26720
26771
  try {
26721
- process.kill(-pgid, "SIGKILL");
26772
+ _cleanupDeps.kill(-pgid, "SIGKILL");
26722
26773
  } catch {}
26723
26774
  }
26724
26775
  } catch (error48) {
@@ -26729,8 +26780,14 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
26729
26780
  });
26730
26781
  }
26731
26782
  }
26783
+ var _cleanupDeps;
26732
26784
  var init_cleanup = __esm(() => {
26733
26785
  init_logger2();
26786
+ _cleanupDeps = {
26787
+ spawn: Bun.spawn,
26788
+ sleep: Bun.sleep,
26789
+ kill: process.kill.bind(process)
26790
+ };
26734
26791
  });
26735
26792
 
26736
26793
  // src/tdd/prompts.ts
@@ -26753,10 +26810,12 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
26753
26810
  storyId: story.id,
26754
26811
  timeout: fullSuiteTimeout
26755
26812
  });
26756
- const fullSuiteResult = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, { cwd: workdir });
26813
+ const fullSuiteResult = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
26814
+ cwd: workdir
26815
+ });
26757
26816
  const fullSuitePassed = fullSuiteResult.success && fullSuiteResult.exitCode === 0;
26758
26817
  if (!fullSuitePassed && fullSuiteResult.output) {
26759
- const testSummary = parseBunTestOutput(fullSuiteResult.output);
26818
+ const testSummary = _rectificationGateDeps.parseBunTestOutput(fullSuiteResult.output);
26760
26819
  if (testSummary.failed > 0) {
26761
26820
  return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName);
26762
26821
  }
@@ -26797,8 +26856,14 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26797
26856
  failedTests: testSummary.failed,
26798
26857
  passedTests: testSummary.passed
26799
26858
  });
26800
- while (shouldRetryRectification(rectificationState, rectificationConfig)) {
26859
+ const rectificationSessionName = buildSessionName(workdir, featureName, story.id, "implementer");
26860
+ logger.debug("tdd", "Rectification session name (shared across all attempts)", {
26861
+ storyId: story.id,
26862
+ sessionName: rectificationSessionName
26863
+ });
26864
+ while (_rectificationGateDeps.shouldRetryRectification(rectificationState, rectificationConfig)) {
26801
26865
  rectificationState.attempt++;
26866
+ const isLastAttempt = rectificationState.attempt >= rectificationConfig.maxRetries;
26802
26867
  logger.info("tdd", `-> Implementer rectification attempt ${rectificationState.attempt}/${rectificationConfig.maxRetries}`, { storyId: story.id, currentFailures: rectificationState.currentFailures });
26803
26868
  const rectificationPrompt = buildImplementerRectificationPrompt(testSummary.failures, story, contextMarkdown, rectificationConfig);
26804
26869
  const rectifyBeforeRef = await captureGitRef(workdir) ?? "HEAD";
@@ -26814,7 +26879,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26814
26879
  maxInteractionTurns: config2.agent?.maxInteractionTurns,
26815
26880
  featureName,
26816
26881
  storyId: story.id,
26817
- sessionRole: "implementer"
26882
+ sessionRole: "implementer",
26883
+ acpSessionName: rectificationSessionName,
26884
+ keepSessionOpen: !isLastAttempt
26818
26885
  });
26819
26886
  if (!rectifyResult.success && rectifyResult.pid) {
26820
26887
  await cleanupProcessTree(rectifyResult.pid);
@@ -26842,7 +26909,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26842
26909
  });
26843
26910
  break;
26844
26911
  }
26845
- const retryFullSuite = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, { cwd: workdir });
26912
+ const retryFullSuite = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
26913
+ cwd: workdir
26914
+ });
26846
26915
  const retrySuitePassed = retryFullSuite.success && retryFullSuite.exitCode === 0;
26847
26916
  if (retrySuitePassed) {
26848
26917
  logger.info("tdd", "Full suite gate passed after rectification!", {
@@ -26852,7 +26921,7 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26852
26921
  return true;
26853
26922
  }
26854
26923
  if (retryFullSuite.output) {
26855
- const newTestSummary = parseBunTestOutput(retryFullSuite.output);
26924
+ const newTestSummary = _rectificationGateDeps.parseBunTestOutput(retryFullSuite.output);
26856
26925
  rectificationState.currentFailures = newTestSummary.failed;
26857
26926
  testSummary.failures = newTestSummary.failures;
26858
26927
  testSummary.failed = newTestSummary.failed;
@@ -26864,7 +26933,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26864
26933
  remainingFailures: rectificationState.currentFailures
26865
26934
  });
26866
26935
  }
26867
- const finalFullSuite = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, { cwd: workdir });
26936
+ const finalFullSuite = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
26937
+ cwd: workdir
26938
+ });
26868
26939
  const finalSuitePassed = finalFullSuite.success && finalFullSuite.exitCode === 0;
26869
26940
  if (!finalSuitePassed) {
26870
26941
  logger.warn("tdd", "[WARN] Full suite gate failed after rectification exhausted", {
@@ -26877,13 +26948,20 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26877
26948
  logger.info("tdd", "Full suite gate passed", { storyId: story.id });
26878
26949
  return true;
26879
26950
  }
26951
+ var _rectificationGateDeps;
26880
26952
  var init_rectification_gate = __esm(() => {
26953
+ init_adapter2();
26881
26954
  init_config();
26882
26955
  init_git();
26883
26956
  init_verification();
26884
26957
  init_cleanup();
26885
26958
  init_isolation();
26886
26959
  init_prompts();
26960
+ _rectificationGateDeps = {
26961
+ executeWithTimeout,
26962
+ parseBunTestOutput,
26963
+ shouldRetryRectification
26964
+ };
26887
26965
  });
26888
26966
 
26889
26967
  // src/prompts/sections/conventions.ts
@@ -27373,7 +27451,7 @@ ${tail}`;
27373
27451
  async function rollbackToRef(workdir, ref) {
27374
27452
  const logger = getLogger();
27375
27453
  logger.warn("tdd", "Rolling back git changes", { ref });
27376
- const resetProc = Bun.spawn(["git", "reset", "--hard", ref], {
27454
+ const resetProc = _sessionRunnerDeps.spawn(["git", "reset", "--hard", ref], {
27377
27455
  cwd: workdir,
27378
27456
  stdout: "pipe",
27379
27457
  stderr: "pipe"
@@ -27384,7 +27462,7 @@ async function rollbackToRef(workdir, ref) {
27384
27462
  logger.error("tdd", "Failed to rollback git changes", { ref, stderr });
27385
27463
  throw new Error(`Git rollback failed: ${stderr}`);
27386
27464
  }
27387
- const cleanProc = Bun.spawn(["git", "clean", "-fd"], {
27465
+ const cleanProc = _sessionRunnerDeps.spawn(["git", "clean", "-fd"], {
27388
27466
  cwd: workdir,
27389
27467
  stdout: "pipe",
27390
27468
  stderr: "pipe"
@@ -27399,19 +27477,24 @@ async function rollbackToRef(workdir, ref) {
27399
27477
  async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution, featureName) {
27400
27478
  const startTime = Date.now();
27401
27479
  let prompt;
27402
- switch (role) {
27403
- case "test-writer":
27404
- prompt = await PromptBuilder.for("test-writer", { isolation: lite ? "lite" : "strict" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
27405
- break;
27406
- case "implementer":
27407
- prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
27408
- break;
27409
- case "verifier":
27410
- prompt = await PromptBuilder.for("verifier").withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
27411
- break;
27480
+ if (_sessionRunnerDeps.buildPrompt) {
27481
+ prompt = await _sessionRunnerDeps.buildPrompt(role, config2, story, workdir, contextMarkdown, lite, constitution);
27482
+ } else {
27483
+ switch (role) {
27484
+ case "test-writer":
27485
+ prompt = await PromptBuilder.for("test-writer", { isolation: lite ? "lite" : "strict" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
27486
+ break;
27487
+ case "implementer":
27488
+ prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
27489
+ break;
27490
+ case "verifier":
27491
+ prompt = await PromptBuilder.for("verifier").withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
27492
+ break;
27493
+ }
27412
27494
  }
27413
27495
  const logger = getLogger();
27414
27496
  logger.info("tdd", `-> Session: ${role}`, { role, storyId: story.id, lite });
27497
+ const keepSessionOpen = role === "implementer" && (config2.execution.rectification?.enabled ?? false);
27415
27498
  const result = await agent.run({
27416
27499
  prompt,
27417
27500
  workdir,
@@ -27424,10 +27507,11 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
27424
27507
  maxInteractionTurns: config2.agent?.maxInteractionTurns,
27425
27508
  featureName,
27426
27509
  storyId: story.id,
27427
- sessionRole: role
27510
+ sessionRole: role,
27511
+ keepSessionOpen
27428
27512
  });
27429
27513
  if (!result.success && result.pid) {
27430
- await cleanupProcessTree(result.pid);
27514
+ await _sessionRunnerDeps.cleanupProcessTree(result.pid);
27431
27515
  }
27432
27516
  if (result.success) {
27433
27517
  logger.info("tdd", `Session complete: ${role}`, {
@@ -27449,12 +27533,12 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
27449
27533
  if (!skipIsolation) {
27450
27534
  if (role === "test-writer") {
27451
27535
  const allowedPaths = config2.tdd.testWriterAllowedPaths ?? ["src/index.ts", "src/**/index.ts"];
27452
- isolation = await verifyTestWriterIsolation(workdir, beforeRef, allowedPaths);
27536
+ isolation = await _sessionRunnerDeps.verifyTestWriterIsolation(workdir, beforeRef, allowedPaths);
27453
27537
  } else if (role === "implementer" || role === "verifier") {
27454
- isolation = await verifyImplementerIsolation(workdir, beforeRef);
27538
+ isolation = await _sessionRunnerDeps.verifyImplementerIsolation(workdir, beforeRef);
27455
27539
  }
27456
27540
  }
27457
- const filesChanged = await getChangedFiles2(workdir, beforeRef);
27541
+ const filesChanged = await _sessionRunnerDeps.getChangedFiles(workdir, beforeRef);
27458
27542
  const durationMs = Date.now() - startTime;
27459
27543
  if (isolation && !isolation.passed) {
27460
27544
  logger.error("tdd", "Isolation violated", {
@@ -27497,10 +27581,18 @@ var init_session_runner = __esm(() => {
27497
27581
  init_logger2();
27498
27582
  init_prompts2();
27499
27583
  init_git();
27584
+ init_git();
27500
27585
  init_cleanup();
27501
27586
  init_isolation();
27502
27587
  _sessionRunnerDeps = {
27503
- autoCommitIfDirty
27588
+ autoCommitIfDirty,
27589
+ spawn: Bun.spawn,
27590
+ getChangedFiles: getChangedFiles2,
27591
+ verifyTestWriterIsolation,
27592
+ verifyImplementerIsolation,
27593
+ captureGitRef,
27594
+ cleanupProcessTree,
27595
+ buildPrompt: null
27504
27596
  };
27505
27597
  });
27506
27598
 
@@ -28981,7 +29073,7 @@ class AcceptanceStrategy {
28981
29073
  }
28982
29074
  const start = Date.now();
28983
29075
  const timeoutMs = ctx.timeoutSeconds * 1000;
28984
- const proc = Bun.spawn(["bun", "test", testPath], {
29076
+ const proc = _acceptanceDeps.spawn(["bun", "test", testPath], {
28985
29077
  cwd: ctx.workdir,
28986
29078
  stdout: "pipe",
28987
29079
  stderr: "pipe"
@@ -29049,8 +29141,10 @@ ${stderr}`;
29049
29141
  });
29050
29142
  }
29051
29143
  }
29144
+ var _acceptanceDeps;
29052
29145
  var init_acceptance3 = __esm(() => {
29053
29146
  init_logger2();
29147
+ _acceptanceDeps = { spawn: Bun.spawn };
29054
29148
  });
29055
29149
 
29056
29150
  // src/verification/strategies/regression.ts
@@ -68243,6 +68337,45 @@ class PipelineEventEmitter {
68243
68337
  init_stages();
68244
68338
  init_prd();
68245
68339
 
68340
+ // src/cli/prompts-shared.ts
68341
+ function buildFrontmatter(story, ctx, role) {
68342
+ const lines = [];
68343
+ lines.push(`storyId: ${story.id}`);
68344
+ lines.push(`title: "${story.title}"`);
68345
+ lines.push(`testStrategy: ${ctx.routing.testStrategy}`);
68346
+ lines.push(`modelTier: ${ctx.routing.modelTier}`);
68347
+ if (role) {
68348
+ lines.push(`role: ${role}`);
68349
+ }
68350
+ const builtContext = ctx.builtContext;
68351
+ const contextTokens = builtContext?.totalTokens ?? 0;
68352
+ const promptTokens = ctx.prompt ? Math.ceil(ctx.prompt.length / 3) : 0;
68353
+ lines.push(`contextTokens: ${contextTokens}`);
68354
+ lines.push(`promptTokens: ${promptTokens}`);
68355
+ if (story.dependencies && story.dependencies.length > 0) {
68356
+ lines.push(`dependencies: [${story.dependencies.join(", ")}]`);
68357
+ }
68358
+ lines.push("contextElements:");
68359
+ if (builtContext) {
68360
+ for (const element of builtContext.elements) {
68361
+ lines.push(` - type: ${element.type}`);
68362
+ if (element.storyId) {
68363
+ lines.push(` storyId: ${element.storyId}`);
68364
+ }
68365
+ if (element.filePath) {
68366
+ lines.push(` filePath: ${element.filePath}`);
68367
+ }
68368
+ lines.push(` tokens: ${element.tokens}`);
68369
+ }
68370
+ }
68371
+ if (builtContext?.truncated) {
68372
+ lines.push("truncated: true");
68373
+ }
68374
+ return `${lines.join(`
68375
+ `)}
68376
+ `;
68377
+ }
68378
+
68246
68379
  // src/cli/prompts-tdd.ts
68247
68380
  init_prompts2();
68248
68381
  import { join as join28 } from "path";
@@ -68388,43 +68521,6 @@ ${"=".repeat(80)}`);
68388
68521
  });
68389
68522
  return processedStories;
68390
68523
  }
68391
- function buildFrontmatter(story, ctx, role) {
68392
- const lines = [];
68393
- lines.push(`storyId: ${story.id}`);
68394
- lines.push(`title: "${story.title}"`);
68395
- lines.push(`testStrategy: ${ctx.routing.testStrategy}`);
68396
- lines.push(`modelTier: ${ctx.routing.modelTier}`);
68397
- if (role) {
68398
- lines.push(`role: ${role}`);
68399
- }
68400
- const builtContext = ctx.builtContext;
68401
- const contextTokens = builtContext?.totalTokens ?? 0;
68402
- const promptTokens = ctx.prompt ? Math.ceil(ctx.prompt.length / 3) : 0;
68403
- lines.push(`contextTokens: ${contextTokens}`);
68404
- lines.push(`promptTokens: ${promptTokens}`);
68405
- if (story.dependencies && story.dependencies.length > 0) {
68406
- lines.push(`dependencies: [${story.dependencies.join(", ")}]`);
68407
- }
68408
- lines.push("contextElements:");
68409
- if (builtContext) {
68410
- for (const element of builtContext.elements) {
68411
- lines.push(` - type: ${element.type}`);
68412
- if (element.storyId) {
68413
- lines.push(` storyId: ${element.storyId}`);
68414
- }
68415
- if (element.filePath) {
68416
- lines.push(` filePath: ${element.filePath}`);
68417
- }
68418
- lines.push(` tokens: ${element.tokens}`);
68419
- }
68420
- }
68421
- if (builtContext?.truncated) {
68422
- lines.push("truncated: true");
68423
- }
68424
- return `${lines.join(`
68425
- `)}
68426
- `;
68427
- }
68428
68524
  // src/cli/prompts-init.ts
68429
68525
  import { existsSync as existsSync19, mkdirSync as mkdirSync4 } from "fs";
68430
68526
  import { join as join30 } from "path";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.49.3",
3
+ "version": "0.49.6",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {