@nathapp/nax 0.49.6 → 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 = [
@@ -18744,6 +18745,17 @@ IMPORTANT: Output raw TypeScript code only. Do NOT use markdown code fences (\`\
18744
18745
  config: options.config
18745
18746
  });
18746
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
+ }
18747
18759
  const refinedJsonContent = JSON.stringify(refinedCriteria.map((c, i) => ({
18748
18760
  acId: `AC-${i + 1}`,
18749
18761
  original: c.original,
@@ -18870,6 +18882,15 @@ async function generateAcceptanceTests(adapter, options) {
18870
18882
  config: options.config
18871
18883
  });
18872
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
+ }
18873
18894
  return {
18874
18895
  testCode,
18875
18896
  criteria
@@ -18883,15 +18904,30 @@ async function generateAcceptanceTests(adapter, options) {
18883
18904
  }
18884
18905
  }
18885
18906
  function extractTestCode(output) {
18907
+ let code;
18886
18908
  const fenceMatch = output.match(/```(?:typescript|ts)?\s*([\s\S]*?)\s*```/);
18887
18909
  if (fenceMatch) {
18888
- 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
+ }
18889
18917
  }
18890
- const importMatch = output.match(/import\s+{[\s\S]+/);
18891
- if (importMatch) {
18892
- 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;
18893
18929
  }
18894
- return output.trim();
18930
+ return code;
18895
18931
  }
18896
18932
  function generateSkeletonTests(featureName, criteria) {
18897
18933
  const tests = criteria.map((ac) => {
@@ -20363,7 +20399,8 @@ function applyDecomposition(prd, result) {
20363
20399
  const originalIndex = prd.userStories.findIndex((s) => s.id === parentStoryId);
20364
20400
  if (originalIndex === -1)
20365
20401
  return;
20366
- prd.userStories[originalIndex].status = "decomposed";
20402
+ const parentStory = prd.userStories[originalIndex];
20403
+ parentStory.status = "decomposed";
20367
20404
  const newStories = subStories.map((sub) => ({
20368
20405
  id: sub.id,
20369
20406
  title: sub.title,
@@ -20375,7 +20412,8 @@ function applyDecomposition(prd, result) {
20375
20412
  passes: false,
20376
20413
  escalations: [],
20377
20414
  attempts: 0,
20378
- parentStoryId: sub.parentStoryId
20415
+ parentStoryId: sub.parentStoryId,
20416
+ ...parentStory.workdir !== undefined && { workdir: parentStory.workdir }
20379
20417
  }));
20380
20418
  prd.userStories.splice(originalIndex + 1, 0, ...newStories);
20381
20419
  }
@@ -22255,7 +22293,7 @@ function markStoryPassed(prd, storyId) {
22255
22293
  story.status = "passed";
22256
22294
  }
22257
22295
  }
22258
- function markStoryFailed(prd, storyId, failureCategory) {
22296
+ function markStoryFailed(prd, storyId, failureCategory, failureStage) {
22259
22297
  const story = prd.userStories.find((s) => s.id === storyId);
22260
22298
  if (story) {
22261
22299
  story.status = "failed";
@@ -22263,6 +22301,9 @@ function markStoryFailed(prd, storyId, failureCategory) {
22263
22301
  if (failureCategory !== undefined) {
22264
22302
  story.failureCategory = failureCategory;
22265
22303
  }
22304
+ if (failureStage !== undefined) {
22305
+ story.failureStage = failureStage;
22306
+ }
22266
22307
  }
22267
22308
  }
22268
22309
  function markStorySkipped(prd, storyId) {
@@ -22289,7 +22330,7 @@ var package_default;
22289
22330
  var init_package = __esm(() => {
22290
22331
  package_default = {
22291
22332
  name: "@nathapp/nax",
22292
- version: "0.49.6",
22333
+ version: "0.50.0",
22293
22334
  description: "AI Coding Agent Orchestrator \u2014 loops until done",
22294
22335
  type: "module",
22295
22336
  bin: {
@@ -22362,8 +22403,8 @@ var init_version = __esm(() => {
22362
22403
  NAX_VERSION = package_default.version;
22363
22404
  NAX_COMMIT = (() => {
22364
22405
  try {
22365
- if (/^[0-9a-f]{6,10}$/.test("a1f7e2d"))
22366
- return "a1f7e2d";
22406
+ if (/^[0-9a-f]{6,10}$/.test("0eeefb4"))
22407
+ return "0eeefb4";
22367
22408
  } catch {}
22368
22409
  try {
22369
22410
  const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
@@ -24755,6 +24796,9 @@ ${c.output}
24755
24796
  \`\`\``).join(`
24756
24797
 
24757
24798
  `);
24799
+ const scopeConstraint = story.workdir ? `
24800
+
24801
+ IMPORTANT: Only modify files within \`${story.workdir}/\`. Do NOT touch files outside this directory.` : "";
24758
24802
  return `You are fixing lint/typecheck errors from a code review.
24759
24803
 
24760
24804
  Story: ${story.title} (${story.id})
@@ -24765,7 +24809,7 @@ ${errors3}
24765
24809
 
24766
24810
  Fix ALL errors listed above. Do NOT change test files or test behavior.
24767
24811
  Do NOT add new features \u2014 only fix the quality check errors.
24768
- Commit your fixes when done.`;
24812
+ Commit your fixes when done.${scopeConstraint}`;
24769
24813
  }
24770
24814
  async function runAgentRectification(ctx) {
24771
24815
  const logger = getLogger();
@@ -24792,9 +24836,10 @@ async function runAgentRectification(ctx) {
24792
24836
  const prompt = buildReviewRectificationPrompt(failedChecks, ctx.story);
24793
24837
  const modelTier = ctx.story.routing?.modelTier ?? ctx.config.autoMode.escalation.tierOrder[0]?.tier ?? "balanced";
24794
24838
  const modelDef = resolveModel(ctx.config.models[modelTier]);
24839
+ const rectificationWorkdir = ctx.story.workdir ? join18(ctx.workdir, ctx.story.workdir) : ctx.workdir;
24795
24840
  await agent.run({
24796
24841
  prompt,
24797
- workdir: ctx.workdir,
24842
+ workdir: rectificationWorkdir,
24798
24843
  modelTier,
24799
24844
  modelDef,
24800
24845
  timeoutSeconds: ctx.config.execution.sessionTimeoutSeconds,
@@ -25348,6 +25393,32 @@ var init_elements = __esm(() => {
25348
25393
  init_logger2();
25349
25394
  });
25350
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
+
25351
25422
  // src/context/test-scanner.ts
25352
25423
  import path6 from "path";
25353
25424
  var {Glob } = globalThis.Bun;
@@ -25695,6 +25766,18 @@ async function buildContext(storyContext, budget) {
25695
25766
  }
25696
25767
  }
25697
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
+ }
25698
25781
  addDependencyElements(elements, currentStory, prd);
25699
25782
  await addTestCoverageElement(elements, storyContext, currentStory);
25700
25783
  await addFileElements(elements, storyContext, currentStory);
@@ -25755,6 +25838,15 @@ async function addFileElements(elements, storyContext, story) {
25755
25838
  if (fileInjection !== "keyword")
25756
25839
  return;
25757
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
+ }
25758
25850
  if (contextFiles.length === 0 && storyContext.config?.context?.autoDetect?.enabled !== false && storyContext.workdir) {
25759
25851
  const autoDetectConfig = storyContext.config?.context?.autoDetect;
25760
25852
  try {
@@ -25822,6 +25914,7 @@ var init_builder3 = __esm(() => {
25822
25914
  init_prd();
25823
25915
  init_auto_detect();
25824
25916
  init_elements();
25917
+ init_parent_context();
25825
25918
  init_test_scanner();
25826
25919
  init_elements();
25827
25920
  _deps5 = {
@@ -26291,6 +26384,22 @@ async function autoCommitIfDirty(workdir, stage, role, storyId) {
26291
26384
  await commitProc.exited;
26292
26385
  } catch {}
26293
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
+ }
26294
26403
  var _gitDeps, GIT_TIMEOUT_MS = 1e4;
26295
26404
  var init_git = __esm(() => {
26296
26405
  init_logger2();
@@ -33141,7 +33250,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
33141
33250
  worktreePath
33142
33251
  });
33143
33252
  } catch (error48) {
33144
- markStoryFailed(currentPrd, story.id);
33253
+ markStoryFailed(currentPrd, story.id, undefined, undefined);
33145
33254
  logger?.error("parallel", "Failed to create worktree", {
33146
33255
  storyId: story.id,
33147
33256
  error: errorMessage(error48)
@@ -33169,7 +33278,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
33169
33278
  retryCount: mergeResult.retryCount
33170
33279
  });
33171
33280
  } else {
33172
- markStoryFailed(currentPrd, mergeResult.storyId);
33281
+ markStoryFailed(currentPrd, mergeResult.storyId, undefined, undefined);
33173
33282
  batchResult.mergeConflicts.push({
33174
33283
  storyId: mergeResult.storyId,
33175
33284
  conflictFiles: mergeResult.conflictFiles || [],
@@ -33187,7 +33296,7 @@ async function executeParallel(stories, prdPath, projectRoot, config2, hooks, pl
33187
33296
  }
33188
33297
  }
33189
33298
  for (const { story, error: error48 } of batchResult.failed) {
33190
- markStoryFailed(currentPrd, story.id);
33299
+ markStoryFailed(currentPrd, story.id, undefined, undefined);
33191
33300
  logger?.error("parallel", "Cleaning up failed story worktree", {
33192
33301
  storyId: story.id,
33193
33302
  error: error48
@@ -34124,7 +34233,7 @@ async function handleNoTierAvailable(ctx, failureCategory) {
34124
34233
  return { outcome: "paused", prdDirty: true, prd: pausedPrd };
34125
34234
  }
34126
34235
  const failedPrd = { ...ctx.prd };
34127
- markStoryFailed(failedPrd, ctx.story.id, failureCategory);
34236
+ markStoryFailed(failedPrd, ctx.story.id, failureCategory, undefined);
34128
34237
  await savePRD(failedPrd, ctx.prdPath);
34129
34238
  logger?.error("execution", "Story failed - execution failed", {
34130
34239
  storyId: ctx.story.id
@@ -34164,7 +34273,7 @@ async function handleMaxAttemptsReached(ctx, failureCategory) {
34164
34273
  return { outcome: "paused", prdDirty: true, prd: pausedPrd };
34165
34274
  }
34166
34275
  const failedPrd = { ...ctx.prd };
34167
- markStoryFailed(failedPrd, ctx.story.id, failureCategory);
34276
+ markStoryFailed(failedPrd, ctx.story.id, failureCategory, undefined);
34168
34277
  await savePRD(failedPrd, ctx.prdPath);
34169
34278
  logger?.error("execution", "Story failed - max attempts reached", {
34170
34279
  storyId: ctx.story.id,
@@ -34329,6 +34438,17 @@ var init_escalation = __esm(() => {
34329
34438
  });
34330
34439
 
34331
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
+ }
34332
34452
  async function handlePipelineSuccess(ctx, pipelineResult) {
34333
34453
  const logger = getSafeLogger();
34334
34454
  const costDelta = pipelineResult.context.agentResult?.estimatedCost || 0;
@@ -34357,6 +34477,17 @@ async function handlePipelineSuccess(ctx, pipelineResult) {
34357
34477
  testStrategy: ctx.routing.testStrategy
34358
34478
  });
34359
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
+ }
34360
34491
  const updatedCounts = countStories(prd);
34361
34492
  logger?.info("progress", "Progress update", {
34362
34493
  totalStories: updatedCounts.total,
@@ -34393,7 +34524,7 @@ async function handlePipelineFailure(ctx, pipelineResult) {
34393
34524
  prdDirty = true;
34394
34525
  break;
34395
34526
  case "fail":
34396
- markStoryFailed(prd, ctx.story.id, pipelineResult.context.tddFailureCategory);
34527
+ markStoryFailed(prd, ctx.story.id, pipelineResult.context.tddFailureCategory, pipelineResult.stoppedAtStage);
34397
34528
  await savePRD(prd, ctx.prdPath);
34398
34529
  prdDirty = true;
34399
34530
  logger?.error("pipeline", "Story failed", { storyId: ctx.story.id, reason: pipelineResult.reason });
@@ -34447,6 +34578,7 @@ var init_pipeline_result_handler = __esm(() => {
34447
34578
  init_logger2();
34448
34579
  init_event_bus();
34449
34580
  init_prd();
34581
+ init_git();
34450
34582
  init_escalation();
34451
34583
  init_progress();
34452
34584
  });
@@ -35101,25 +35233,44 @@ var init_precheck_runner = __esm(() => {
35101
35233
  var exports_run_initialization = {};
35102
35234
  __export(exports_run_initialization, {
35103
35235
  logActiveProtocol: () => logActiveProtocol,
35104
- initializeRun: () => initializeRun
35236
+ initializeRun: () => initializeRun,
35237
+ _reconcileDeps: () => _reconcileDeps
35105
35238
  });
35106
- async function reconcileState(prd, prdPath, workdir) {
35239
+ import { join as join51 } from "path";
35240
+ async function reconcileState(prd, prdPath, workdir, config2) {
35107
35241
  const logger = getSafeLogger();
35108
35242
  let reconciledCount = 0;
35109
35243
  let modified = false;
35110
35244
  for (const story of prd.userStories) {
35111
- if (story.status === "failed") {
35112
- const hasCommits = await hasCommitsForStory(workdir, story.id);
35113
- if (hasCommits) {
35114
- logger?.warn("reconciliation", "Failed story has commits in git history, marking as passed", {
35115
- storyId: story.id,
35116
- title: story.title
35117
- });
35118
- markStoryPassed(prd, story.id);
35119
- reconciledCount++;
35120
- 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;
35121
35265
  }
35122
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;
35123
35274
  }
35124
35275
  if (reconciledCount > 0) {
35125
35276
  logger?.info("reconciliation", `Reconciled ${reconciledCount} failed stories from git history`);
@@ -35169,7 +35320,7 @@ async function initializeRun(ctx) {
35169
35320
  const logger = getSafeLogger();
35170
35321
  await checkAgentInstalled(ctx.config, ctx.dryRun, ctx.agentGetFn);
35171
35322
  let prd = await loadPRD(ctx.prdPath);
35172
- prd = await reconcileState(prd, ctx.prdPath, ctx.workdir);
35323
+ prd = await reconcileState(prd, ctx.prdPath, ctx.workdir, ctx.config);
35173
35324
  const counts = countStories(prd);
35174
35325
  validateStoryCount(counts, ctx.config);
35175
35326
  logger?.info("execution", "Run initialization complete", {
@@ -35179,11 +35330,17 @@ async function initializeRun(ctx) {
35179
35330
  });
35180
35331
  return { prd, storyCounts: counts };
35181
35332
  }
35333
+ var _reconcileDeps;
35182
35334
  var init_run_initialization = __esm(() => {
35183
35335
  init_errors3();
35184
35336
  init_logger2();
35185
35337
  init_prd();
35338
+ init_runner2();
35186
35339
  init_git();
35340
+ _reconcileDeps = {
35341
+ hasCommitsForStory: (workdir, storyId) => hasCommitsForStory(workdir, storyId),
35342
+ runReview: (reviewConfig, workdir, executionConfig) => runReview(reviewConfig, workdir, executionConfig)
35343
+ };
35187
35344
  });
35188
35345
 
35189
35346
  // src/execution/lifecycle/run-setup.ts
@@ -66225,7 +66382,7 @@ var require_jsx_dev_runtime = __commonJS((exports, module) => {
66225
66382
  init_source();
66226
66383
  import { existsSync as existsSync34, mkdirSync as mkdirSync6 } from "fs";
66227
66384
  import { homedir as homedir10 } from "os";
66228
- import { join as join51 } from "path";
66385
+ import { join as join52 } from "path";
66229
66386
 
66230
66387
  // node_modules/commander/esm.mjs
66231
66388
  var import__ = __toESM(require_commander(), 1);
@@ -67651,14 +67808,48 @@ For each user story, set the "workdir" field to the relevant package path (e.g.
67651
67808
  "workdir": "string \u2014 optional, relative path to package (e.g. \\"packages/api\\"). Omit for root-level stories.",` : "";
67652
67809
  return `You are a senior software architect generating a product requirements document (PRD) as JSON.
67653
67810
 
67811
+ ## Step 1: Understand the Spec
67812
+
67813
+ Read the spec carefully. Identify the goal, scope, constraints, and what "done" looks like.
67814
+
67654
67815
  ## Spec
67655
67816
 
67656
67817
  ${specContent}
67657
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
+
67658
67837
  ## Codebase Context
67659
67838
 
67660
67839
  ${codebaseContext}${monorepoHint}
67661
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
+
67662
67853
  ## Output Schema
67663
67854
 
67664
67855
  Generate a JSON object with this exact structure (no markdown, no explanation \u2014 JSON only):
@@ -67666,6 +67857,7 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
67666
67857
  {
67667
67858
  "project": "string \u2014 project name",
67668
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.",
67669
67861
  "branchName": "string \u2014 git branch (e.g. feat/my-feature)",
67670
67862
  "createdAt": "ISO 8601 timestamp",
67671
67863
  "updatedAt": "ISO 8601 timestamp",
@@ -67675,13 +67867,14 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
67675
67867
  "title": "string \u2014 concise story title",
67676
67868
  "description": "string \u2014 detailed description of the story",
67677
67869
  "acceptanceCriteria": ["string \u2014 each AC line"],
67870
+ "contextFiles": ["string \u2014 key source files the agent should read (max 5, relative paths)"],
67678
67871
  "tags": ["string \u2014 routing tags, e.g. feature, security, api"],
67679
67872
  "dependencies": ["string \u2014 story IDs this story depends on"],${workdirField}
67680
67873
  "status": "pending",
67681
67874
  "passes": false,
67682
67875
  "routing": {
67683
67876
  "complexity": "simple | medium | complex | expert",
67684
- "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",
67685
67878
  "reasoning": "string \u2014 brief classification rationale"
67686
67879
  },
67687
67880
  "escalations": [],
@@ -67690,12 +67883,6 @@ Generate a JSON object with this exact structure (no markdown, no explanation \u
67690
67883
  ]
67691
67884
  }
67692
67885
 
67693
- ${COMPLEXITY_GUIDE}
67694
-
67695
- ${TEST_STRATEGY_GUIDE}
67696
-
67697
- ${GROUPING_RULES}
67698
-
67699
67886
  ${outputFilePath ? `Write the PRD JSON directly to this file path: ${outputFilePath}
67700
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."}`;
67701
67888
  }
