@nathapp/nax 0.49.1 → 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
@@ -3267,10 +3267,10 @@ Security-critical functions (authentication, cryptography, tokens, sessions, cre
3267
3267
  password hashing, access control) must be classified at MINIMUM "medium" complexity
3268
3268
  regardless of LOC count. These require at minimum "tdd-simple" test strategy.`, TEST_STRATEGY_GUIDE = `## Test Strategy Guide
3269
3269
 
3270
- - test-after: Simple changes with well-understood behavior. Write tests after implementation.
3271
- - tdd-simple: Medium complexity. Write key tests first, implement, then fill coverage.
3272
- - three-session-tdd: Complex stories. Full TDD cycle with separate test-writer and implementer sessions.
3273
- - three-session-tdd-lite: Expert/high-risk stories. Full TDD with additional verifier session.`, GROUPING_RULES = `## Grouping Rules
3270
+ - test-after: Simple changes with well-understood behavior. Write tests after implementation in a single session.
3271
+ - tdd-simple: Medium complexity. Write failing tests first, then implement to pass them \u2014 all in one session.
3272
+ - three-session-tdd: Complex stories. 3 sessions: (1) test-writer writes failing tests \u2014 no src/ changes allowed, (2) implementer makes them pass without modifying test files, (3) verifier confirms correctness.
3273
+ - three-session-tdd-lite: Expert/high-risk stories. 3 sessions: (1) test-writer writes failing tests and may create minimal src/ stubs for imports, (2) implementer makes tests pass and may add missing coverage or replace stubs, (3) verifier confirms correctness.`, GROUPING_RULES = `## Grouping Rules
3274
3274
 
3275
3275
  - Combine small, related tasks into a single "simple" or "medium" story.
3276
3276
  - Do NOT create separate stories for every single file or function unless complex.
@@ -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.1",
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("635a552"))
22327
- return "635a552";
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(),
@@ -24357,6 +24406,8 @@ async function resolveCommand(check2, config2, executionConfig, workdir) {
24357
24406
  }
