@nathapp/nax 0.49.3 → 0.50.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.
package/dist/nax.js CHANGED
@@ -3256,29 +3256,30 @@ function resolveTestStrategy(raw) {
3256
3256
  }
3257
3257
  var VALID_TEST_STRATEGIES, COMPLEXITY_GUIDE = `## Complexity Classification Guide
3258
3258
 
3259
- - simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192 test-after
3260
- - medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 tdd-simple
3259
+ - simple: \u226450 LOC, single-file change, purely additive, no new dependencies \u2192 tdd-simple
3260
+ - medium: 50\u2013200 LOC, 2\u20135 files, standard patterns, clear requirements \u2192 three-session-tdd-lite
3261
3261
  - complex: 200\u2013500 LOC, multiple modules, new abstractions or integrations \u2192 three-session-tdd
3262
- - expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd-lite
3262
+ - expert: 500+ LOC, architectural changes, cross-cutting concerns, high risk \u2192 three-session-tdd
3263
3263
 
3264
3264
  ### Security Override
3265
3265
 
3266
3266
  Security-critical functions (authentication, cryptography, tokens, sessions, credentials,
3267
- password hashing, access control) must be classified at MINIMUM "medium" complexity
3268
- regardless of LOC count. These require at minimum "tdd-simple" test strategy.`, TEST_STRATEGY_GUIDE = `## Test Strategy Guide
3269
-
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
-
3267
+ password hashing, access control) must use three-session-tdd regardless of complexity.`, TEST_STRATEGY_GUIDE = `## Test Strategy Guide
3268
+
3269
+ - tdd-simple: Simple stories (\u226450 LOC). Write failing tests first, then implement to pass them \u2014 all in one session.
3270
+ - three-session-tdd-lite: Medium stories, or complex stories involving UI/CLI/integration. 3 sessions: (1) test-writer writes failing tests and may create minimal src/ stubs for imports, (2) implementer makes tests pass and may replace stubs, (3) verifier confirms correctness.
3271
+ - three-session-tdd: Complex/expert stories or security-critical code. 3 sessions with strict isolation: (1) test-writer writes failing tests \u2014 no src/ changes allowed, (2) implementer makes them pass without modifying test files, (3) verifier confirms correctness.
3272
+ - test-after: Only when explicitly configured (tddStrategy: "off"). Write tests after implementation. Not auto-assigned.`, GROUPING_RULES = `## Story Rules
3273
+
3274
+ - Every story must produce code changes verifiable by tests or review.
3275
+ - NEVER create stories for analysis, planning, documentation, or migration plans.
3276
+ Your analysis belongs in the "analysis" field, not in a story.
3277
+ - NEVER create stories whose primary purpose is writing tests, achieving coverage
3278
+ targets, or running validation/regression suites. Each story's testStrategy
3279
+ handles test creation as part of implementation. Testing is a built-in pipeline
3280
+ stage, not a user story. No exceptions.
3275
3281
  - Combine small, related tasks into a single "simple" or "medium" story.
3276
- - Do NOT create separate stories for every single file or function unless complex.
3277
- - Do NOT create standalone stories purely for test coverage or testing.
3278
- Each story's testStrategy already handles testing (tdd-simple writes tests first,
3279
- three-session-tdd uses separate test-writer session, test-after writes tests after).
3280
- Only create a dedicated test story for unique integration/E2E test logic that spans
3281
- multiple stories and cannot be covered by individual story test strategies.
3282
+ Do NOT create separate stories for every single file or function unless complex.
3282
3283
  - Aim for coherent units of value. Maximum recommended stories: 10-15 per feature.`;