@@ -78012,15 +78199,15 @@ Next: nax generate --package ${options.package}`));
78012
78199
  }
78013
78200
  return;
78014
78201
  }
78015
- const naxDir = join51(workdir, "nax");
78202
+ const naxDir = join52(workdir, "nax");
78016
78203
  if (existsSync34(naxDir) && !options.force) {
78017
78204
  console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
78018
78205
  return;
78019
78206
  }
78020
- mkdirSync6(join51(naxDir, "features"), { recursive: true });
78021
- mkdirSync6(join51(naxDir, "hooks"), { recursive: true });
78022
- await Bun.write(join51(naxDir, "config.json"), JSON.stringify(DEFAULT_CONFIG, null, 2));
78023
- 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({
78024
78211
  hooks: {
78025
78212
  "on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
78026
78213
  "on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
@@ -78028,12 +78215,12 @@ Next: nax generate --package ${options.package}`));
78028
78215
  "on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
78029
78216
  }
78030
78217
  }, null, 2));
78031
- await Bun.write(join51(naxDir, ".gitignore"), `# nax temp files
78218
+ await Bun.write(join52(naxDir, ".gitignore"), `# nax temp files
78032
78219
  *.tmp
78033
78220
  .paused.json
78034
78221
  .nax-verifier-verdict.json
78035
78222
  `);
78036
- await Bun.write(join51(naxDir, "context.md"), `# Project Context
78223
+ await Bun.write(join52(naxDir, "context.md"), `# Project Context
78037
78224
 
78038
78225
  This document defines coding standards, architectural decisions, and forbidden patterns for this project.
78039
78226
  Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
@@ -78159,8 +78346,8 @@ program2.command("run").description("Run the orchestration loop for a feature").
78159
78346
  console.error(source_default.red("nax not initialized. Run: nax init"));
78160
78347
  process.exit(1);
78161
78348
  }
78162
- const featureDir = join51(naxDir, "features", options.feature);
78163
- const prdPath = join51(featureDir, "prd.json");
78349
+ const featureDir = join52(naxDir, "features", options.feature);
78350
+ const prdPath = join52(featureDir, "prd.json");
78164
78351
  if (options.plan && options.from) {
78165
78352
  if (existsSync34(prdPath) && !options.force) {
78166
78353
  console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
@@ -78182,10 +78369,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
78182
78369
  }
78183
78370
  }
78184
78371
  try {
78185
- const planLogDir = join51(featureDir, "plan");
78372
+ const planLogDir = join52(featureDir, "plan");
78186
78373
  mkdirSync6(planLogDir, { recursive: true });
78187
78374
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
78188
- const planLogPath = join51(planLogDir, `${planLogId}.jsonl`);
78375
+ const planLogPath = join52(planLogDir, `${planLogId}.jsonl`);
78189
78376
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
78190
78377
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
78191
78378
  console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
@@ -78223,10 +78410,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
78223
78410
  process.exit(1);
78224
78411
  }
78225
78412
  resetLogger();
78226
- const runsDir = join51(featureDir, "runs");
78413
+ const runsDir = join52(featureDir, "runs");
78227
78414
  mkdirSync6(runsDir, { recursive: true });
78228
78415
  const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
78229
- const logFilePath = join51(runsDir, `${runId}.jsonl`);
78416
+ const logFilePath = join52(runsDir, `${runId}.jsonl`);
78230
78417
  const isTTY = process.stdout.isTTY ?? false;
78231
78418
  const headlessFlag = options.headless ?? false;
78232
78419
  const headlessEnv = process.env.NAX_HEADLESS === "1";
@@ -78242,7 +78429,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
78242
78429
  config2.autoMode.defaultAgent = options.agent;
78243
78430
  }
78244
78431
  config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
78245
- const globalNaxDir = join51(homedir10(), ".nax");
78432
+ const globalNaxDir = join52(homedir10(), ".nax");
78246
78433
  const hooks = await loadHooksConfig(naxDir, globalNaxDir);
78247
78434
  const eventEmitter = new PipelineEventEmitter;
78248
78435
  let tuiInstance;
@@ -78265,7 +78452,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
78265
78452
  } else {
78266
78453
  console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
78267
78454
  }
78268
- const statusFilePath = join51(workdir, "nax", "status.json");
78455
+ const statusFilePath = join52(workdir, "nax", "status.json");
78269
78456
  let parallel;
78270
78457
  if (options.parallel !== undefined) {
78271
78458
  parallel = Number.parseInt(options.parallel, 10);
@@ -78291,7 +78478,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
78291
78478
  headless: useHeadless,
78292
78479
  skipPrecheck: options.skipPrecheck ?? false
78293
78480
  });
78294
- const latestSymlink = join51(runsDir, "latest.jsonl");
78481
+ const latestSymlink = join52(runsDir, "latest.jsonl");
78295
78482
  try {
78296
78483
  if (existsSync34(latestSymlink)) {
78297
78484
  Bun.spawnSync(["rm", latestSymlink]);
@@ -78329,9 +78516,9 @@ features.command("create <name>").description("Create a new feature").option("-d
78329
78516
  console.error(source_default.red("nax not initialized. Run: nax init"));
78330
78517
  process.exit(1);
78331
78518
  }
78332
- const featureDir = join51(naxDir, "features", name);
78519
+ const featureDir = join52(naxDir, "features", name);
78333
78520
  mkdirSync6(featureDir, { recursive: true });
78334
- await Bun.write(join51(featureDir, "spec.md"), `# Feature: ${name}
78521
+ await Bun.write(join52(featureDir, "spec.md"), `# Feature: ${name}
78335
78522
 
78336
78523
  ## Overview
78337
78524
 
@@ -78339,7 +78526,7 @@ features.command("create <name>").description("Create a new feature").option("-d
78339
78526
 
78340
78527
  ## Acceptance Criteria
78341
78528
  `);
78342
- await Bun.write(join51(featureDir, "plan.md"), `# Plan: ${name}
78529
+ await Bun.write(join52(featureDir, "plan.md"), `# Plan: ${name}
78343
78530
 
78344
78531
  ## Architecture
78345
78532
 
@@ -78347,7 +78534,7 @@ features.command("create <name>").description("Create a new feature").option("-d
78347
78534
 
78348
78535
  ## Dependencies
78349
78536
  `);
78350
- await Bun.write(join51(featureDir, "tasks.md"), `# Tasks: ${name}
78537
+ await Bun.write(join52(featureDir, "tasks.md"), `# Tasks: ${name}
78351
78538
 
78352
78539
  ## US-001: [Title]
78353
78540
 
@@ -78356,7 +78543,7 @@ features.command("create <name>").description("Create a new feature").option("-d
78356
78543
  ### Acceptance Criteria
78357
78544
  - [ ] Criterion 1
78358
78545
  `);
78359
- await Bun.write(join51(featureDir, "progress.txt"), `# Progress: ${name}
78546
+ await Bun.write(join52(featureDir, "progress.txt"), `# Progress: ${name}
78360
78547
 
78361
78548
  Created: ${new Date().toISOString()}
78362
78549
 
@@ -78384,7 +78571,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
78384
78571
  console.error(source_default.red("nax not initialized."));
78385
78572
  process.exit(1);
78386
78573
  }
78387
- const featuresDir = join51(naxDir, "features");
78574
+ const featuresDir = join52(naxDir, "features");
78388
78575
  if (!existsSync34(featuresDir)) {
78389
78576
  console.log(source_default.dim("No features yet."));
78390
78577
  return;
@@ -78399,7 +78586,7 @@ features.command("list").description("List all features").option("-d, --dir <pat
78399
78586
  Features:
78400
78587
  `));
78401
78588
  for (const name of entries) {
78402
- const prdPath = join51(featuresDir, name, "prd.json");
78589
+ const prdPath = join52(featuresDir, name, "prd.json");
78403
78590
  if (existsSync34(prdPath)) {
78404
78591
  const prd = await loadPRD(prdPath);
78405
78592
  const c = countStories(prd);
@@ -78430,10 +78617,10 @@ Use: nax plan -f <feature> --from <spec>`));
78430
78617
  process.exit(1);
78431
78618
  }
78432
78619
  const config2 = await loadConfig(workdir);
78433
- const featureLogDir = join51(naxDir, "features", options.feature, "plan");
78620
+ const featureLogDir = join52(naxDir, "features", options.feature, "plan");
78434
78621
  mkdirSync6(featureLogDir, { recursive: true });
78435
78622
  const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
78436
- const planLogPath = join51(featureLogDir, `${planLogId}.jsonl`);
78623
+ const planLogPath = join52(featureLogDir, `${planLogId}.jsonl`);
78437
78624
  initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
78438
78625
  console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
78439
78626
  try {
@@ -78470,7 +78657,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
78470
78657
  console.error(source_default.red("nax not initialized. Run: nax init"));
78471
78658
  process.exit(1);
78472
78659
  }
78473
- const featureDir = join51(naxDir, "features", options.feature);
78660
+ const featureDir = join52(naxDir, "features", options.feature);
78474
78661
  if (!existsSync34(featureDir)) {
78475
78662
  console.error(source_default.red(`Feature "${options.feature}" not found.`));
78476
78663
  process.exit(1);
@@ -78486,7 +78673,7 @@ program2.command("analyze").description("(deprecated) Parse spec.md into prd.jso
78486
78673
  specPath: options.from,
78487
78674
  reclassify: options.reclassify
78488
78675
  });
78489
- const prdPath = join51(featureDir, "prd.json");
78676
+ const prdPath = join52(featureDir, "prd.json");
78490
78677
  await Bun.write(prdPath, JSON.stringify(prd, null, 2));
78491
78678
  const c = countStories(prd);
78492
78679
  console.log(source_default.green(`