24358
24407
  async function runCheck(check2, command, workdir) {
24359
24408
  const startTime = Date.now();
24409
+ const logger = getSafeLogger();
24410
+ logger?.info("review", `Running ${check2} check`, { check: check2, command, workdir });
24360
24411
  try {
24361
24412
  const parts = command.split(/\s+/);
24362
24413
  const executable = parts[0];
@@ -24395,6 +24446,17 @@ async function runCheck(check2, command, workdir) {
24395
24446
  const stderr = await new Response(proc.stderr).text();
24396
24447
  const output = [stdout, stderr].filter(Boolean).join(`
24397
24448
  `);
24449
+ if (exitCode !== 0) {
24450
+ logger?.warn("review", `${check2} check failed`, {
24451
+ check: check2,
24452
+ command,
24453
+ workdir,
24454
+ exitCode,
24455
+ output: output.slice(0, 2000)
24456
+ });
24457
+ } else {
24458
+ logger?.debug("review", `${check2} check passed`, { check: check2, command, durationMs: Date.now() - startTime });
24459
+ }
24398
24460
  return {
24399
24461
  check: check2,
24400
24462
  command,
@@ -24680,8 +24742,8 @@ async function recheckReview(ctx) {
24680
24742
  const { reviewStage: reviewStage2 } = await Promise.resolve().then(() => (init_review(), exports_review));
24681
24743
  if (!reviewStage2.enabled(ctx))
24682
24744
  return true;
24683
- const result = await reviewStage2.execute(ctx);
24684
- return result.action === "continue";
24745
+ await reviewStage2.execute(ctx);
24746
+ return ctx.reviewResult?.success === true;
24685
24747
  }
24686
24748
  function collectFailedChecks(ctx) {
24687
24749
  return (ctx.reviewResult?.checks ?? []).filter((c) => !c.success);
@@ -24793,11 +24855,18 @@ var init_autofix = __esm(() => {
24793
24855
  const lintFixCmd = effectiveConfig.quality.commands.lintFix;
24794
24856
  const formatFixCmd = effectiveConfig.quality.commands.formatFix;
24795
24857
  const effectiveWorkdir = ctx.story.workdir ? join18(ctx.workdir, ctx.story.workdir) : ctx.workdir;
24796
- if (lintFixCmd || formatFixCmd) {
24858
+ const failedCheckNames = new Set((reviewResult.checks ?? []).filter((c) => !c.success).map((c) => c.check));
24859
+ const hasLintFailure = failedCheckNames.has("lint");
24860
+ logger.info("autofix", "Starting autofix", {
24861
+ storyId: ctx.story.id,
24862
+ failedChecks: [...failedCheckNames],
24863
+ workdir: effectiveWorkdir
24864
+ });
24865
+ if (hasLintFailure && (lintFixCmd || formatFixCmd)) {
24797
24866
  if (lintFixCmd) {
24798
24867
  pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: lintFixCmd });
24799
24868
  const lintResult = await _autofixDeps.runCommand(lintFixCmd, effectiveWorkdir);
24800
- logger.debug("autofix", `lintFix exit=${lintResult.exitCode}`, { storyId: ctx.story.id });
24869
+ logger.debug("autofix", `lintFix exit=${lintResult.exitCode}`, { storyId: ctx.story.id, command: lintFixCmd });
24801
24870
  if (lintResult.exitCode !== 0) {
24802
24871
  logger.warn("autofix", "lintFix command failed \u2014 may not have fixed all issues", {
24803
24872
  storyId: ctx.story.id,
@@ -24808,7 +24877,10 @@ var init_autofix = __esm(() => {
24808
24877
  if (formatFixCmd) {
24809
24878
  pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: formatFixCmd });
24810
24879
  const fmtResult = await _autofixDeps.runCommand(formatFixCmd, effectiveWorkdir);
24811
- logger.debug("autofix", `formatFix exit=${fmtResult.exitCode}`, { storyId: ctx.story.id });
24880
+ logger.debug("autofix", `formatFix exit=${fmtResult.exitCode}`, {
24881
+ storyId: ctx.story.id,
24882
+ command: formatFixCmd
24883
+ });
24812
24884
  if (fmtResult.exitCode !== 0) {
24813
24885
  logger.warn("autofix", "formatFix command failed \u2014 may not have fixed all issues", {
24814
24886
  storyId: ctx.story.id,
@@ -24819,11 +24891,12 @@ var init_autofix = __esm(() => {
24819
24891
  const recheckPassed = await _autofixDeps.recheckReview(ctx);
24820
24892
  pipelineEventBus.emit({ type: "autofix:completed", storyId: ctx.story.id, fixed: recheckPassed });
24821
24893
  if (recheckPassed) {
24822
- if (ctx.reviewResult)
24823
- ctx.reviewResult = { ...ctx.reviewResult, success: true };
24824
24894
  logger.info("autofix", "Mechanical autofix succeeded \u2014 retrying review", { storyId: ctx.story.id });
24825
24895
  return { action: "retry", fromStage: "review" };
24826
24896
  }
24897
+ logger.info("autofix", "Mechanical autofix did not resolve all failures \u2014 proceeding to agent rectification", {
24898
+ storyId: ctx.story.id
24899
+ });
24827
24900
  }
24828
24901
  const agentFixed = await _autofixDeps.runAgentRectification(ctx);
24829
24902
  if (agentFixed) {
@@ -26013,13 +26086,13 @@ function isSourceFile(filePath) {
26013
26086
  return SRC_PATTERNS.some((pattern) => pattern.test(filePath));
26014
26087
  }
26015
26088
  async function getChangedFiles2(workdir, fromRef = "HEAD") {
26016
- const proc = Bun.spawn(["git", "diff", "--name-only", fromRef], {
26089
+ const proc = _isolationDeps.spawn(["git", "diff", "--name-only", fromRef], {
26017
26090
  cwd: workdir,
26018
26091
  stdout: "pipe",
26019
26092
  stderr: "pipe"
26020
26093
  });
26094
+ const output = await Bun.readableStreamToText(proc.stdout);
26021
26095
  await proc.exited;
26022
- const output = await new Response(proc.stdout).text();
26023
26096
  return output.trim().split(`
26024
26097
  `).filter(Boolean);
26025
26098
  }
@@ -26066,8 +26139,9 @@ async function verifyImplementerIsolation(workdir, beforeRef) {
26066
26139
  description: "Implementer should not modify test files"
26067
26140
  };
26068
26141
  }
26069
- var TEST_PATTERNS, SRC_PATTERNS;
26142
+ var _isolationDeps, TEST_PATTERNS, SRC_PATTERNS;
26070
26143
  var init_isolation = __esm(() => {
26144
+ _isolationDeps = { spawn: Bun.spawn };
26071
26145
  TEST_PATTERNS = [/^test\//, /^tests\//, /^__tests__\//, /\.spec\.\w+$/, /\.test\.\w+$/, /\.e2e-spec\.\w+$/];
26072
26146
  SRC_PATTERNS = [/^src\//, /^lib\//, /^packages\//];
26073
26147
  });
@@ -26265,7 +26339,7 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
26265
26339
  const shell = options?.shell ?? "/bin/sh";
26266
26340
  const gracePeriodMs = options?.gracePeriodMs ?? 5000;
26267
26341
  const drainTimeoutMs = options?.drainTimeoutMs ?? 2000;
26268
- const proc = Bun.spawn([shell, "-c", command], {
26342
+ const proc = _executorDeps.spawn([shell, "-c", command], {
26269
26343
  stdout: "pipe",
26270
26344
  stderr: "pipe",
26271
26345
  env: env2 || normalizeEnvironment(process.env),
@@ -26359,8 +26433,9 @@ function buildTestCommand(baseCommand, options) {
26359
26433
  }
26360
26434
  return command;
26361
26435
  }
26362
- var DEFAULT_STRIP_ENV_VARS;
26436
+ var _executorDeps, DEFAULT_STRIP_ENV_VARS;
26363
26437
  var init_executor = __esm(() => {
26438
+ _executorDeps = { spawn: Bun.spawn };
26364
26439
  DEFAULT_STRIP_ENV_VARS = ["CLAUDECODE", "REPL_ID", "AGENT"];
26365
26440
  });
26366
26441
 
@@ -26660,15 +26735,15 @@ var init_verification = __esm(() => {
26660
26735
  // src/tdd/cleanup.ts
26661
26736
  async function getPgid(pid) {
26662
26737
  try {
26663
- const proc = Bun.spawn(["ps", "-o", "pgid=", "-p", String(pid)], {
26738
+ const proc = _cleanupDeps.spawn(["ps", "-o", "pgid=", "-p", String(pid)], {
26664
26739
  stdout: "pipe",
26665
26740
  stderr: "pipe"
26666
26741
  });
26742
+ const output = await Bun.readableStreamToText(proc.stdout);
26667
26743
  const exitCode = await proc.exited;
26668
26744
  if (exitCode !== 0) {
26669
26745
  return null;
26670
26746
  }
26671
- const output = await new Response(proc.stdout).text();
26672
26747
  const pgid = Number.parseInt(output.trim(), 10);
26673
26748
  return Number.isNaN(pgid) ? null : pgid;
26674
26749
  } catch {
@@ -26682,7 +26757,7 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
26682
26757
  return;
26683
26758
  }
26684
26759
  try {
26685
- process.kill(-pgid, "SIGTERM");
26760
+ _cleanupDeps.kill(-pgid, "SIGTERM");
26686
26761
  } catch (error48) {
26687
26762
  const err = error48;
26688
26763
  if (err.code !== "ESRCH") {
@@ -26690,11 +26765,11 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
26690
26765
  }
26691
26766
  return;
26692
26767
  }
26693
- await Bun.sleep(gracePeriodMs);
26768
+ await _cleanupDeps.sleep(gracePeriodMs);
26694
26769
  const pgidAfterWait = await getPgid(pid);
26695
26770
  if (pgidAfterWait && pgidAfterWait === pgid) {
26696
26771
  try {
26697
- process.kill(-pgid, "SIGKILL");
26772
+ _cleanupDeps.kill(-pgid, "SIGKILL");
26698
26773
  } catch {}
26699
26774
  }
26700
26775
  } catch (error48) {
@@ -26705,8 +26780,14 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
26705
26780
  });
26706
26781
  }
26707
26782
  }
26783
+ var _cleanupDeps;
26708
26784
  var init_cleanup = __esm(() => {
26709
26785
  init_logger2();
26786
+ _cleanupDeps = {
26787
+ spawn: Bun.spawn,
26788
+ sleep: Bun.sleep,
26789
+ kill: process.kill.bind(process)
26790
+ };
26710
26791
  });
26711
26792
 
26712
26793
  // src/tdd/prompts.ts
@@ -26729,10 +26810,12 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
26729
26810
  storyId: story.id,
26730
26811
  timeout: fullSuiteTimeout
26731
26812
  });
26732
- const fullSuiteResult = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, { cwd: workdir });
26813
+ const fullSuiteResult = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
26814
+ cwd: workdir
26815
+ });
26733
26816
  const fullSuitePassed = fullSuiteResult.success && fullSuiteResult.exitCode === 0;
26734
26817
  if (!fullSuitePassed && fullSuiteResult.output) {
26735
- const testSummary = parseBunTestOutput(fullSuiteResult.output);
26818
+ const testSummary = _rectificationGateDeps.parseBunTestOutput(fullSuiteResult.output);
26736
26819
  if (testSummary.failed > 0) {
26737
26820
  return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName);
26738
26821
  }
@@ -26773,8 +26856,14 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26773
26856
  failedTests: testSummary.failed,
26774
26857
  passedTests: testSummary.passed
26775
26858
  });
26776
- 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)) {
26777
26865
  rectificationState.attempt++;
26866
+ const isLastAttempt = rectificationState.attempt >= rectificationConfig.maxRetries;
26778
26867
  logger.info("tdd", `-> Implementer rectification attempt ${rectificationState.attempt}/${rectificationConfig.maxRetries}`, { storyId: story.id, currentFailures: rectificationState.currentFailures });
26779
26868
  const rectificationPrompt = buildImplementerRectificationPrompt(testSummary.failures, story, contextMarkdown, rectificationConfig);
26780
26869
  const rectifyBeforeRef = await captureGitRef(workdir) ?? "HEAD";
@@ -26790,7 +26879,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26790
26879
  maxInteractionTurns: config2.agent?.maxInteractionTurns,
26791
26880
  featureName,
26792
26881
  storyId: story.id,
26793
- sessionRole: "implementer"
26882
+ sessionRole: "implementer",
26883
+ acpSessionName: rectificationSessionName,
26884
+ keepSessionOpen: !isLastAttempt
26794
26885
  });
26795
26886
  if (!rectifyResult.success && rectifyResult.pid) {
26796
26887
  await cleanupProcessTree(rectifyResult.pid);
@@ -26818,7 +26909,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26818
26909
  });
26819
26910
  break;
26820
26911
  }
26821
- const retryFullSuite = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, { cwd: workdir });
26912
+ const retryFullSuite = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
26913
+ cwd: workdir
26914
+ });
26822
26915
  const retrySuitePassed = retryFullSuite.success && retryFullSuite.exitCode === 0;
26823
26916
  if (retrySuitePassed) {
26824
26917
  logger.info("tdd", "Full suite gate passed after rectification!", {
@@ -26828,7 +26921,7 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26828
26921
  return true;
26829
26922
  }
26830
26923
  if (retryFullSuite.output) {
26831
- const newTestSummary = parseBunTestOutput(retryFullSuite.output);
26924
+ const newTestSummary = _rectificationGateDeps.parseBunTestOutput(retryFullSuite.output);
26832
26925
  rectificationState.currentFailures = newTestSummary.failed;
26833
26926
  testSummary.failures = newTestSummary.failures;
26834
26927
  testSummary.failed = newTestSummary.failed;
@@ -26840,7 +26933,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26840
26933
  remainingFailures: rectificationState.currentFailures
26841
26934
  });
26842
26935
  }
26843
- const finalFullSuite = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, { cwd: workdir });
26936
+ const finalFullSuite = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
26937
+ cwd: workdir
26938
+ });
26844
26939
  const finalSuitePassed = finalFullSuite.success && finalFullSuite.exitCode === 0;
26845
26940
  if (!finalSuitePassed) {
26846
26941
  logger.warn("tdd", "[WARN] Full suite gate failed after rectification exhausted", {
@@ -26853,13 +26948,20 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26853
26948
  logger.info("tdd", "Full suite gate passed", { storyId: story.id });
26854
26949
  return true;
26855
26950
  }
26951
+ var _rectificationGateDeps;
26856
26952
  var init_rectification_gate = __esm(() => {
26953
+ init_adapter2();
26857
26954
  init_config();
26858
26955
  init_git();
26859
26956
  init_verification();
26860
26957
  init_cleanup();
26861
26958
  init_isolation();
26862
26959
  init_prompts();
26960
+ _rectificationGateDeps = {
26961
+ executeWithTimeout,
26962
+ parseBunTestOutput,
26963
+ shouldRetryRectification
26964
+ };
26863
26965
  });
26864
26966
 
26865
26967
  // src/prompts/sections/conventions.ts
@@ -27349,7 +27451,7 @@ ${tail}`;
27349
27451
  async function rollbackToRef(workdir, ref) {
27350
27452
  const logger = getLogger();
27351
27453
  logger.warn("tdd", "Rolling back git changes", { ref });
27352
- const resetProc = Bun.spawn(["git", "reset", "--hard", ref], {
27454
+ const resetProc = _sessionRunnerDeps.spawn(["git", "reset", "--hard", ref], {
27353
27455
  cwd: workdir,
27354
27456
  stdout: "pipe",
27355
27457
  stderr: "pipe"
@@ -27360,7 +27462,7 @@ async function rollbackToRef(workdir, ref) {
27360
27462
  logger.error("tdd", "Failed to rollback git changes", { ref, stderr });
27361
27463
  throw new Error(`Git rollback failed: ${stderr}`);
27362
27464
  }
27363
- const cleanProc = Bun.spawn(["git", "clean", "-fd"], {
27465
+ const cleanProc = _sessionRunnerDeps.spawn(["git", "clean", "-fd"], {
27364
27466
  cwd: workdir,
27365
27467
  stdout: "pipe",
27366
27468
  stderr: "pipe"
@@ -27375,19 +27477,24 @@ async function rollbackToRef(workdir, ref) {
27375
27477
  async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution, featureName) {
27376
27478
  const startTime = Date.now();
27377
27479
  let prompt;
27378
- switch (role) {
27379
- case "test-writer":
27380
- 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();
27381
- break;
27382
- case "implementer":
27383
- prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
27384
- break;
27385
- case "verifier":
27386
- prompt = await PromptBuilder.for("verifier").withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
27387
- 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
+ }
27388
27494
  }
27389
27495
  const logger = getLogger();
27390
27496
  logger.info("tdd", `-> Session: ${role}`, { role, storyId: story.id, lite });
27497
+ const keepSessionOpen = role === "implementer" && (config2.execution.rectification?.enabled ?? false);
27391
27498
  const result = await agent.run({
27392
27499
  prompt,
27393
27500
  workdir,
@@ -27400,10 +27507,11 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
27400
27507
  maxInteractionTurns: config2.agent?.maxInteractionTurns,
27401
27508
  featureName,
27402
27509
  storyId: story.id,
27403
- sessionRole: role
27510
+ sessionRole: role,
27511
+ keepSessionOpen
27404
27512
  });
27405
27513
  if (!result.success && result.pid) {
27406
- await cleanupProcessTree(result.pid);
27514
+ await _sessionRunnerDeps.cleanupProcessTree(result.pid);
27407
27515
  }
27408
27516
  if (result.success) {
27409
27517
  logger.info("tdd", `Session complete: ${role}`, {
@@ -27425,12 +27533,12 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
27425
27533
  if (!skipIsolation) {
27426
27534
  if (role === "test-writer") {
27427
27535
  const allowedPaths = config2.tdd.testWriterAllowedPaths ?? ["src/index.ts", "src/**/index.ts"];
27428
- isolation = await verifyTestWriterIsolation(workdir, beforeRef, allowedPaths);
27536
+ isolation = await _sessionRunnerDeps.verifyTestWriterIsolation(workdir, beforeRef, allowedPaths);
27429
27537
  } else if (role === "implementer" || role === "verifier") {
27430
- isolation = await verifyImplementerIsolation(workdir, beforeRef);
27538
+ isolation = await _sessionRunnerDeps.verifyImplementerIsolation(workdir, beforeRef);
27431
27539
  }
27432
27540
  }
27433
- const filesChanged = await getChangedFiles2(workdir, beforeRef);
27541
+ const filesChanged = await _sessionRunnerDeps.getChangedFiles(workdir, beforeRef);
27434
27542
  const durationMs = Date.now() - startTime;
27435
27543
  if (isolation && !isolation.passed) {
27436
27544
  logger.error("tdd", "Isolation violated", {
@@ -27473,10 +27581,18 @@ var init_session_runner = __esm(() => {
27473
27581
  init_logger2();
27474
27582
  init_prompts2();
27475
27583
  init_git();
27584
+ init_git();
27476
27585
  init_cleanup();
27477
27586
  init_isolation();
27478
27587
  _sessionRunnerDeps = {
27479
- autoCommitIfDirty
27588
+ autoCommitIfDirty,
27589
+ spawn: Bun.spawn,
27590
+ getChangedFiles: getChangedFiles2,
27591
+ verifyTestWriterIsolation,
27592
+ verifyImplementerIsolation,
27593
+ captureGitRef,
27594
+ cleanupProcessTree,
27595
+ buildPrompt: null
27480
27596
  };
27481
27597
  });
27482
27598
 
@@ -28957,7 +29073,7 @@ class AcceptanceStrategy {
28957
29073
  }
28958
29074
  const start = Date.now();
28959
29075
  const timeoutMs = ctx.timeoutSeconds * 1000;
28960
- const proc = Bun.spawn(["bun", "test", testPath], {
29076
+ const proc = _acceptanceDeps.spawn(["bun", "test", testPath], {
28961
29077
  cwd: ctx.workdir,
28962
29078
  stdout: "pipe",
28963
29079
  stderr: "pipe"
@@ -29025,8 +29141,10 @@ ${stderr}`;
29025
29141
  });
29026
29142
  }
29027
29143
  }
29144
+ var _acceptanceDeps;
29028
29145
  var init_acceptance3 = __esm(() => {
29029
29146
  init_logger2();
29147
+ _acceptanceDeps = { spawn: Bun.spawn };
29030
29148
  });
29031
29149
 
29032
29150
  // src/verification/strategies/regression.ts
@@ -29553,7 +29671,7 @@ var init_routing2 = __esm(() => {
29553
29671
  logger.debug("routing", ctx.routing.reasoning);
29554
29672
  }
29555
29673
  const decomposeConfig = ctx.config.decompose;
29556
- if (decomposeConfig) {
29674
+ if (decomposeConfig && ctx.story.status !== "decomposed") {
29557
29675
  const acCount = ctx.story.acceptanceCriteria.length;
29558
29676
  const complexity = ctx.routing.complexity;
29559
29677
  const isOversized = acCount > decomposeConfig.maxAcceptanceCriteria && (complexity === "complex" || complexity === "expert");
@@ -34256,6 +34374,7 @@ async function handlePipelineFailure(ctx, pipelineResult) {
34256
34374
  const logger = getSafeLogger();
34257
34375
  let prd = ctx.prd;
34258
34376
  let prdDirty = false;
34377
+ const costDelta = pipelineResult.context.agentResult?.estimatedCost || 0;
34259
34378
  switch (pipelineResult.finalAction) {
34260
34379
  case "pause":
34261
34380
  markStoryPaused(prd, ctx.story.id);
@@ -34322,7 +34441,7 @@ async function handlePipelineFailure(ctx, pipelineResult) {
34322
34441
  break;
34323
34442
  }
34324
34443
  }
34325
- return { prd, prdDirty };
34444
+ return { prd, prdDirty, costDelta };
34326
34445
  }
34327
34446
  var init_pipeline_result_handler = __esm(() => {
34328
34447
  init_logger2();
@@ -34427,7 +34546,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
34427
34546
  return {
34428
34547
  prd: r.prd,
34429
34548
  storiesCompletedDelta: 0,
34430
- costDelta: 0,
34549
+ costDelta: r.costDelta,
34431
34550
  prdDirty: r.prdDirty,
34432
34551
  finalAction: pipelineResult.finalAction,
34433
34552
  reason: pipelineResult.reason
@@ -34465,7 +34584,7 @@ function buildPreviewRouting(story, config2) {
34465
34584
  function selectNextStories(prd, config2, batchPlan, currentBatchIndex, lastStoryId, useBatch) {
34466
34585
  if (useBatch && currentBatchIndex < batchPlan.length) {
34467
34586
  const batch = batchPlan[currentBatchIndex];
34468
- const storiesToExecute = batch.stories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "failed" && s.status !== "paused");
34587
+ const storiesToExecute = batch.stories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "failed" && s.status !== "paused" && s.status !== "decomposed");
34469
34588
  if (storiesToExecute.length === 0) {
34470
34589
  return { selection: null, nextBatchIndex: currentBatchIndex + 1 };
34471
34590
  }
@@ -68218,6 +68337,45 @@ class PipelineEventEmitter {
68218
68337
  init_stages();
68219
68338
  init_prd();
68220
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
+
68221
68379
  // src/cli/prompts-tdd.ts
68222
68380
  init_prompts2();
68223
68381
  import { join as join28 } from "path";
@@ -68363,43 +68521,6 @@ ${"=".repeat(80)}`);
68363
68521
  });
68364
68522
  return processedStories;
68365
68523
  }
68366
- function buildFrontmatter(story, ctx, role) {
68367
- const lines = [];
68368
- lines.push(`storyId: ${story.id}`);
68369
- lines.push(`title: "${story.title}"`);
68370
- lines.push(`testStrategy: ${ctx.routing.testStrategy}`);
68371
- lines.push(`modelTier: ${ctx.routing.modelTier}`);
68372
- if (role) {
68373
- lines.push(`role: ${role}`);
68374
- }
68375
- const builtContext = ctx.builtContext;
68376
- const contextTokens = builtContext?.totalTokens ?? 0;
68377
- const promptTokens = ctx.prompt ? Math.ceil(ctx.prompt.length / 3) : 0;
68378
- lines.push(`contextTokens: ${contextTokens}`);
68379
- lines.push(`promptTokens: ${promptTokens}`);
68380
- if (story.dependencies && story.dependencies.length > 0) {
68381
- lines.push(`dependencies: [${story.dependencies.join(", ")}]`);
68382
- }
68383
- lines.push("contextElements:");
68384
- if (builtContext) {
68385
- for (const element of builtContext.elements) {
68386
- lines.push(` - type: ${element.type}`);
68387
- if (element.storyId) {
68388
- lines.push(` storyId: ${element.storyId}`);
68389
- }
68390
- if (element.filePath) {
68391
- lines.push(` filePath: ${element.filePath}`);
68392
- }
68393
- lines.push(` tokens: ${element.tokens}`);
68394
- }
68395
- }
68396
- if (builtContext?.truncated) {
68397
- lines.push("truncated: true");
68398
- }
68399
- return `${lines.join(`
68400
- `)}
68401
- `;
68402
- }
68403
68524
  // src/cli/prompts-init.ts
68404
68525
  import { existsSync as existsSync19, mkdirSync as mkdirSync4 } from "fs";
68405
68526
  import { join as join30 } from "path";