3283
3284
  var init_test_strategy = __esm(() => {
3284
3285
  VALID_TEST_STRATEGIES = [
@@ -3667,6 +3668,16 @@ function buildAllowedEnv(options) {
3667
3668
  async function executeOnce(binary, options, pidRegistry) {
3668
3669
  const cmd = _runOnceDeps.buildCmd(binary, options);
3669
3670
  const startTime = Date.now();
3671
+ if (options.sessionRole || options.acpSessionName || options.keepSessionOpen) {
3672
+ const logger2 = getLogger();
3673
+ logger2.debug("agent", "CLI mode: session options received (unused)", {
3674
+ sessionRole: options.sessionRole,
3675
+ acpSessionName: options.acpSessionName,
3676
+ keepSessionOpen: options.keepSessionOpen,
3677
+ featureName: options.featureName,
3678
+ storyId: options.storyId
3679
+ });
3680
+ }
3670
3681
  const proc = Bun.spawn(cmd, {
3671
3682
  cwd: options.workdir,
3672
3683
  stdout: "pipe",
@@ -18734,6 +18745,17 @@ IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\
18734
18745
  config: options.config
18735
18746
  });
18736
18747
  const testCode = extractTestCode(rawOutput);
18748
+ if (!testCode) {
18749
+ logger.warn("acceptance", "LLM returned non-code output for acceptance tests \u2014 falling back to skeleton", {
18750
+ outputPreview: rawOutput.slice(0, 200)
18751
+ });
18752
+ const skeletonCriteria = refinedCriteria.map((c, i) => ({
18753
+ id: `AC-${i + 1}`,
18754
+ text: c.refined,
18755
+ lineNumber: i + 1
18756
+ }));
18757
+ return { testCode: generateSkeletonTests(options.featureName, skeletonCriteria), criteria: skeletonCriteria };
18758
+ }
18737
18759
  const refinedJsonContent = JSON.stringify(refinedCriteria.map((c, i) => ({
18738
18760
  acId: `AC-${i + 1}`,
18739
18761
  original: c.original,
@@ -18860,6 +18882,15 @@ async function generateAcceptanceTests(adapter, options) {
18860
18882
  config: options.config
18861
18883
  });
18862
18884
  const testCode = extractTestCode(output);
18885
+ if (!testCode) {
18886
+ logger.warn("acceptance", "LLM returned non-code output for acceptance tests \u2014 falling back to skeleton", {
18887
+ outputPreview: output.slice(0, 200)
18888
+ });
18889
+ return {
18890
+ testCode: generateSkeletonTests(options.featureName, criteria),
18891
+ criteria
18892
+ };
18893
+ }
18863
18894
  return {
18864
18895
  testCode,
18865
18896
  criteria
@@ -18873,15 +18904,30 @@ async function generateAcceptanceTests(adapter, options) {
18873
18904
  }
18874
18905
  }
18875
18906
  function extractTestCode(output) {
18907
+ let code;
18876
18908
  const fenceMatch = output.match(/```(?:typescript|ts)?\s*([\s\S]*?)\s*```/);
18877
18909
  if (fenceMatch) {
18878
- return fenceMatch[1].trim();
18910
+ code = fenceMatch[1].trim();
18911
+ }
18912
+ if (!code) {
18913
+ const importMatch = output.match(/import\s+{[\s\S]+/);
18914
+ if (importMatch) {
18915
+ code = importMatch[0].trim();
18916
+ }
18879
18917
  }
18880
- const importMatch = output.match(/import\s+{[\s\S]+/);
18881
- if (importMatch) {
18882
- return importMatch[0].trim();
18918
+ if (!code) {
18919
+ const describeMatch = output.match(/describe\s*\([\s\S]+/);
18920
+ if (describeMatch) {
18921
+ code = describeMatch[0].trim();
18922
+ }
18923
+ }
18924
+ if (!code)
18925
+ return null;
18926
+ const hasTestKeyword = /\b(?:describe|test|it|expect)\s*\(/.test(code);
18927
+ if (!hasTestKeyword) {
18928
+ return null;
18883
18929
  }
18884
- return output.trim();
18930
+ return code;
18885
18931
  }
18886
18932
  function generateSkeletonTests(featureName, criteria) {
18887
18933
  const tests = criteria.map((ac) => {
@@ -19276,7 +19322,6 @@ class SpawnAcpClient {
19276
19322
  model;
19277
19323
  cwd;
19278
19324
  timeoutSeconds;
19279
- permissionMode;
19280
19325
  env;
19281
19326
  pidRegistry;
19282
19327
  constructor(cmdStr, cwd, timeoutSeconds, pidRegistry) {
@@ -19290,7 +19335,6 @@ class SpawnAcpClient {
19290
19335
  this.agentName = lastToken;
19291
19336
  this.cwd = cwd || process.cwd();
19292
19337
  this.timeoutSeconds = timeoutSeconds || 1800;
19293
- this.permissionMode = "approve-reads";
19294
19338
  this.env = buildAllowedEnv2();
19295
19339
  this.pidRegistry = pidRegistry;
19296
19340
  }
@@ -19432,7 +19476,13 @@ async function closeAcpSession(session) {
19432
19476
  function acpSessionsPath(workdir, featureName) {
19433
19477
  return join3(workdir, "nax", "features", featureName, "acp-sessions.json");
19434
19478
  }
19435
- async function saveAcpSession(workdir, featureName, storyId, sessionName) {
19479
+ function sidecarSessionName(entry) {
19480
+ return typeof entry === "string" ? entry : entry.sessionName;
19481
+ }
19482
+ function sidecarAgentName(entry) {
19483
+ return typeof entry === "string" ? "claude" : entry.agentName;
19484
+ }
19485
+ async function saveAcpSession(workdir, featureName, storyId, sessionName, agentName = "claude") {
19436
19486
  try {
19437
19487
  const path = acpSessionsPath(workdir, featureName);
19438
19488
  let data = {};
@@ -19440,7 +19490,7 @@ async function saveAcpSession(workdir, featureName, storyId, sessionName) {
19440
19490
  const existing = await Bun.file(path).text();
19441
19491
  data = JSON.parse(existing);
19442
19492
  } catch {}
19443
- data[storyId] = sessionName;
19493
+ data[storyId] = { sessionName, agentName };
19444
19494
  await Bun.write(path, JSON.stringify(data, null, 2));
19445
19495
  } catch (err) {
19446
19496
  getSafeLogger()?.warn("acp-adapter", "Failed to save session to sidecar", { error: String(err) });
@@ -19467,7 +19517,8 @@ async function readAcpSession(workdir, featureName, storyId) {
19467
19517
  const path = acpSessionsPath(workdir, featureName);
19468
19518
  const existing = await Bun.file(path).text();
19469
19519
  const data = JSON.parse(existing);
19470
- return data[storyId] ?? null;
19520
+ const entry = data[storyId];
19521
+ return entry ? sidecarSessionName(entry) : null;
19471
19522
  } catch {
19472
19523
  return null;
19473
19524
  }
@@ -19486,24 +19537,34 @@ async function sweepFeatureSessions(workdir, featureName) {
19486
19537
  return;
19487
19538
  const logger = getSafeLogger();
19488
19539
  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(() => {});
19540
+ const byAgent = new Map;
19541
+ for (const [, entry] of entries) {
19542
+ const agent = sidecarAgentName(entry);
19543
+ const name = sidecarSessionName(entry);
19544
+ if (!byAgent.has(agent))
19545
+ byAgent.set(agent, []);
19546
+ byAgent.get(agent)?.push(name);
19547
+ }
19548
+ for (const [agentName, sessionNames] of byAgent) {
19549
+ const cmdStr = `acpx ${agentName}`;
19550
+ const client = _acpAdapterDeps.createClient(cmdStr, workdir);
19551
+ try {
19552
+ await client.start();
19553
+ for (const sessionName of sessionNames) {
19554
+ try {
19555
+ if (client.loadSession) {
19556
+ const session = await client.loadSession(sessionName, agentName, "approve-reads");
19557
+ if (session) {
19558
+ await session.close().catch(() => {});
19559
+ }
19499
19560
  }
19561
+ } catch (err) {
19562
+ logger?.warn("acp-adapter", `[sweep] Failed to close session ${sessionName}`, { error: String(err) });
19500
19563
  }
19501
- } catch (err) {
19502
- logger?.warn("acp-adapter", `[sweep] Failed to close session ${sessionName}`, { error: String(err) });
19503
19564
  }
19565
+ } finally {
19566
+ await client.close().catch(() => {});
19504
19567
  }
19505
- } finally {
19506
- await client.close().catch(() => {});
19507
19568
  }
19508
19569
  try {
19509
19570
  await Bun.write(path, JSON.stringify({}, null, 2));
@@ -19644,7 +19705,7 @@ class AcpAgentAdapter {
19644
19705
  });
19645
19706
  const session = await ensureAcpSession(client, sessionName, this.name, permissionMode);
19646
19707
  if (options.featureName && options.storyId) {
19647
- await saveAcpSession(options.workdir, options.featureName, options.storyId, sessionName);
19708
+ await saveAcpSession(options.workdir, options.featureName, options.storyId, sessionName, this.name);
19648
19709
  }
19649
19710
  let lastResponse = null;
19650
19711
  let timedOut = false;
@@ -19702,13 +19763,15 @@ class AcpAgentAdapter {
19702
19763
  }
19703
19764
  runState.succeeded = !timedOut && lastResponse?.stopReason === "end_turn";
19704
19765
  } finally {
19705
- if (runState.succeeded) {
19766
+ if (runState.succeeded && !options.keepSessionOpen) {
19706
19767
  await closeAcpSession(session);
19707
19768
  if (options.featureName && options.storyId) {
19708
19769
  await clearAcpSession(options.workdir, options.featureName, options.storyId);
19709
19770
  }
19710
- } else {
19771
+ } else if (!runState.succeeded) {
19711
19772
  getSafeLogger()?.info("acp-adapter", "Keeping session open for retry", { sessionName });
19773
+ } else {
19774
+ getSafeLogger()?.debug("acp-adapter", "Keeping session open (keepSessionOpen=true)", { sessionName });
19712
19775
  }
19713
19776
  await client.close().catch(() => {});
19714
19777
  }
@@ -20336,7 +20399,8 @@ function applyDecomposition(prd, result) {
20336
20399
  const originalIndex = prd.userStories.findIndex((s) => s.id === parentStoryId);
20337
20400
  if (originalIndex === -1)
20338
20401
  return;
20339
- prd.userStories[originalIndex].status = "decomposed";
20402
+ const parentStory = prd.userStories[originalIndex];
20403
+ parentStory.status = "decomposed";
20340
20404
  const newStories = subStories.map((sub) => ({
20341
20405
  id: sub.id,
20342
20406
  title: sub.title,
@@ -20348,7 +20412,8 @@ function applyDecomposition(prd, result) {
20348
20412
  passes: false,
20349
20413
  escalations: [],
20350
20414
  attempts: 0,
20351
- parentStoryId: sub.parentStoryId
20415
+ parentStoryId: sub.parentStoryId,
20416
+ ...parentStory.workdir !== undefined && { workdir: parentStory.workdir }
20352
20417
  }));
20353
20418
  prd.userStories.splice(originalIndex + 1, 0, ...newStories);
20354
20419
  }
@@ -20726,6 +20791,18 @@ function mergePackageConfig(root, packageOverride) {
20726
20791
  ...packageOverride.review,
20727
20792
  commands: {
20728
20793
  ...root.review.commands,
20794
+ ...packageOverride.quality?.commands?.lint !== undefined && {
20795
+ lint: packageOverride.quality.commands.lint
20796
+ },
20797
+ ...packageOverride.quality?.commands?.lintFix !== undefined && {
20798
+ lintFix: packageOverride.quality.commands.lintFix
20799
+ },
20800
+ ...packageOverride.quality?.commands?.typecheck !== undefined && {
20801
+ typecheck: packageOverride.quality.commands.typecheck
20802
+ },
20803
+ ...packageOverride.quality?.commands?.test !== undefined && {
20804
+ test: packageOverride.quality.commands.test
20805
+ },
20729
20806
  ...packageOverride.review?.commands
20730
20807
  }
20731
20808
  },
@@ -22216,7 +22293,7 @@ function markStoryPassed(prd, storyId) {
22216
22293
  story.status = "passed";
22217
22294
  }
22218
22295
  }
22219
- function markStoryFailed(prd, storyId, failureCategory) {
22296
+ function markStoryFailed(prd, storyId, failureCategory, failureStage) {
22220
22297
  const story = prd.userStories.find((s) => s.id === storyId);
22221
22298
  if (story) {
22222
22299
  story.status = "failed";
@@ -22224,6 +22301,9 @@ function markStoryFailed(prd, storyId, failureCategory) {
22224
22301
  if (failureCategory !== undefined) {
22225
22302
  story.failureCategory = failureCategory;
22226
22303
  }
22304
+ if (failureStage !== undefined) {
22305
+ story.failureStage = failureStage;
22306
+ }
22227
22307
  }
22228
22308
  }
22229
22309
  function markStorySkipped(prd, storyId) {
@@ -22250,7 +22330,7 @@ var package_default;
22250
22330
  var init_package = __esm(() => {
22251
22331
  package_default = {
22252
22332
  name: "@nathapp/nax",
22253
- version: "0.49.3",
22333
+ version: "0.50.0",
22254
22334
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22255
22335
  type: "module",
22256
22336
  bin: {
@@ -22323,8 +22403,8 @@ var init_version = __esm(() => {
22323
22403
  NAX_VERSION = package_default.version;
22324
22404
  NAX_COMMIT = (() => {
22325
22405
  try {
22326
- if (/^[0-9a-f]{6,10}$/.test("30ff375"))
22327
- return "30ff375";
22406
+ if (/^[0-9a-f]{6,10}$/.test("0eeefb4"))
22407
+ return "0eeefb4";
22328
22408
  } catch {}
22329
22409
  try {
22330
22410
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -23309,6 +23389,7 @@ class WebhookInteractionPlugin {
23309
23389
  server = null;
23310
23390
  serverStartPromise = null;
23311
23391
  pendingResponses = new Map;
23392
+ receiveCallbacks = new Map;
23312
23393
  async init(config2) {
23313
23394
  const cfg = WebhookConfigSchema.parse(config2);
23314
23395
  this.config = {
@@ -23358,27 +23439,39 @@ class WebhookInteractionPlugin {
23358
23439
  }
23359
23440
  async receive(requestId, timeout = 60000) {
23360
23441
  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);
23442
+ const early = this.pendingResponses.get(requestId);
23443
+ if (early) {
23444
+ this.pendingResponses.delete(requestId);
23445
+ return early;
23372
23446
  }
23373
- return {
23374
- requestId,
23375
- action: "skip",
23376
- respondedBy: "timeout",
23377
- respondedAt: Date.now()
23378
- };
23447
+ return new Promise((resolve7) => {
23448
+ const timer = setTimeout(() => {
23449
+ this.receiveCallbacks.delete(requestId);
23450
+ resolve7({
23451
+ requestId,
23452
+ action: "skip",
23453
+ respondedBy: "timeout",
23454
+ respondedAt: Date.now()
23455
+ });
23456
+ }, timeout);
23457
+ this.receiveCallbacks.set(requestId, (response) => {
23458
+ clearTimeout(timer);
23459
+ this.receiveCallbacks.delete(requestId);
23460
+ resolve7(response);
23461
+ });
23462
+ });
23379
23463
  }
23380
23464
  async cancel(requestId) {
23381
23465
  this.pendingResponses.delete(requestId);
23466
+ this.receiveCallbacks.delete(requestId);
23467
+ }
23468
+ deliverResponse(requestId, response) {
23469
+ const cb = this.receiveCallbacks.get(requestId);
23470
+ if (cb) {
23471
+ cb(response);
23472
+ } else {
23473
+ this.pendingResponses.set(requestId, response);
23474
+ }
23382
23475
  }
23383
23476
  async startServer() {
23384
23477
  if (this.server)
@@ -23431,7 +23524,7 @@ class WebhookInteractionPlugin {
23431
23524
  try {
23432
23525
  const parsed = JSON.parse(body);
23433
23526
  const response = InteractionResponseSchema.parse(parsed);
23434
- this.pendingResponses.set(requestId, response);
23527
+ this.deliverResponse(requestId, response);
23435
23528
  } catch {
23436
23529
  return new Response("Bad Request: Invalid response format", { status: 400 });
23437
23530
  }
@@ -23439,7 +23532,7 @@ class WebhookInteractionPlugin {
23439
23532
  try {
23440
23533
  const parsed = await req.json();
23441
23534
  const response = InteractionResponseSchema.parse(parsed);
23442
- this.pendingResponses.set(requestId, response);
23535
+ this.deliverResponse(requestId, response);
23443
23536
  } catch {
23444
23537
  return new Response("Bad Request: Invalid response format", { status: 400 });
23445
23538
  }
@@ -23466,12 +23559,9 @@ class WebhookInteractionPlugin {
23466
23559
  }
23467
23560
  }
23468
23561
  }
23469
- var _webhookPluginDeps, WebhookConfigSchema, InteractionResponseSchema;
23562
+ var WebhookConfigSchema, InteractionResponseSchema;
23470
23563
  var init_webhook = __esm(() => {
23471
23564
  init_zod();
23472
- _webhookPluginDeps = {
23473
- sleep: (ms) => Bun.sleep(ms)
23474
- };
23475
23565
  WebhookConfigSchema = exports_external.object({
23476
23566
  url: exports_external.string().url().optional(),
23477
23567
  callbackPort: exports_external.number().int().min(1024).max(65535).optional(),
@@ -24706,6 +24796,9 @@ ${c.output}
24706
24796
  \`\`\``).join(`
24707
24797
 
24708
24798
  `);
24799
+ const scopeConstraint = story.workdir ? `
24800
+
24801
+ IMPORTANT: Only modify files within \`${story.workdir}/\`. Do NOT touch files outside this directory.` : "";
24709
24802
  return `You are fixing lint/typecheck errors from a code review.
24710
24803
 
24711
24804
  Story: ${story.title} (${story.id})
@@ -24716,7 +24809,7 @@ ${errors3}
24716
24809
 
24717
24810
  Fix ALL errors listed above. Do NOT change test files or test behavior.
24718
24811
  Do NOT add new features \u2014 only fix the quality check errors.
24719
- Commit your fixes when done.`;
24812
+ Commit your fixes when done.${scopeConstraint}`;
24720
24813
  }
24721
24814
  async function runAgentRectification(ctx) {
24722
24815
  const logger = getLogger();
@@ -24743,9 +24836,10 @@ async function runAgentRectification(ctx) {
24743
24836
  const prompt = buildReviewRectificationPrompt(failedChecks, ctx.story);
24744
24837
  const modelTier = ctx.story.routing?.modelTier ?? ctx.config.autoMode.escalation.tierOrder[0]?.tier ?? "balanced";
24745
24838
  const modelDef = resolveModel(ctx.config.models[modelTier]);
24839
+ const rectificationWorkdir = ctx.story.workdir ? join18(ctx.workdir, ctx.story.workdir) : ctx.workdir;
24746
24840
  await agent.run({
24747
24841
  prompt,
24748
- workdir: ctx.workdir,
24842
+ workdir: rectificationWorkdir,
24749
24843
  modelTier,
24750
24844
  modelDef,
24751
24845
  timeoutSeconds: ctx.config.execution.sessionTimeoutSeconds,
@@ -25299,6 +25393,32 @@ var init_elements = __esm(() => {
25299
25393
  init_logger2();
25300
25394
  });
25301
25395
 
25396
+ // src/context/parent-context.ts
25397
+ function getParentOutputFiles(story, allStories) {
25398
+ if (!story.dependencies || story.dependencies.length === 0)
25399
+ return [];
25400
+ const parentFiles = [];
25401
+ for (const depId of story.dependencies) {
25402
+ const parent = allStories.find((s) => s.id === depId);
25403
+ if (parent?.outputFiles) {
25404
+ parentFiles.push(...parent.outputFiles);
25405
+ }
25406
+ }
25407
+ const unique = [...new Set(parentFiles)];
25408
+ return unique.filter((f) => !NOISE_PATTERNS.some((p) => p.test(f))).slice(0, MAX_PARENT_FILES);
25409
+ }
25410
+ var MAX_PARENT_FILES = 10, NOISE_PATTERNS;
25411
+ var init_parent_context = __esm(() => {
25412
+ NOISE_PATTERNS = [
25413
+ /\.test\.(ts|js|tsx|jsx)$/,
25414
+ /\.spec\.(ts|js|tsx|jsx)$/,
25415
+ /package-lock\.json$/,
25416
+ /bun\.lockb?$/,
25417
+ /\.gitignore$/,
25418
+ /^nax\//
25419
+ ];
25420
+ });
25421
+
25302
25422
  // src/context/test-scanner.ts
25303
25423
  import path6 from "path";
25304
25424
  var {Glob } = globalThis.Bun;
@@ -25646,6 +25766,18 @@ async function buildContext(storyContext, budget) {
25646
25766
  }
25647
25767
  }
25648
25768
  elements.push(createStoryContext(currentStory, 80));
25769
+ if (prd.analysis) {
25770
+ const analysisContent = `The following analysis was performed during the planning phase. Use it to understand the codebase context before implementing:
25771
+
25772
+ ${prd.analysis}`;
25773
+ elements.push({
25774
+ type: "planning-analysis",
25775
+ label: "Planning Analysis",
25776
+ content: analysisContent,
25777
+ priority: 88,
25778
+ tokens: estimateTokens(analysisContent)
25779
+ });
25780
+ }
25649
25781
  addDependencyElements(elements, currentStory, prd);
25650
25782
  await addTestCoverageElement(elements, storyContext, currentStory);
25651
25783
  await addFileElements(elements, storyContext, currentStory);
@@ -25706,6 +25838,15 @@ async function addFileElements(elements, storyContext, story) {
25706
25838
  if (fileInjection !== "keyword")
25707
25839
  return;
25708
25840
  let contextFiles = getContextFiles(story);
25841
+ const parentFiles = getParentOutputFiles(story, storyContext.prd?.userStories ?? []);
25842
+ if (parentFiles.length > 0) {
25843
+ const logger = getLogger();
25844
+ logger.info("context", "Injecting parent output files for context chaining", {
25845
+ storyId: story.id,
25846
+ parentFiles
25847
+ });
25848
+ contextFiles = [...new Set([...contextFiles, ...parentFiles])];
25849
+ }
25709
25850
  if (contextFiles.length === 0 && storyContext.config?.context?.autoDetect?.enabled !== false && storyContext.workdir) {
25710
25851
  const autoDetectConfig = storyContext.config?.context?.autoDetect;
25711
25852
  try {
@@ -25773,6 +25914,7 @@ var init_builder3 = __esm(() => {
25773
25914
  init_prd();
25774
25915
  init_auto_detect();
25775
25916
  init_elements();
25917
+ init_parent_context();
25776
25918
  init_test_scanner();
25777
25919
  init_elements();
25778
25920
  _deps5 = {
@@ -26037,13 +26179,13 @@ function isSourceFile(filePath) {
26037
26179
  return SRC_PATTERNS.some((pattern) => pattern.test(filePath));
26038
26180
  }
26039
26181
  async function getChangedFiles2(workdir, fromRef = "HEAD") {
26040
- const proc = Bun.spawn(["git", "diff", "--name-only", fromRef], {
26182
+ const proc = _isolationDeps.spawn(["git", "diff", "--name-only", fromRef], {
26041
26183
  cwd: workdir,
26042
26184
  stdout: "pipe",
26043
26185
  stderr: "pipe"
26044
26186
  });
26187
+ const output = await Bun.readableStreamToText(proc.stdout);
26045
26188
  await proc.exited;
26046
- const output = await new Response(proc.stdout).text();
26047
26189
  return output.trim().split(`
26048
26190
  `).filter(Boolean);
26049
26191
  }
@@ -26090,8 +26232,9 @@ async function verifyImplementerIsolation(workdir, beforeRef) {
26090
26232
  description: "Implementer should not modify test files"
26091
26233
  };
26092
26234
  }
26093
- var TEST_PATTERNS, SRC_PATTERNS;
26235
+ var _isolationDeps, TEST_PATTERNS, SRC_PATTERNS;
26094
26236
  var init_isolation = __esm(() => {
26237
+ _isolationDeps = { spawn: Bun.spawn };
26095
26238
  TEST_PATTERNS = [/^test\//, /^tests\//, /^__tests__\//, /\.spec\.\w+$/, /\.test\.\w+$/, /\.e2e-spec\.\w+$/];
26096
26239
  SRC_PATTERNS = [/^src\//, /^lib\//, /^packages\//];
26097
26240
  });
@@ -26241,6 +26384,22 @@ async function autoCommitIfDirty(workdir, stage, role, storyId) {
26241
26384
  await commitProc.exited;
26242
26385
  } catch {}
26243
26386
  }
26387
+ async function captureOutputFiles(workdir, baseRef, scopePrefix) {
26388
+ if (!baseRef)
26389
+ return [];
26390
+ try {
26391
+ const args = ["diff", "--name-only", `${baseRef}..HEAD`];
26392
+ if (scopePrefix)
26393
+ args.push("--", `${scopePrefix}/`);
26394
+ const proc = _gitDeps.spawn(["git", ...args], { cwd: workdir, stdout: "pipe", stderr: "pipe" });
26395
+ const output = await new Response(proc.stdout).text();
26396
+ await proc.exited;
26397
+ return output.trim().split(`
26398
+ `).filter(Boolean);
26399
+ } catch {
26400
+ return [];
26401
+ }
26402
+ }
26244
26403
  var _gitDeps, GIT_TIMEOUT_MS = 1e4;
26245
26404
  var init_git = __esm(() => {
26246
26405
  init_logger2();
@@ -26289,7 +26448,7 @@ async function executeWithTimeout(command, timeoutSeconds, env2, options) {
26289
26448
  const shell = options?.shell ?? "/bin/sh";
26290
26449
  const gracePeriodMs = options?.gracePeriodMs ?? 5000;
26291
26450
  const drainTimeoutMs = options?.drainTimeoutMs ?? 2000;
26292
- const proc = Bun.spawn([shell, "-c", command], {
26451
+ const proc = _executorDeps.spawn([shell, "-c", command], {
26293
26452
  stdout: "pipe",
26294
26453
  stderr: "pipe",
26295
26454
  env: env2 || normalizeEnvironment(process.env),
@@ -26383,8 +26542,9 @@ function buildTestCommand(baseCommand, options) {
26383
26542
  }
26384
26543
  return command;
26385
26544
  }
26386
- var DEFAULT_STRIP_ENV_VARS;
26545
+ var _executorDeps, DEFAULT_STRIP_ENV_VARS;
26387
26546
  var init_executor = __esm(() => {
26547
+ _executorDeps = { spawn: Bun.spawn };
26388
26548
  DEFAULT_STRIP_ENV_VARS = ["CLAUDECODE", "REPL_ID", "AGENT"];
26389
26549
  });
26390
26550
 
@@ -26684,15 +26844,15 @@ var init_verification = __esm(() => {
26684
26844
  // src/tdd/cleanup.ts
26685
26845
  async function getPgid(pid) {
26686
26846
  try {
26687
- const proc = Bun.spawn(["ps", "-o", "pgid=", "-p", String(pid)], {
26847
+ const proc = _cleanupDeps.spawn(["ps", "-o", "pgid=", "-p", String(pid)], {
26688
26848
  stdout: "pipe",
26689
26849
  stderr: "pipe"
26690
26850
  });
26851
+ const output = await Bun.readableStreamToText(proc.stdout);
26691
26852
  const exitCode = await proc.exited;
26692
26853
  if (exitCode !== 0) {
26693
26854
  return null;
26694
26855
  }
26695
- const output = await new Response(proc.stdout).text();
26696
26856
  const pgid = Number.parseInt(output.trim(), 10);
26697
26857
  return Number.isNaN(pgid) ? null : pgid;
26698
26858
  } catch {
@@ -26706,7 +26866,7 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
26706
26866
  return;
26707
26867
  }
26708
26868
  try {
26709
- process.kill(-pgid, "SIGTERM");
26869
+ _cleanupDeps.kill(-pgid, "SIGTERM");
26710
26870
  } catch (error48) {
26711
26871
  const err = error48;
26712
26872
  if (err.code !== "ESRCH") {
@@ -26714,11 +26874,11 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
26714
26874
  }
26715
26875
  return;
26716
26876
  }
26717
- await Bun.sleep(gracePeriodMs);
26877
+ await _cleanupDeps.sleep(gracePeriodMs);
26718
26878
  const pgidAfterWait = await getPgid(pid);
26719
26879
  if (pgidAfterWait && pgidAfterWait === pgid) {
26720
26880
  try {
26721
- process.kill(-pgid, "SIGKILL");
26881
+ _cleanupDeps.kill(-pgid, "SIGKILL");
26722
26882
  } catch {}
26723
26883
  }
26724
26884
  } catch (error48) {
@@ -26729,8 +26889,14 @@ async function cleanupProcessTree(pid, gracePeriodMs = 3000) {
26729
26889
  });
26730
26890
  }
26731
26891
  }
26892
+ var _cleanupDeps;
26732
26893
  var init_cleanup = __esm(() => {
26733
26894
  init_logger2();
26895
+ _cleanupDeps = {
26896
+ spawn: Bun.spawn,
26897
+ sleep: Bun.sleep,
26898
+ kill: process.kill.bind(process)
26899
+ };
26734
26900
  });
26735
26901
 
26736
26902
  // src/tdd/prompts.ts
@@ -26753,10 +26919,12 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
26753
26919
  storyId: story.id,
26754
26920
  timeout: fullSuiteTimeout
26755
26921
  });
26756
- const fullSuiteResult = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, { cwd: workdir });
26922
+ const fullSuiteResult = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
26923
+ cwd: workdir
26924
+ });
26757
26925
  const fullSuitePassed = fullSuiteResult.success && fullSuiteResult.exitCode === 0;
26758
26926
  if (!fullSuitePassed && fullSuiteResult.output) {
26759
- const testSummary = parseBunTestOutput(fullSuiteResult.output);
26927
+ const testSummary = _rectificationGateDeps.parseBunTestOutput(fullSuiteResult.output);
26760
26928
  if (testSummary.failed > 0) {
26761
26929
  return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout, featureName);
26762
26930
  }
@@ -26797,8 +26965,14 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26797
26965
  failedTests: testSummary.failed,
26798
26966
  passedTests: testSummary.passed
26799
26967
  });
26800
- while (shouldRetryRectification(rectificationState, rectificationConfig)) {
26968
+ const rectificationSessionName = buildSessionName(workdir, featureName, story.id, "implementer");
26969
+ logger.debug("tdd", "Rectification session name (shared across all attempts)", {
26970
+ storyId: story.id,
26971
+ sessionName: rectificationSessionName
26972
+ });
26973
+ while (_rectificationGateDeps.shouldRetryRectification(rectificationState, rectificationConfig)) {
26801
26974
  rectificationState.attempt++;
26975
+ const isLastAttempt = rectificationState.attempt >= rectificationConfig.maxRetries;
26802
26976
  logger.info("tdd", `-> Implementer rectification attempt ${rectificationState.attempt}/${rectificationConfig.maxRetries}`, { storyId: story.id, currentFailures: rectificationState.currentFailures });
26803
26977
  const rectificationPrompt = buildImplementerRectificationPrompt(testSummary.failures, story, contextMarkdown, rectificationConfig);
26804
26978
  const rectifyBeforeRef = await captureGitRef(workdir) ?? "HEAD";
@@ -26814,7 +26988,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26814
26988
  maxInteractionTurns: config2.agent?.maxInteractionTurns,
26815
26989
  featureName,
26816
26990
  storyId: story.id,
26817
- sessionRole: "implementer"
26991
+ sessionRole: "implementer",
26992
+ acpSessionName: rectificationSessionName,
26993
+ keepSessionOpen: !isLastAttempt
26818
26994
  });
26819
26995
  if (!rectifyResult.success && rectifyResult.pid) {
26820
26996
  await cleanupProcessTree(rectifyResult.pid);
@@ -26842,7 +27018,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26842
27018
  });
26843
27019
  break;
26844
27020
  }
26845
- const retryFullSuite = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, { cwd: workdir });
27021
+ const retryFullSuite = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
27022
+ cwd: workdir
27023
+ });
26846
27024
  const retrySuitePassed = retryFullSuite.success && retryFullSuite.exitCode === 0;
26847
27025
  if (retrySuitePassed) {
26848
27026
  logger.info("tdd", "Full suite gate passed after rectification!", {
@@ -26852,7 +27030,7 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26852
27030
  return true;
26853
27031
  }
26854
27032
  if (retryFullSuite.output) {
26855
- const newTestSummary = parseBunTestOutput(retryFullSuite.output);
27033
+ const newTestSummary = _rectificationGateDeps.parseBunTestOutput(retryFullSuite.output);
26856
27034
  rectificationState.currentFailures = newTestSummary.failed;
26857
27035
  testSummary.failures = newTestSummary.failures;
26858
27036
  testSummary.failed = newTestSummary.failed;
@@ -26864,7 +27042,9 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26864
27042
  remainingFailures: rectificationState.currentFailures
26865
27043
  });
26866
27044
  }
26867
- const finalFullSuite = await executeWithTimeout(testCmd, fullSuiteTimeout, undefined, { cwd: workdir });
27045
+ const finalFullSuite = await _rectificationGateDeps.executeWithTimeout(testCmd, fullSuiteTimeout, undefined, {
27046
+ cwd: workdir
27047
+ });
26868
27048
  const finalSuitePassed = finalFullSuite.success && finalFullSuite.exitCode === 0;
26869
27049
  if (!finalSuitePassed) {
26870
27050
  logger.warn("tdd", "[WARN] Full suite gate failed after rectification exhausted", {
@@ -26877,13 +27057,20 @@ async function runRectificationLoop(story, config2, workdir, agent, implementerT
26877
27057
  logger.info("tdd", "Full suite gate passed", { storyId: story.id });
26878
27058
  return true;
26879
27059
  }
27060
+ var _rectificationGateDeps;
26880
27061
  var init_rectification_gate = __esm(() => {
27062
+ init_adapter2();
26881
27063
  init_config();
26882
27064
  init_git();
26883
27065
  init_verification();
26884
27066
  init_cleanup();
26885
27067
  init_isolation();
26886
27068
  init_prompts();
27069
+ _rectificationGateDeps = {
27070
+ executeWithTimeout,
27071
+ parseBunTestOutput,
27072
+ shouldRetryRectification
27073
+ };
26887
27074
  });
26888
27075
 
26889
27076
  // src/prompts/sections/conventions.ts
@@ -27373,7 +27560,7 @@ ${tail}`;
27373
27560
  async function rollbackToRef(workdir, ref) {
27374
27561
  const logger = getLogger();
27375
27562
  logger.warn("tdd", "Rolling back git changes", { ref });
27376
- const resetProc = Bun.spawn(["git", "reset", "--hard", ref], {
27563
+ const resetProc = _sessionRunnerDeps.spawn(["git", "reset", "--hard", ref], {
27377
27564
  cwd: workdir,
27378
27565
  stdout: "pipe",
27379
27566
  stderr: "pipe"
@@ -27384,7 +27571,7 @@ async function rollbackToRef(workdir, ref) {
27384
27571
  logger.error("tdd", "Failed to rollback git changes", { ref, stderr });
27385
27572
  throw new Error(`Git rollback failed: ${stderr}`);
27386
27573
  }
27387
- const cleanProc = Bun.spawn(["git", "clean", "-fd"], {
27574
+ const cleanProc = _sessionRunnerDeps.spawn(["git", "clean", "-fd"], {
27388
27575
  cwd: workdir,
27389
27576
  stdout: "pipe",
27390
27577
  stderr: "pipe"
@@ -27399,19 +27586,24 @@ async function rollbackToRef(workdir, ref) {
27399
27586
  async function runTddSession(role, agent, story, config2, workdir, modelTier, beforeRef, contextMarkdown, lite = false, skipIsolation = false, constitution, featureName) {
27400
27587
  const startTime = Date.now();
27401
27588
  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;
27589
+ if (_sessionRunnerDeps.buildPrompt) {
27590
+ prompt = await _sessionRunnerDeps.buildPrompt(role, config2, story, workdir, contextMarkdown, lite, constitution);
27591
+ } else {
27592
+ switch (role) {
27593
+ case "test-writer":
27594
+ 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();
27595
+ break;
27596
+ case "implementer":
27597
+ prompt = await PromptBuilder.for("implementer", { variant: lite ? "lite" : "standard" }).withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
27598
+ break;
27599
+ case "verifier":
27600
+ prompt = await PromptBuilder.for("verifier").withLoader(workdir, config2).story(story).context(contextMarkdown).constitution(constitution).testCommand(config2.quality?.commands?.test).build();
27601
+ break;
27602
+ }
27412
27603
  }
27413
27604
  const logger = getLogger();
27414
27605
  logger.info("tdd", `-> Session: ${role}`, { role, storyId: story.id, lite });
27606
+ const keepSessionOpen = role === "implementer" && (config2.execution.rectification?.enabled ?? false);
27415
27607
  const result = await agent.run({
27416
27608
  prompt,
27417
27609
  workdir,
@@ -27424,10 +27616,11 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
27424
27616
  maxInteractionTurns: config2.agent?.maxInteractionTurns,
27425
27617
  featureName,
27426
27618
  storyId: story.id,
27427
- sessionRole: role
27619
+ sessionRole: role,
27620
+ keepSessionOpen
27428
27621
  });
27429
27622
  if (!result.success && result.pid) {
27430
- await cleanupProcessTree(result.pid);
27623
+ await _sessionRunnerDeps.cleanupProcessTree(result.pid);
27431
27624
  }
27432
27625
  if (result.success) {
27433
27626
  logger.info("tdd", `Session complete: ${role}`, {
@@ -27449,12 +27642,12 @@ async function runTddSession(role, agent, story, config2, workdir, modelTier, be
27449
27642
  if (!skipIsolation) {
27450
27643
  if (role === "test-writer") {
27451
27644
  const allowedPaths = config2.tdd.testWriterAllowedPaths ?? ["src/index.ts", "src/**/index.ts"];
27452
- isolation = await verifyTestWriterIsolation(workdir, beforeRef, allowedPaths);
27645
+ isolation = await _sessionRunnerDeps.verifyTestWriterIsolation(workdir, beforeRef, allowedPaths);
27453
27646
  } else if (role === "implementer" || role === "verifier") {
27454
- isolation = await verifyImplementerIsolation(workdir, beforeRef);
27647
+ isolation = await _sessionRunnerDeps.verifyImplementerIsolation(workdir, beforeRef);
27455
27648
  }
27456
27649
  }
27457
- const filesChanged = await getChangedFiles2(workdir, beforeRef);
27650
+ const filesChanged = await _sessionRunnerDeps.getChangedFiles(workdir, beforeRef);
27458
27651
  const durationMs = Date.now() - startTime;
27459
27652
  if (isolation && !isolation.passed) {
27460
27653
  logger.error("tdd", "Isolation violated", {
@@ -27497,10 +27690,18 @@ var init_session_runner = __esm(() => {
27497
27690
  init_logger2();
27498
27691
  init_prompts2();
27499
27692
  init_git();
27693
+ init_git();
27500
27694
  init_cleanup();
27501
27695
  init_isolation();
27502
27696
  _sessionRunnerDeps = {
27503
- autoCommitIfDirty
27697
+ autoCommitIfDirty,
27698
+ spawn: Bun.spawn,
27699
+ getChangedFiles: getChangedFiles2,
27700
+ verifyTestWriterIsolation,
27701
+ verifyImplementerIsolation,
27702
+ captureGitRef,
27703
+ cleanupProcessTree,
27704
+ buildPrompt: null
27504
27705
  };
27505
27706
  });
27506
27707
 
@@ -28981,7 +29182,7 @@ class AcceptanceStrategy {
28981
29182
  }
28982
29183
  const start = Date.now();
28983
29184
  const timeoutMs = ctx.timeoutSeconds * 1000;
28984
- const proc = Bun.spawn(["bun", "test", testPath], {
29185
+ const proc = _acceptanceDeps.spawn(["bun", "test", testPath], {
28985
29186
  cwd: ctx.workdir,
28986
29187
  stdout: "pipe",
28987
29188
  stderr: "pipe"
@@ -29049,8 +29250,10 @@ ${stderr}`;
29049
29250
  });
29050
29251
  }
29051
29252
  }
29253
+ var _acceptanceDeps;
29052
29254
  var init_acceptance3 = __esm(() => {
29053
29255
  init_logger2();
29256
+ _acceptanceDeps = { spawn: Bun.spawn };
29054
29257
  });
29055
29258
 
29056
29259
  // src/verification/strategies/regression.ts
@@ -33047,7 +33250,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
33047
33250
  worktreePath
33048
33251
  });
33049
33252
  } catch (error48) {
33050
- markStoryFailed(currentPrd, story.id);
33253
+ markStoryFailed(currentPrd, story.id, undefined, undefined);
33051
33254
  logger?.error("parallel", "Failed to create worktree", {
33052
33255
  storyId: story.id,
33053
33256
  error: errorMessage(error48)
@@ -33075,7 +33278,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
33075
33278
  retryCount: mergeResult.retryCount
33076
33279
  });
33077
33280
  } else {
33078
- markStoryFailed(currentPrd, mergeResult.storyId);
33281
+ markStoryFailed(currentPrd, mergeResult.storyId, undefined, undefined);
33079
33282
  batchResult.mergeConflicts.push({
33080
33283
  storyId: mergeResult.storyId,
33081
33284
  conflictFiles: mergeResult.conflictFiles || [],
@@ -33093,7 +33296,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
33093
33296
  }
33094
33297
  }
33095
33298
  for (const { story, error: error48 } of batchResult.failed) {
33096
- markStoryFailed(currentPrd, story.id);
33299
+ markStoryFailed(currentPrd, story.id, undefined, undefined);
33097
33300
  logger?.error("parallel", "Cleaning up failed story worktree", {
33098
33301
  storyId: story.id,
33099
33302
  error: error48
@@ -34030,7 +34233,7 @@ async function handleNoTierAvailable(ctx, failureCategory) {
34030
34233
  return { outcome: "paused", prdDirty: true, prd: pausedPrd };
34031
34234
  }
34032
34235
  const failedPrd = { ...ctx.prd };
34033
- markStoryFailed(failedPrd, ctx.story.id, failureCategory);
34236
+ markStoryFailed(failedPrd, ctx.story.id, failureCategory, undefined);
34034
34237
  await savePRD(failedPrd, ctx.prdPath);
34035
34238
  logger?.error("execution", "Story failed - execution failed", {
34036
34239
  storyId: ctx.story.id
@@ -34070,7 +34273,7 @@ async function handleMaxAttemptsReached(ctx, failureCategory) {
34070
34273
  return { outcome: "paused", prdDirty: true, prd: pausedPrd };
34071
34274
  }
34072
34275
  const failedPrd = { ...ctx.prd };
34073
- markStoryFailed(failedPrd, ctx.story.id, failureCategory);
34276
+ markStoryFailed(failedPrd, ctx.story.id, failureCategory, undefined);
34074
34277
  await savePRD(failedPrd, ctx.prdPath);
34075
34278
  logger?.error("execution", "Story failed - max attempts reached", {
34076
34279
  storyId: ctx.story.id,
@@ -34235,6 +34438,17 @@ var init_escalation = __esm(() => {
34235
34438
  });
34236
34439
 
34237
34440
  // src/execution/pipeline-result-handler.ts
34441
+ function filterOutputFiles(files) {
34442
+ const NOISE = [
34443
+ /\.test\.(ts|js|tsx|jsx)$/,
34444
+ /\.spec\.(ts|js|tsx|jsx)$/,
34445
+ /package-lock\.json$/,
34446
+ /bun\.lock(b?)$/,
34447
+ /\.gitignore$/,
34448
+ /^nax\//
34449
+ ];
34450
+ return files.filter((f) => !NOISE.some((p) => p.test(f))).slice(0, 15);
34451
+ }
34238
34452
  async function handlePipelineSuccess(ctx, pipelineResult) {
34239
34453
  const logger = getSafeLogger();
34240
34454
  const costDelta = pipelineResult.context.agentResult?.estimatedCost || 0;
@@ -34263,6 +34477,17 @@ async function handlePipelineSuccess(ctx, pipelineResult) {
34263
34477
  testStrategy: ctx.routing.testStrategy
34264
34478
  });
34265
34479
  }
34480
+ if (ctx.storyGitRef) {
34481
+ for (const completedStory of ctx.storiesToExecute) {
34482
+ try {
34483
+ const rawFiles = await captureOutputFiles(ctx.workdir, ctx.storyGitRef, completedStory.workdir);
34484
+ const filtered = filterOutputFiles(rawFiles);
34485
+ if (filtered.length > 0) {
34486
+ completedStory.outputFiles = filtered;
34487
+ }
34488
+ } catch {}
34489
+ }
34490
+ }
34266
34491
  const updatedCounts = countStories(prd);
34267
34492
  logger?.info("progress", "Progress update", {
34268
34493
  totalStories: updatedCounts.total,
@@ -34299,7 +34524,7 @@ async function handlePipelineFailure(ctx, pipelineResult) {
34299
34524
  prdDirty = true;
34300
34525
  break;
34301
34526
  case "fail":
34302
- markStoryFailed(prd, ctx.story.id, pipelineResult.context.tddFailureCategory);
34527
+ markStoryFailed(prd, ctx.story.id, pipelineResult.context.tddFailureCategory, pipelineResult.stoppedAtStage);
34303
34528
  await savePRD(prd, ctx.prdPath);
34304
34529
  prdDirty = true;
34305
34530
  logger?.error("pipeline", "Story failed", { storyId: ctx.story.id, reason: pipelineResult.reason });
@@ -34353,6 +34578,7 @@ var init_pipeline_result_handler = __esm(() => {
34353
34578
  init_logger2();
34354
34579
  init_event_bus();
34355
34580
  init_prd();
34581
+ init_git();
34356
34582
  init_escalation();
34357
34583
  init_progress();
34358
34584
  });
@@ -35007,25 +35233,44 @@ var init_precheck_runner = __esm(() => {
35007
35233
  var exports_run_initialization = {};
35008
35234
  __export(exports_run_initialization, {
35009
35235
  logActiveProtocol: () => logActiveProtocol,
35010
- initializeRun: () => initializeRun
35236
+ initializeRun: () => initializeRun,
35237
+ _reconcileDeps: () => _reconcileDeps
35011
35238
  });
35012
- async function reconcileState(prd, prdPath, workdir) {
35239
+ import { join as join51 } from "path";
35240
+ async function reconcileState(prd, prdPath, workdir, config2) {
35013
35241
  const logger = getSafeLogger();
35014
35242
  let reconciledCount = 0;
35015
35243
  let modified = false;
35016
35244
  for (const story of prd.userStories) {
35017
- if (story.status === "failed") {
35018
- const hasCommits = await hasCommitsForStory(workdir, story.id);
35019
- if (hasCommits) {
35020
- logger?.warn("reconciliation", "Failed story has commits in git history, marking as passed", {
35021
- storyId: story.id,
35022
- title: story.title
35023
- });
35024
- markStoryPassed(prd, story.id);
35025
- reconciledCount++;
35026
- modified = true;
35245
+ if (story.status !== "failed")
35246
+ continue;
35247
+ const hasCommits = await _reconcileDeps.hasCommitsForStory(workdir, story.id);
35248
+ if (!hasCommits)
35249
+ continue;
35250
+ if (story.failureStage === "review" || story.failureStage === "autofix") {
35251
+ const effectiveWorkdir = story.workdir ? join51(workdir, story.workdir) : workdir;
35252
+ try {
35253
+ const reviewResult = await _reconcileDeps.runReview(config2.review, effectiveWorkdir, config2.execution);
35254
+ if (!reviewResult.success) {
35255
+ logger?.warn("reconciliation", "Review still fails \u2014 not reconciling story", {
35256
+ storyId: story.id,
35257
+ failureReason: reviewResult.failureReason
35258
+ });
35259
+ continue;
35260
+ }
35261
+ logger?.info("reconciliation", "Review now passes \u2014 reconciling story", { storyId: story.id });
35262
+ } catch {
35263
+ logger?.warn("reconciliation", "Review check errored \u2014 not reconciling story", { storyId: story.id });
35264
+ continue;
35027
35265
  }
35028
35266
  }
35267
+ logger?.warn("reconciliation", "Failed story has commits in git history, marking as passed", {
35268
+ storyId: story.id,
35269
+ title: story.title
35270
+ });
35271
+ markStoryPassed(prd, story.id);
35272
+ reconciledCount++;
35273
+ modified = true;
35029
35274
  }
35030
35275
  if (reconciledCount > 0) {
35031
35276
  logger?.info("reconciliation", `Reconciled ${reconciledCount} failed stories from git history`);
@@ -35075,7 +35320,7 @@ async function initializeRun(ctx) {
35075
35320
  const logger = getSafeLogger();
35076
35321
  await checkAgentInstalled(ctx.config, ctx.dryRun, ctx.agentGetFn);
35077
35322
  let prd = await loadPRD(ctx.prdPath);
35078
- prd = await reconcileState(prd, ctx.prdPath, ctx.workdir);
35323
+ prd = await reconcileState(prd, ctx.prdPath, ctx.workdir, ctx.config);
35079
35324
  const counts = countStories(prd);
35080
35325
  validateStoryCount(counts, ctx.config);
35081
35326
  logger?.info("execution", "Run initialization complete", {
@@ -35085,11 +35330,17 @@ async function initializeRun(ctx) {
35085
35330
  });
35086
35331
  return { prd, storyCounts: counts };
35087
35332
  }
35333
+ var _reconcileDeps;
35088
35334
  var init_run_initialization = __esm(() => {
35089
35335
  init_errors3();
35090
35336
  init_logger2();
35091
35337
  init_prd();
35338
+ init_runner2();
35092
35339
  init_git();
35340
+ _reconcileDeps = {
35341
+ hasCommitsForStory: (workdir, storyId) => hasCommitsForStory(workdir, storyId),
35342
+ runReview: (reviewConfig, workdir, executionConfig) => runReview(reviewConfig, workdir, executionConfig)
35343
+ };
35093
35344
  });
35094
35345
 
35095
35346
  // src/execution/lifecycle/run-setup.ts
@@ -66131,7 +66382,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
66131
66382
  init_source();
66132
66383
  import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
66133
66384
  import { homedir as homedir10 } from "os";
66134
- import { join as join51 } from "path";
66385
+ import { join as join52 } from "path";
66135
66386
 
66136
66387
  // node_modules/commander/esm.mjs
66137
66388
  var import__ = __toESM(require_commander(), 1);
@@ -67557,14 +67808,48 @@ For each user story, set the "workdir" field to the relevant package path (e.g.
67557
67808
  "workdir": "string \u2014 optional, relative path to package (e.g. \\"packages/api\\"). Omit for root-level stories.",` : "";
67558
67809
  return `You are a senior software architect generating a product requirements document (PRD) as JSON.
67559
67810
 
67811
+ ## Step 1: Understand the Spec
67812
+
67813
+ Read the spec carefully. Identify the goal, scope, constraints, and what "done" looks like.
67814
+
67560
67815
  ## Spec
67561
67816
 
67562
67817
  ${specContent}
67563
67818
 
67819
+ ## Step 2: Analyze
67820
+
67821
+ Examine the codebase context below.
67822
+
67823
+ If the codebase has existing code (refactoring, enhancement, bug fix):
67824
+ - Which existing files need modification?
67825
+ - Which files import from or depend on them?
67826
+ - What tests cover the affected code?
67827
+ - What are the risks (breaking changes, backward compatibility)?
67828
+ - What is the migration path?
67829
+
67830
+ If this is a greenfield project (empty or minimal codebase):
67831
+ - What is the target architecture?
67832
+ - What are the key technical decisions (framework, patterns, conventions)?
67833
+ - What should be built first (dependency order)?
67834
+
67835
+ Record ALL findings in the "analysis" field of the output JSON. This analysis is provided to every implementation agent as context \u2014 be thorough.
67836
+
67564
67837
  ## Codebase Context
67565
67838
 
67566
67839
  ${codebaseContext}${monorepoHint}
67567
67840
 
67841
+ ## Step 3: Generate Implementation Stories
67842
+
67843
+ Based on your Step 2 analysis, create stories that produce CODE CHANGES.
67844
+
67845
+ ${GROUPING_RULES}
67846
+
67847
+ For each story, set "contextFiles" to the key source files the agent should read before implementing (max 5 per story). Use your Step 2 analysis to identify the most relevant files. Leave empty for greenfield stories with no existing files to reference.
67848
+
67849
+ ${COMPLEXITY_GUIDE}
67850
+
67851
+ ${TEST_STRATEGY_GUIDE}
67852
+
67568
67853
  ## Output Schema
67569
67854
 
67570
67855
  Generate a JSON object with this exact structure (no markdown, no explanation \u2014 JSON only):
@@ -67572,6 +67857,7 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
67572
67857
  {
67573
67858
  "project": "string \u2014 project name",
67574
67859
  "feature": "string \u2014 feature name",
67860
+ "analysis": "string \u2014 your Step 2 analysis: key files, impact areas, risks, architecture decisions, migration notes. All implementation agents will receive this.",
67575
67861
  "branchName": "string \u2014 git branch (e.g. feat/my-feature)",
67576
67862
  "createdAt": "ISO 8601 timestamp",
67577
67863
  "updatedAt": "ISO 8601 timestamp",
@@ -67581,13 +67867,14 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
67581
67867
  "title": "string \u2014 concise story title",
67582
67868
  "description": "string \u2014 detailed description of the story",
67583
67869
  "acceptanceCriteria": ["string \u2014 each AC line"],
67870
+ "contextFiles": ["string \u2014 key source files the agent should read (max 5, relative paths)"],
67584
67871
  "tags": ["string \u2014 routing tags, e.g. feature, security, api"],
67585
67872
  "dependencies": ["string \u2014 story IDs this story depends on"],${workdirField}
67586
67873
  "status": "pending",
67587
67874
  "passes": false,
67588
67875
  "routing": {
67589
67876
  "complexity": "simple | medium | complex | expert",
67590
- "testStrategy": "test-after | tdd-simple | three-session-tdd | three-session-tdd-lite",
67877
+ "testStrategy": "tdd-simple | three-session-tdd-lite | three-session-tdd | test-after",
67591
67878
  "reasoning": "string \u2014 brief classification rationale"
67592
67879
  },
67593
67880
  "escalations": [],
@@ -67596,12 +67883,6 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
67596
67883
  ]
67597
67884
  }
67598
67885
 
67599
- ${COMPLEXITY_GUIDE}
67600
-
67601
- ${TEST_STRATEGY_GUIDE}
67602
-
67603
- ${GROUPING_RULES}
67604
-
67605
67886
  ${outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
67606
67887
  Do NOT output the JSON to the conversation. Write the file, then reply with a brief confirmation.` : "Output ONLY the JSON object. Do not wrap in markdown code blocks."}`;
67607
67888
  }
@@ -68243,6 +68524,45 @@ class PipelineEventEmitter {
68243
68524
  init_stages();
68244
68525
  init_prd();
68245
68526
 
68527
+ // src/cli/prompts-shared.ts
68528
+ function buildFrontmatter(story, ctx, role) {
68529
+ const lines = [];
68530
+ lines.push(`storyId: ${story.id}`);
68531
+ lines.push(`title: "${story.title}"`);
68532
+ lines.push(`testStrategy: ${ctx.routing.testStrategy}`);
68533
+ lines.push(`modelTier: ${ctx.routing.modelTier}`);
68534
+ if (role) {
68535
+ lines.push(`role: ${role}`);
68536
+ }
68537
+ const builtContext = ctx.builtContext;
68538
+ const contextTokens = builtContext?.totalTokens ?? 0;
68539
+ const promptTokens = ctx.prompt ? Math.ceil(ctx.prompt.length / 3) : 0;
68540
+ lines.push(`contextTokens: ${contextTokens}`);
68541
+ lines.push(`promptTokens: ${promptTokens}`);
68542
+ if (story.dependencies && story.dependencies.length > 0) {
68543
+ lines.push(`dependencies: [${story.dependencies.join(", ")}]`);
68544
+ }
68545
+ lines.push("contextElements:");
68546
+ if (builtContext) {
68547
+ for (const element of builtContext.elements) {
68548
+ lines.push(` - type: ${element.type}`);
68549
+ if (element.storyId) {
68550
+ lines.push(` storyId: ${element.storyId}`);
68551
+ }
68552
+ if (element.filePath) {
68553
+ lines.push(` filePath: ${element.filePath}`);
68554
+ }
68555
+ lines.push(` tokens: ${element.tokens}`);
68556
+ }
68557
+ }
68558
+ if (builtContext?.truncated) {
68559
+ lines.push("truncated: true");
68560
+ }
68561
+ return `${lines.join(`
68562
+ `)}
68563
+ `;
68564
+ }
68565
+
68246
68566
  // src/cli/prompts-tdd.ts
68247
68567
  init_prompts2();
68248
68568
  import { join as join28 } from "path";
@@ -68388,43 +68708,6 @@ ${"=".repeat(80)}`);
68388
68708
  });
68389
68709
  return processedStories;
68390
68710
  }
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
68711
  // src/cli/prompts-init.ts
68429
68712
  import { existsSync as existsSync19, mkdirSync as mkdirSync4 } from "fs";
68430
68713
  import { join as join30 } from "path";
@@ -77916,15 +78199,15 @@ Next: nax generate --package ${options.package}`));
77916
78199
  }
77917
78200
  return;
77918
78201
  }
77919
- const naxDir = join51(workdir, "nax");
78202
+ const naxDir = join52(workdir, "nax");
77920
78203
  if (existsSync34(naxDir) && !options.force) {
77921
78204
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
77922
78205
  return;
77923
78206
  }
77924
- mkdirSync6(join51(naxDir, "features"), { recursive: true });
77925
- mkdirSync6(join51(naxDir, "hooks"), { recursive: true });
77926
- await Bun.write(join51(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
77927
- await Bun.write(join51(naxDir, "hooks.json"), JSON.stringify({
78207
+ mkdirSync6(join52(naxDir, "features"), { recursive: true });
78208
+ mkdirSync6(join52(naxDir, "hooks"), { recursive: true });
78209
+ await Bun.write(join52(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
78210
+ await Bun.write(join52(naxDir, "hooks.json"), JSON.stringify({
77928
78211
  hooks: {
77929
78212
  "on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
77930
78213
  "on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
@@ -77932,12 +78215,12 @@ Next: nax generate --package ${options.package}`));
77932
78215
  "on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
77933
78216
  }
77934
78217
  }, null, 2));
77935
- await Bun.write(join51(naxDir, ".gitignore"), `# nax temp files
78218
+ await Bun.write(join52(naxDir, ".gitignore"), `# nax temp files
77936
78219
  *.tmp
77937
78220
  .paused.json
77938
78221
  .nax-verifier-verdict.json
77939
78222
  `);
77940
- await Bun.write(join51(naxDir, "context.md"), `# Project Context
78223
+ await Bun.write(join52(naxDir, "context.md"), `# Project Context
77941
78224
 
77942
78225
  This document defines coding standards, architectural decisions, and forbidden patterns for this project.
77943
78226
  Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
@@ -78063,8 +78346,8 @@ program2.command("run").description("Run the orchestration loop for a feature").
78063
78346
  console.error(source_default.red("nax not initialized. Run: nax init"));
78064
78347
  process.exit(1);
78065
78348
  }
78066
- const featureDir = join51(naxDir, "features", options.feature);
78067
- const prdPath = join51(featureDir, "prd.json");
78349
+ const featureDir = join52(naxDir, "features", options.feature);
78350
+ const prdPath = join52(featureDir, "prd.json");
78068
78351
  if (options.plan && options.from) {
78069
78352
  if (existsSync34(prdPath) && !options.force) {
78070
78353
  console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
@@ -78086,10 +78369,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
78086
78369
  }
78087
78370
  }
78088
78371
  try {
78089
- const planLogDir = join51(featureDir, "plan");
78372
+ const planLogDir = join52(featureDir, "plan");
78090
78373
  mkdirSync6(planLogDir, { recursive: true });
78091
78374
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
78092
- const planLogPath = join51(planLogDir, `${planLogId}.jsonl`);
78375
+ const planLogPath = join52(planLogDir, `${planLogId}.jsonl`);
78093
78376
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
78094
78377
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
78095
78378
  console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
@@ -78127,10 +78410,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
78127
78410
  process.exit(1);
78128
78411
  }
78129
78412
  resetLogger();
78130
- const runsDir = join51(featureDir, "runs");
78413
+ const runsDir = join52(featureDir, "runs");
78131
78414
  mkdirSync6(runsDir, { recursive: true });
78132
78415
  const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
78133
- const logFilePath = join51(runsDir, `${runId}.jsonl`);
78416
+ const logFilePath = join52(runsDir, `${runId}.jsonl`);
78134
78417
  const isTTY = process.stdout.isTTY ?? false;
78135
78418
  const headlessFlag = options.headless ?? false;
78136
78419
  const headlessEnv = process.env.NAX_HEADLESS === "1";
@@ -78146,7 +78429,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
78146
78429
  config2.autoMode.defaultAgent = options.agent;
78147
78430
  }
78148
78431
  config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
78149
- const globalNaxDir = join51(homedir10(), ".nax");
78432
+ const globalNaxDir = join52(homedir10(), ".nax");
78150
78433
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
78151
78434
  const eventEmitter = new PipelineEventEmitter;
78152
78435
  let tuiInstance;
@@ -78169,7 +78452,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
78169
78452
  } else {
78170
78453
  console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
78171
78454
  }
78172
- const statusFilePath = join51(workdir, "nax", "status.json");
78455
+ const statusFilePath = join52(workdir, "nax", "status.json");
78173
78456
  let parallel;
78174
78457
  if (options.parallel !== undefined) {
78175
78458
  parallel = Number.parseInt(options.parallel, 10);
@@ -78195,7 +78478,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
78195
78478
  headless: useHeadless,
78196
78479
  skipPrecheck: options.skipPrecheck ?? false
78197
78480
  });
78198
- const latestSymlink = join51(runsDir, "latest.jsonl");
78481
+ const latestSymlink = join52(runsDir, "latest.jsonl");
78199
78482
  try {
78200
78483
  if (existsSync34(latestSymlink)) {
78201
78484
  Bun.spawnSync(["rm", latestSymlink]);
@@ -78233,9 +78516,9 @@ features.command("create <name>").description("Create a new feature").option("-d
78233
78516
  console.error(source_default.red("nax not initialized. Run: nax init"));
78234
78517
  process.exit(1);
78235
78518
  }
78236
- const featureDir = join51(naxDir, "features", name);
78519
+ const featureDir = join52(naxDir, "features", name);
78237
78520
  mkdirSync6(featureDir, { recursive: true });
78238
- await Bun.write(join51(featureDir, "spec.md"), `# Feature: ${name}
78521
+ await Bun.write(join52(featureDir, "spec.md"), `# Feature: ${name}
78239
78522
 
78240
78523
  ## Overview
78241
78524
 
@@ -78243,7 +78526,7 @@ features.command("create <name>").description("Create a new feature").option("-d
78243
78526
 
78244
78527
  ## Acceptance Criteria
78245
78528
  `);
78246
- await Bun.write(join51(featureDir, "plan.md"), `# Plan: ${name}
78529
+ await Bun.write(join52(featureDir, "plan.md"), `# Plan: ${name}
78247
78530
 
78248
78531
  ## Architecture
78249
78532
 
@@ -78251,7 +78534,7 @@ features.command("create <name>").description("Create a new feature").option("-d
78251
78534
 
78252
78535
  ## Dependencies
78253
78536
  `);
78254
- await Bun.write(join51(featureDir, "tasks.md"), `# Tasks: ${name}
78537
+ await Bun.write(join52(featureDir, "tasks.md"), `# Tasks: ${name}
78255
78538
 
78256
78539
  ## US-001: [Title]
78257
78540
 
@@ -78260,7 +78543,7 @@ features.command("create <name>").description("Create a new feature").option("-d
78260
78543
  ### Acceptance Criteria
78261
78544
  - [ ] Criterion 1
78262
78545
  `);
78263
- await Bun.write(join51(featureDir, "progress.txt"), `# Progress: ${name}
78546
+ await Bun.write(join52(featureDir, "progress.txt"), `# Progress: ${name}
78264
78547
 
78265
78548
  Created: ${new Date().toISOString()}
78266
78549
 
@@ -78288,7 +78571,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
78288
78571
  console.error(source_default.red("nax not initialized."));
78289
78572
  process.exit(1);
78290
78573
  }
78291
- const featuresDir = join51(naxDir, "features");
78574
+ const featuresDir = join52(naxDir, "features");
78292
78575
  if (!existsSync34(featuresDir)) {
78293
78576
  console.log(source_default.dim("No features yet."));
78294
78577
  return;
@@ -78303,7 +78586,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
78303
78586
  Features:
78304
78587
  `));
78305
78588
  for (const name of entries) {
78306
- const prdPath = join51(featuresDir, name, "prd.json");
78589
+ const prdPath = join52(featuresDir, name, "prd.json");
78307
78590
  if (existsSync34(prdPath)) {
78308
78591
  const prd = await loadPRD(prdPath);
78309
78592
  const c = countStories(prd);
@@ -78334,10 +78617,10 @@ Use: nax plan -f <feature> --from <spec>`));
78334
78617
  process.exit(1);
78335
78618
  }
78336
78619
  const config2 = await loadConfig(workdir);
78337
- const featureLogDir = join51(naxDir, "features", options.feature, "plan");
78620
+ const featureLogDir = join52(naxDir, "features", options.feature, "plan");
78338
78621
  mkdirSync6(featureLogDir, { recursive: true });
78339
78622
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
78340
- const planLogPath = join51(featureLogDir, `${planLogId}.jsonl`);
78623
+ const planLogPath = join52(featureLogDir, `${planLogId}.jsonl`);
78341
78624
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
78342
78625
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
78343
78626
  try {
@@ -78374,7 +78657,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
78374
78657
  console.error(source_default.red("nax not initialized. Run: nax init"));
78375
78658
  process.exit(1);
78376
78659
  }
78377
- const featureDir = join51(naxDir, "features", options.feature);
78660
+ const featureDir = join52(naxDir, "features", options.feature);
78378
78661
  if (!existsSync34(featureDir)) {
78379
78662
  console.error(source_default.red(`Feature "${options.feature}" not found.`));
78380
78663
  process.exit(1);
@@ -78390,7 +78673,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
78390
78673
  specPath: options.from,
78391
78674
  reclassify: options.reclassify
78392
78675
  });
78393
- const prdPath = join51(featureDir, "prd.json");
78676
+ const prdPath = join52(featureDir, "prd.json");
78394
78677
  await Bun.write(prdPath, JSON.stringify(prd, null, 2));
78395
78678
  const c = countStories(prd);
78396
78679
  console.log(source_default.green(`