@nathapp/nax 0.32.2 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/nax.js +808 -104
  2. package/package.json +1 -1
  3. package/src/cli/analyze.ts +145 -0
  4. package/src/cli/config.ts +9 -0
  5. package/src/config/defaults.ts +8 -0
  6. package/src/config/schema.ts +1 -0
  7. package/src/config/schemas.ts +10 -0
  8. package/src/config/types.ts +18 -0
  9. package/src/context/elements.ts +13 -0
  10. package/src/context/greenfield.ts +1 -1
  11. package/src/decompose/apply.ts +44 -0
  12. package/src/decompose/builder.ts +181 -0
  13. package/src/decompose/index.ts +8 -0
  14. package/src/decompose/sections/codebase.ts +26 -0
  15. package/src/decompose/sections/constraints.ts +32 -0
  16. package/src/decompose/sections/index.ts +4 -0
  17. package/src/decompose/sections/sibling-stories.ts +25 -0
  18. package/src/decompose/sections/target-story.ts +31 -0
  19. package/src/decompose/types.ts +55 -0
  20. package/src/decompose/validators/complexity.ts +45 -0
  21. package/src/decompose/validators/coverage.ts +134 -0
  22. package/src/decompose/validators/dependency.ts +91 -0
  23. package/src/decompose/validators/index.ts +35 -0
  24. package/src/decompose/validators/overlap.ts +128 -0
  25. package/src/execution/escalation/tier-escalation.ts +9 -2
  26. package/src/execution/sequential-executor.ts +4 -3
  27. package/src/interaction/index.ts +1 -0
  28. package/src/interaction/triggers.ts +21 -0
  29. package/src/interaction/types.ts +7 -0
  30. package/src/pipeline/stages/review.ts +6 -0
  31. package/src/pipeline/stages/routing.ts +89 -0
  32. package/src/pipeline/types.ts +2 -0
  33. package/src/plugins/types.ts +33 -0
  34. package/src/prd/index.ts +5 -1
  35. package/src/prd/types.ts +11 -1
  36. package/src/review/orchestrator.ts +1 -0
  37. package/src/review/types.ts +2 -0
  38. package/src/tdd/isolation.ts +1 -1
package/dist/nax.js CHANGED
@@ -17993,7 +17993,7 @@ var init_zod = __esm(() => {
17993
17993
  });
17994
17994
 
17995
17995
  // src/config/schemas.ts
17996
- var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, AdaptiveRoutingConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, NaxConfigSchema;
17996
+ var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, AdaptiveRoutingConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, DecomposeConfigSchema, NaxConfigSchema;
17997
17997
  var init_schemas3 = __esm(() => {
17998
17998
  init_zod();
17999
17999
  TokenPricingSchema = exports_external.object({
@@ -18231,6 +18231,14 @@ var init_schemas3 = __esm(() => {
18231
18231
  PromptsConfigSchema = exports_external.object({
18232
18232
  overrides: exports_external.record(exports_external.enum(["test-writer", "implementer", "verifier", "single-session"]), exports_external.string().min(1, "Override path must be non-empty")).optional()
18233
18233
  });
18234
+ DecomposeConfigSchema = exports_external.object({
18235
+ trigger: exports_external.enum(["auto", "confirm", "disabled"]).default("auto"),
18236
+ maxAcceptanceCriteria: exports_external.number().int().min(1).default(6),
18237
+ maxSubstories: exports_external.number().int().min(1).default(5),
18238
+ maxSubstoryComplexity: exports_external.enum(["simple", "medium", "complex", "expert"]).default("medium"),
18239
+ maxRetries: exports_external.number().int().min(0).default(2),
18240
+ model: exports_external.string().min(1).default("balanced")
18241
+ });
18234
18242
  NaxConfigSchema = exports_external.object({
18235
18243
  version: exports_external.number(),
18236
18244
  models: ModelMapSchema,
@@ -18250,7 +18258,8 @@ var init_schemas3 = __esm(() => {
18250
18258
  hooks: HooksConfigSchema.optional(),
18251
18259
  interaction: InteractionConfigSchema.optional(),
18252
18260
  precheck: PrecheckConfigSchema.optional(),
18253
- prompts: PromptsConfigSchema.optional()
18261
+ prompts: PromptsConfigSchema.optional(),
18262
+ decompose: DecomposeConfigSchema.optional()
18254
18263
  }).refine((data) => data.version === 1, {
18255
18264
  message: "Invalid version: expected 1",
18256
18265
  path: ["version"]
@@ -18414,7 +18423,15 @@ var init_defaults = __esm(() => {
18414
18423
  maxBulletPoints: 8
18415
18424
  }
18416
18425
  },
18417
- prompts: {}
18426
+ prompts: {},
18427
+ decompose: {
18428
+ trigger: "auto",
18429
+ maxAcceptanceCriteria: 6,
18430
+ maxSubstories: 5,
18431
+ maxSubstoryComplexity: "medium",
18432
+ maxRetries: 2,
18433
+ model: "balanced"
18434
+ }
18418
18435
  };
18419
18436
  });
18420
18437
 
@@ -18424,6 +18441,120 @@ var init_schema = __esm(() => {
18424
18441
  init_defaults();
18425
18442
  });
18426
18443
 
18444
+ // src/decompose/apply.ts
18445
+ function applyDecomposition(prd, result) {
18446
+ const { subStories } = result;
18447
+ if (subStories.length === 0)
18448
+ return;
18449
+ const parentStoryId = subStories[0].parentStoryId;
18450
+ const originalIndex = prd.userStories.findIndex((s) => s.id === parentStoryId);
18451
+ if (originalIndex === -1)
18452
+ return;
18453
+ prd.userStories[originalIndex].status = "decomposed";
18454
+ const newStories = subStories.map((sub) => ({
18455
+ id: sub.id,
18456
+ title: sub.title,
18457
+ description: sub.description,
18458
+ acceptanceCriteria: sub.acceptanceCriteria,
18459
+ tags: sub.tags,
18460
+ dependencies: sub.dependencies,
18461
+ status: "pending",
18462
+ passes: false,
18463
+ escalations: [],
18464
+ attempts: 0,
18465
+ parentStoryId: sub.parentStoryId
18466
+ }));
18467
+ prd.userStories.splice(originalIndex + 1, 0, ...newStories);
18468
+ }
18469
+
18470
+ // src/decompose/sections/codebase.ts
18471
+ function buildCodebaseSection(scan) {
18472
+ const deps = Object.entries(scan.dependencies).slice(0, 15).map(([k, v]) => ` ${k}: ${v}`).join(`
18473
+ `);
18474
+ return [
18475
+ "# Codebase Context",
18476
+ "",
18477
+ "**File Tree:**",
18478
+ scan.fileTree,
18479
+ "",
18480
+ "**Dependencies:**",
18481
+ deps || " (none)",
18482
+ "",
18483
+ `**Test Patterns:** ${scan.testPatterns.join(", ")}`
18484
+ ].join(`
18485
+ `);
18486
+ }
18487
+
18488
+ // src/decompose/sections/constraints.ts
18489
+ function buildConstraintsSection(config2) {
18490
+ return [
18491
+ "# Decomposition Constraints",
18492
+ "",
18493
+ `- **Max sub-stories:** ${config2.maxSubStories}`,
18494
+ `- **Max complexity per sub-story:** ${config2.maxComplexity}`,
18495
+ "",
18496
+ "Respond with ONLY a JSON array (no markdown code fences):",
18497
+ "[{",
18498
+ ` "id": "PARENT-ID-1",`,
18499
+ ` "parentStoryId": "PARENT-ID",`,
18500
+ ` "title": "Sub-story title",`,
18501
+ ` "description": "What to implement",`,
18502
+ ` "acceptanceCriteria": ["Criterion 1"],`,
18503
+ ` "tags": [],`,
18504
+ ` "dependencies": [],`,
18505
+ ` "complexity": "simple",`,
18506
+ ` "nonOverlapJustification": "Why this sub-story does not overlap with sibling stories"`,
18507
+ "}]",
18508
+ "",
18509
+ "The nonOverlapJustification field is required for every sub-story."
18510
+ ].join(`
18511
+ `);
18512
+ }
18513
+
18514
+ // src/decompose/sections/sibling-stories.ts
18515
+ function buildSiblingStoriesSection(targetStory, prd) {
18516
+ const siblings = prd.userStories.filter((s) => s.id !== targetStory.id);
18517
+ if (siblings.length === 0) {
18518
+ return `# Sibling Stories
18519
+
18520
+ No other stories exist in this PRD.`;
18521
+ }
18522
+ const entries = siblings.map((s) => {
18523
+ const acSummary = s.acceptanceCriteria.slice(0, 3).join("; ");
18524
+ return `- **${s.id}** \u2014 ${s.title} [${s.status}]
18525
+ AC: ${acSummary}`;
18526
+ }).join(`
18527
+ `);
18528
+ return ["# Sibling Stories", "", "Avoid overlapping with these existing stories in the PRD:", "", entries].join(`
18529
+ `);
18530
+ }
18531
+
18532
+ // src/decompose/sections/target-story.ts
18533
+ function buildTargetStorySection(story) {
18534
+ const ac = story.acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join(`
18535
+ `);
18536
+ const tags = story.tags.length > 0 ? story.tags.join(", ") : "none";
18537
+ const deps = story.dependencies.length > 0 ? story.dependencies.join(", ") : "none";
18538
+ return [
18539
+ "# Target Story to Decompose",
18540
+ "",
18541
+ `**ID:** ${story.id}`,
18542
+ `**Title:** ${story.title}`,
18543
+ "",
18544
+ "**Description:**",
18545
+ story.description,
18546
+ "",
18547
+ "**Acceptance Criteria:**",
18548
+ ac,
18549
+ "",
18550
+ `**Tags:** ${tags}`,
18551
+ `**Dependencies:** ${deps}`,
18552
+ "",
18553
+ "Decompose this story into smaller sub-stories that can each be implemented independently."
18554
+ ].join(`
18555
+ `);
18556
+ }
18557
+
18427
18558
  // src/routing/chain.ts
18428
18559
  class StrategyChain {
18429
18560
  strategies;
@@ -19526,90 +19657,471 @@ var init_routing = __esm(() => {
19526
19657
  init_batch_route();
19527
19658
  });
19528
19659
 
19529
- // package.json
19530
- var package_default;
19531
- var init_package = __esm(() => {
19532
- package_default = {
19533
- name: "@nathapp/nax",
19534
- version: "0.32.2",
19535
- description: "AI Coding Agent Orchestrator \u2014 loops until done",
19536
- type: "module",
19537
- bin: {
19538
- nax: "./dist/nax.js"
19539
- },
19540
- scripts: {
19541
- prepare: "git config core.hooksPath .githooks",
19542
- dev: "bun run bin/nax.ts",
19543
- build: 'bun build bin/nax.ts --outdir dist --target bun --define "GIT_COMMIT=\\"$(git rev-parse --short HEAD)\\""',
19544
- typecheck: "bun x tsc --noEmit",
19545
- lint: "bun x biome check src/ bin/",
19546
- test: "NAX_SKIP_PRECHECK=1 bun test test/ --timeout=60000",
19547
- "test:watch": "bun test --watch",
19548
- "test:unit": "bun test ./test/unit/ --timeout=60000",
19549
- "test:integration": "bun test ./test/integration/ --timeout=60000",
19550
- "test:ui": "bun test ./test/ui/ --timeout=60000",
19551
- prepublishOnly: "bun run build"
19552
- },
19553
- dependencies: {
19554
- "@anthropic-ai/sdk": "^0.74.0",
19555
- "@types/react": "^19.2.14",
19556
- chalk: "^5.6.2",
19557
- commander: "^13.1.0",
19558
- ink: "^6.7.0",
19559
- "ink-spinner": "^5.0.0",
19560
- "ink-testing-library": "^4.0.0",
19561
- react: "^19.2.4",
19562
- zod: "^4.3.6"
19563
- },
19564
- devDependencies: {
19565
- "@biomejs/biome": "^1.9.4",
19566
- "@types/bun": "^1.3.8",
19567
- "react-devtools-core": "^7.0.1",
19568
- typescript: "^5.7.3"
19569
- },
19570
- license: "MIT",
19571
- author: "William Khoo",
19572
- keywords: [
19573
- "ai",
19574
- "agent",
19575
- "orchestrator",
19576
- "tdd",
19577
- "coding"
19578
- ],
19579
- files: [
19580
- "dist/",
19581
- "src/",
19582
- "bin/",
19583
- "README.md",
19584
- "CHANGELOG.md"
19585
- ]
19660
+ // src/decompose/validators/complexity.ts
19661
+ function validateComplexity2(substories, maxComplexity) {
19662
+ const errors3 = [];
19663
+ const warnings = [];
19664
+ const maxOrder = COMPLEXITY_ORDER[maxComplexity];
19665
+ for (const sub of substories) {
19666
+ const assignedOrder = COMPLEXITY_ORDER[sub.complexity];
19667
+ if (assignedOrder > maxOrder) {
19668
+ errors3.push(`Substory ${sub.id} complexity "${sub.complexity}" exceeds maxComplexity "${maxComplexity}"`);
19669
+ }
19670
+ const classified = classifyComplexity2(sub.title, sub.description, sub.acceptanceCriteria, sub.tags);
19671
+ if (classified !== sub.complexity) {
19672
+ const classifiedOrder = COMPLEXITY_ORDER[classified] ?? 0;
19673
+ if (classifiedOrder > assignedOrder) {
19674
+ warnings.push(`Substory ${sub.id} is assigned complexity "${sub.complexity}" but classifier estimates "${classified}" \u2014 may be underestimated`);
19675
+ }
19676
+ }
19677
+ }
19678
+ return { valid: errors3.length === 0, errors: errors3, warnings };
19679
+ }
19680
+ var COMPLEXITY_ORDER;
19681
+ var init_complexity = __esm(() => {
19682
+ init_routing();
19683
+ COMPLEXITY_ORDER = {
19684
+ simple: 0,
19685
+ medium: 1,
19686
+ complex: 2,
19687
+ expert: 3
19586
19688
  };
19587
19689
  });
19588
19690
 
19589
- // src/version.ts
19590
- var NAX_VERSION, NAX_COMMIT, NAX_BUILD_INFO;
19591
- var init_version = __esm(() => {
19592
- init_package();
19593
- NAX_VERSION = package_default.version;
19594
- NAX_COMMIT = (() => {
19595
- try {
19596
- if (/^[0-9a-f]{6,10}$/.test("1012b41"))
19597
- return "1012b41";
19598
- } catch {}
19599
- try {
19600
- const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
19601
- cwd: import.meta.dir,
19602
- stderr: "ignore"
19603
- });
19604
- if (result.exitCode === 0) {
19605
- const hash2 = result.stdout.toString().trim();
19606
- if (/^[0-9a-f]{6,10}$/.test(hash2))
19607
- return hash2;
19691
+ // src/decompose/validators/coverage.ts
19692
+ function extractKeywords(text) {
19693
+ return text.toLowerCase().split(/[\s,.:;!?()\[\]{}"'`\-_/\\]+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
19694
+ }
19695
+ function commonPrefixLength(a, b) {
19696
+ let i = 0;
19697
+ while (i < a.length && i < b.length && a[i] === b[i])
19698
+ i++;
19699
+ return i;
19700
+ }
19701
+ function keywordsMatch(a, b) {
19702
+ return a === b || commonPrefixLength(a, b) >= 5;
19703
+ }
19704
+ function isCovered(originalAc, substoryAcs) {
19705
+ const originalKw = extractKeywords(originalAc);
19706
+ if (originalKw.length === 0)
19707
+ return true;
19708
+ const substoryKwList = substoryAcs.flatMap(extractKeywords);
19709
+ let matchCount = 0;
19710
+ for (const kw of originalKw) {
19711
+ if (substoryKwList.some((s) => keywordsMatch(kw, s))) {
19712
+ matchCount++;
19713
+ }
19714
+ }
19715
+ return matchCount > originalKw.length / 2;
19716
+ }
19717
+ function validateCoverage(originalStory, substories) {
19718
+ const warnings = [];
19719
+ const allSubstoryAcs = substories.flatMap((s) => s.acceptanceCriteria);
19720
+ for (const ac of originalStory.acceptanceCriteria ?? []) {
19721
+ if (!isCovered(ac, allSubstoryAcs)) {
19722
+ warnings.push(`Original AC not covered by any substory: "${ac}"`);
19723
+ }
19724
+ }
19725
+ return { valid: true, errors: [], warnings };
19726
+ }
19727
+ var STOP_WORDS;
19728
+ var init_coverage = __esm(() => {
19729
+ STOP_WORDS = new Set([
19730
+ "a",
19731
+ "an",
19732
+ "the",
19733
+ "and",
19734
+ "or",
19735
+ "but",
19736
+ "is",
19737
+ "are",
19738
+ "was",
19739
+ "were",
19740
+ "be",
19741
+ "been",
19742
+ "being",
19743
+ "have",
19744
+ "has",
19745
+ "had",
19746
+ "do",
19747
+ "does",
19748
+ "did",
19749
+ "will",
19750
+ "would",
19751
+ "could",
19752
+ "should",
19753
+ "may",
19754
+ "might",
19755
+ "can",
19756
+ "to",
19757
+ "of",
19758
+ "in",
19759
+ "on",
19760
+ "at",
19761
+ "for",
19762
+ "with",
19763
+ "by",
19764
+ "from",
19765
+ "as",
19766
+ "it",
19767
+ "its",
19768
+ "that",
19769
+ "this",
19770
+ "these",
19771
+ "those",
19772
+ "not",
19773
+ "no",
19774
+ "so",
19775
+ "if",
19776
+ "then",
19777
+ "than",
19778
+ "when",
19779
+ "which",
19780
+ "who",
19781
+ "what",
19782
+ "how",
19783
+ "all",
19784
+ "each",
19785
+ "any",
19786
+ "up",
19787
+ "out",
19788
+ "about",
19789
+ "into",
19790
+ "through",
19791
+ "after",
19792
+ "before"
19793
+ ]);
19794
+ });
19795
+
19796
+ // src/decompose/validators/dependency.ts
19797
+ function detectCycles(substories) {
19798
+ const errors3 = [];
19799
+ const idSet = new Set(substories.map((s) => s.id));
19800
+ const adj = new Map;
19801
+ for (const sub of substories) {
19802
+ adj.set(sub.id, sub.dependencies.filter((d) => idSet.has(d)));
19803
+ }
19804
+ const WHITE = 0;
19805
+ const GRAY = 1;
19806
+ const BLACK = 2;
19807
+ const color = new Map;
19808
+ for (const id of idSet)
19809
+ color.set(id, WHITE);
19810
+ const reported = new Set;
19811
+ function dfs(id, path) {
19812
+ color.set(id, GRAY);
19813
+ for (const dep of adj.get(id) ?? []) {
19814
+ if (color.get(dep) === GRAY) {
19815
+ const cycleKey = [...path, dep].sort().join(",");
19816
+ if (!reported.has(cycleKey)) {
19817
+ reported.add(cycleKey);
19818
+ const cycleStart = path.indexOf(dep);
19819
+ const cycleNodes = cycleStart >= 0 ? path.slice(cycleStart) : path;
19820
+ const cycleStr = [...cycleNodes, dep].join(" -> ");
19821
+ errors3.push(`Circular dependency detected: ${cycleStr}`);
19822
+ }
19823
+ } else if (color.get(dep) === WHITE) {
19824
+ dfs(dep, [...path, dep]);
19825
+ }
19826
+ }
19827
+ color.set(id, BLACK);
19828
+ }
19829
+ for (const id of idSet) {
19830
+ if (color.get(id) === WHITE) {
19831
+ dfs(id, [id]);
19832
+ }
19833
+ }
19834
+ return errors3;
19835
+ }
19836
+ function validateDependencies(substories, existingStoryIds) {
19837
+ const errors3 = [];
19838
+ const substoryIdSet = new Set(substories.map((s) => s.id));
19839
+ const existingIdSet = new Set(existingStoryIds);
19840
+ const allKnownIds = new Set([...substoryIdSet, ...existingIdSet]);
19841
+ for (const sub of substories) {
19842
+ if (existingIdSet.has(sub.id)) {
19843
+ errors3.push(`Substory ID "${sub.id}" collides with existing PRD story \u2014 duplicate IDs are not allowed`);
19844
+ }
19845
+ }
19846
+ for (const sub of substories) {
19847
+ for (const dep of sub.dependencies) {
19848
+ if (!allKnownIds.has(dep)) {
19849
+ errors3.push(`Substory ${sub.id} references non-existent story ID "${dep}"`);
19608
19850
  }
19609
- } catch {}
19610
- return "dev";
19611
- })();
19612
- NAX_BUILD_INFO = NAX_COMMIT === "dev" ? `v${NAX_VERSION}` : `v${NAX_VERSION} (${NAX_COMMIT})`;
19851
+ }
19852
+ }
19853
+ const cycleErrors = detectCycles(substories);
19854
+ errors3.push(...cycleErrors);
19855
+ return { valid: errors3.length === 0, errors: errors3, warnings: [] };
19856
+ }
19857
+
19858
+ // src/decompose/validators/overlap.ts
19859
+ function extractKeywords2(texts) {
19860
+ const words = texts.join(" ").toLowerCase().split(/[\s,.:;!?()\[\]{}"'`\-_/\\]+/).filter((w) => w.length > 2 && !STOP_WORDS2.has(w) && !/^\d+$/.test(w));
19861
+ return new Set(words);
19862
+ }
19863
+ function jaccardSimilarity(a, b) {
19864
+ if (a.size === 0 && b.size === 0)
19865
+ return 0;
19866
+ let intersectionSize = 0;
19867
+ for (const word of a) {
19868
+ if (b.has(word))
19869
+ intersectionSize++;
19870
+ }
19871
+ const unionSize = a.size + b.size - intersectionSize;
19872
+ return unionSize === 0 ? 0 : intersectionSize / unionSize;
19873
+ }
19874
+ function substoryKeywords(s) {
19875
+ return extractKeywords2([s.title, ...s.tags]);
19876
+ }
19877
+ function storyKeywords(s) {
19878
+ return extractKeywords2([s.title, ...s.tags ?? []]);
19879
+ }
19880
+ function validateOverlap(substories, existingStories) {
19881
+ const errors3 = [];
19882
+ const warnings = [];
19883
+ for (const sub of substories) {
19884
+ const subKw = substoryKeywords(sub);
19885
+ for (const existing of existingStories) {
19886
+ const exKw = storyKeywords(existing);
19887
+ const sim = jaccardSimilarity(subKw, exKw);
19888
+ if (sim > 0.8) {
19889
+ errors3.push(`Substory ${sub.id} overlaps with existing story ${existing.id} (similarity ${sim.toFixed(2)} > 0.8)`);
19890
+ } else if (sim > 0.6) {
19891
+ warnings.push(`Substory ${sub.id} may overlap with existing story ${existing.id} (similarity ${sim.toFixed(2)} > 0.6)`);
19892
+ }
19893
+ }
19894
+ }
19895
+ return { valid: errors3.length === 0, errors: errors3, warnings };
19896
+ }
19897
+ var STOP_WORDS2;
19898
+ var init_overlap = __esm(() => {
19899
+ STOP_WORDS2 = new Set([
19900
+ "a",
19901
+ "an",
19902
+ "the",
19903
+ "and",
19904
+ "or",
19905
+ "but",
19906
+ "is",
19907
+ "are",
19908
+ "was",
19909
+ "were",
19910
+ "be",
19911
+ "been",
19912
+ "being",
19913
+ "have",
19914
+ "has",
19915
+ "had",
19916
+ "do",
19917
+ "does",
19918
+ "did",
19919
+ "will",
19920
+ "would",
19921
+ "could",
19922
+ "should",
19923
+ "may",
19924
+ "might",
19925
+ "can",
19926
+ "to",
19927
+ "of",
19928
+ "in",
19929
+ "on",
19930
+ "at",
19931
+ "for",
19932
+ "with",
19933
+ "by",
19934
+ "from",
19935
+ "as",
19936
+ "it",
19937
+ "its",
19938
+ "that",
19939
+ "this",
19940
+ "these",
19941
+ "those",
19942
+ "not",
19943
+ "no",
19944
+ "so",
19945
+ "if",
19946
+ "then",
19947
+ "than",
19948
+ "when",
19949
+ "which",
19950
+ "who",
19951
+ "what",
19952
+ "how",
19953
+ "all",
19954
+ "each",
19955
+ "any",
19956
+ "up",
19957
+ "out",
19958
+ "about",
19959
+ "into",
19960
+ "through",
19961
+ "after",
19962
+ "before"
19963
+ ]);
19964
+ });
19965
+
19966
+ // src/decompose/validators/index.ts
19967
+ function runAllValidators(originalStory, substories, existingStories, config2) {
19968
+ const existingIds = existingStories.map((s) => s.id);
19969
+ const maxComplexity = config2.maxComplexity ?? "medium";
19970
+ const results = [
19971
+ validateOverlap(substories, existingStories),
19972
+ validateCoverage(originalStory, substories),
19973
+ validateComplexity2(substories, maxComplexity),
19974
+ validateDependencies(substories, existingIds)
19975
+ ];
19976
+ const errors3 = results.flatMap((r) => r.errors);
19977
+ const warnings = results.flatMap((r) => r.warnings);
19978
+ return { valid: errors3.length === 0, errors: errors3, warnings };
19979
+ }
19980
+ var init_validators = __esm(() => {
19981
+ init_complexity();
19982
+ init_coverage();
19983
+ init_overlap();
19984
+ });
19985
+
19986
+ // src/decompose/builder.ts
19987
+ class DecomposeBuilder {
19988
+ _story;
19989
+ _prd;
19990
+ _scan;
19991
+ _cfg;
19992
+ constructor(story) {
19993
+ this._story = story;
19994
+ }
19995
+ static for(story) {
19996
+ return new DecomposeBuilder(story);
19997
+ }
19998
+ prd(prd) {
19999
+ this._prd = prd;
20000
+ return this;
20001
+ }
20002
+ codebase(scan) {
20003
+ this._scan = scan;
20004
+ return this;
20005
+ }
20006
+ config(cfg) {
20007
+ this._cfg = cfg;
20008
+ return this;
20009
+ }
20010
+ buildPrompt(errorFeedback) {
20011
+ const sections = [];
20012
+ sections.push(buildTargetStorySection(this._story));
20013
+ if (this._prd) {
20014
+ sections.push(buildSiblingStoriesSection(this._story, this._prd));
20015
+ }
20016
+ if (this._scan) {
20017
+ sections.push(buildCodebaseSection(this._scan));
20018
+ }
20019
+ if (this._cfg) {
20020
+ sections.push(buildConstraintsSection(this._cfg));
20021
+ }
20022
+ if (errorFeedback) {
20023
+ sections.push(`## Validation Errors from Previous Attempt
20024
+
20025
+ Fix the following errors and try again:
20026
+
20027
+ ${errorFeedback}`);
20028
+ }
20029
+ return sections.join(SECTION_SEP);
20030
+ }
20031
+ async decompose(adapter) {
20032
+ const cfg = this._cfg;
20033
+ const maxRetries = cfg?.maxRetries ?? 0;
20034
+ const existingStories = this._prd ? this._prd.userStories.filter((s) => s.id !== this._story.id) : [];
20035
+ let lastResult;
20036
+ let errorFeedback;
20037
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
20038
+ const prompt = this.buildPrompt(errorFeedback);
20039
+ const raw = await adapter.decompose(prompt);
20040
+ const parsed = parseSubStories(raw);
20041
+ if (!parsed.validation.valid) {
20042
+ lastResult = parsed;
20043
+ errorFeedback = parsed.validation.errors.join(`
20044
+ `);
20045
+ continue;
20046
+ }
20047
+ const config2 = cfg ?? { maxSubStories: 5, maxComplexity: "medium" };
20048
+ const validation = runAllValidators(this._story, parsed.subStories, existingStories, config2);
20049
+ if (!validation.valid) {
20050
+ lastResult = { subStories: parsed.subStories, validation };
20051
+ errorFeedback = validation.errors.join(`
20052
+ `);
20053
+ continue;
20054
+ }
20055
+ return { subStories: parsed.subStories, validation };
20056
+ }
20057
+ return lastResult ?? {
20058
+ subStories: [],
20059
+ validation: { valid: false, errors: ["Decomposition failed after all retries"], warnings: [] }
20060
+ };
20061
+ }
20062
+ }
20063
+ function parseSubStories(output) {
20064
+ const fenceMatch = output.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
20065
+ let jsonText = fenceMatch ? fenceMatch[1] : output;
20066
+ if (!fenceMatch) {
20067
+ const arrayMatch = output.match(/\[[\s\S]*\]/);
20068
+ if (arrayMatch) {
20069
+ jsonText = arrayMatch[0];
20070
+ }
20071
+ }
20072
+ let parsed;
20073
+ try {
20074
+ parsed = JSON.parse(jsonText.trim());
20075
+ } catch (err) {
20076
+ return {
20077
+ subStories: [],
20078
+ validation: { valid: false, errors: [`Failed to parse JSON: ${err.message}`], warnings: [] }
20079
+ };
20080
+ }
20081
+ if (!Array.isArray(parsed)) {
20082
+ return {
20083
+ subStories: [],
20084
+ validation: { valid: false, errors: ["Output is not a JSON array"], warnings: [] }
20085
+ };
20086
+ }
20087
+ const errors3 = [];
20088
+ const subStories = [];
20089
+ for (const [index, item] of parsed.entries()) {
20090
+ if (typeof item !== "object" || item === null) {
20091
+ errors3.push(`Item at index ${index} is not an object`);
20092
+ continue;
20093
+ }
20094
+ const r = item;
20095
+ subStories.push({
20096
+ id: String(r.id ?? ""),
20097
+ parentStoryId: String(r.parentStoryId ?? ""),
20098
+ title: String(r.title ?? ""),
20099
+ description: String(r.description ?? ""),
20100
+ acceptanceCriteria: Array.isArray(r.acceptanceCriteria) ? r.acceptanceCriteria : [],
20101
+ tags: Array.isArray(r.tags) ? r.tags : [],
20102
+ dependencies: Array.isArray(r.dependencies) ? r.dependencies : [],
20103
+ complexity: normalizeComplexity(r.complexity),
20104
+ nonOverlapJustification: String(r.nonOverlapJustification ?? "")
20105
+ });
20106
+ }
20107
+ return {
20108
+ subStories,
20109
+ validation: { valid: errors3.length === 0, errors: errors3, warnings: [] }
20110
+ };
20111
+ }
20112
+ function normalizeComplexity(value) {
20113
+ if (value === "simple" || value === "medium" || value === "complex" || value === "expert") {
20114
+ return value;
20115
+ }
20116
+ return "medium";
20117
+ }
20118
+ var SECTION_SEP = `
20119
+
20120
+ ---
20121
+
20122
+ `;
20123
+ var init_builder2 = __esm(() => {
20124
+ init_validators();
19613
20125
  });
19614
20126
 
19615
20127
  // src/prd/types.ts
@@ -19673,7 +20185,7 @@ function getNextStory(prd, currentStoryId, maxRetries) {
19673
20185
  }
19674
20186
  }
19675
20187
  const completedIds = new Set(prd.userStories.filter((s) => s.passes || s.status === "passed" || s.status === "skipped").map((s) => s.id));
19676
- return prd.userStories.find((s) => !s.passes && s.status !== "passed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "failed" && s.status !== "paused" && s.dependencies.every((dep) => completedIds.has(dep))) ?? null;
20188
+ return prd.userStories.find((s) => !s.passes && s.status !== "passed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "failed" && s.status !== "paused" && s.status !== "decomposed" && s.dependencies.every((dep) => completedIds.has(dep))) ?? null;
19677
20189
  }
19678
20190
  function isComplete(prd) {
19679
20191
  return prd.userStories.every((s) => s.passes || s.status === "passed" || s.status === "skipped");
@@ -19683,10 +20195,11 @@ function countStories(prd) {
19683
20195
  total: prd.userStories.length,
19684
20196
  passed: prd.userStories.filter((s) => s.passes || s.status === "passed").length,
19685
20197
  failed: prd.userStories.filter((s) => s.status === "failed").length,
19686
- pending: prd.userStories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "failed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "paused").length,
20198
+ pending: prd.userStories.filter((s) => !s.passes && s.status !== "passed" && s.status !== "failed" && s.status !== "skipped" && s.status !== "blocked" && s.status !== "paused" && s.status !== "decomposed").length,
19687
20199
  skipped: prd.userStories.filter((s) => s.status === "skipped").length,
19688
20200
  blocked: prd.userStories.filter((s) => s.status === "blocked").length,
19689
- paused: prd.userStories.filter((s) => s.status === "paused").length
20201
+ paused: prd.userStories.filter((s) => s.status === "paused").length,
20202
+ decomposed: prd.userStories.filter((s) => s.status === "decomposed").length
19690
20203
  };
19691
20204
  }
19692
20205
  function markStoryPassed(prd, storyId) {
@@ -19724,6 +20237,92 @@ var init_prd = __esm(() => {
19724
20237
  PRD_MAX_FILE_SIZE = 5 * 1024 * 1024;
19725
20238
  });
19726
20239
 
20240
+ // package.json
20241
+ var package_default;
20242
+ var init_package = __esm(() => {
20243
+ package_default = {
20244
+ name: "@nathapp/nax",
20245
+ version: "0.33.0",
20246
+ description: "AI Coding Agent Orchestrator \u2014 loops until done",
20247
+ type: "module",
20248
+ bin: {
20249
+ nax: "./dist/nax.js"
20250
+ },
20251
+ scripts: {
20252
+ prepare: "git config core.hooksPath .githooks",
20253
+ dev: "bun run bin/nax.ts",
20254
+ build: 'bun build bin/nax.ts --outdir dist --target bun --define "GIT_COMMIT=\\"$(git rev-parse --short HEAD)\\""',
20255
+ typecheck: "bun x tsc --noEmit",
20256
+ lint: "bun x biome check src/ bin/",
20257
+ test: "NAX_SKIP_PRECHECK=1 bun test test/ --timeout=60000",
20258
+ "test:watch": "bun test --watch",
20259
+ "test:unit": "bun test ./test/unit/ --timeout=60000",
20260
+ "test:integration": "bun test ./test/integration/ --timeout=60000",
20261
+ "test:ui": "bun test ./test/ui/ --timeout=60000",
20262
+ prepublishOnly: "bun run build"
20263
+ },
20264
+ dependencies: {
20265
+ "@anthropic-ai/sdk": "^0.74.0",
20266
+ "@types/react": "^19.2.14",
20267
+ chalk: "^5.6.2",
20268
+ commander: "^13.1.0",
20269
+ ink: "^6.7.0",
20270
+ "ink-spinner": "^5.0.0",
20271
+ "ink-testing-library": "^4.0.0",
20272
+ react: "^19.2.4",
20273
+ zod: "^4.3.6"
20274
+ },
20275
+ devDependencies: {
20276
+ "@biomejs/biome": "^1.9.4",
20277
+ "@types/bun": "^1.3.8",
20278
+ "react-devtools-core": "^7.0.1",
20279
+ typescript: "^5.7.3"
20280
+ },
20281
+ license: "MIT",
20282
+ author: "William Khoo",
20283
+ keywords: [
20284
+ "ai",
20285
+ "agent",
20286
+ "orchestrator",
20287
+ "tdd",
20288
+ "coding"
20289
+ ],
20290
+ files: [
20291
+ "dist/",
20292
+ "src/",
20293
+ "bin/",
20294
+ "README.md",
20295
+ "CHANGELOG.md"
20296
+ ]
20297
+ };
20298
+ });
20299
+
20300
+ // src/version.ts
20301
+ var NAX_VERSION, NAX_COMMIT, NAX_BUILD_INFO;
20302
+ var init_version = __esm(() => {
20303
+ init_package();
20304
+ NAX_VERSION = package_default.version;
20305
+ NAX_COMMIT = (() => {
20306
+ try {
20307
+ if (/^[0-9a-f]{6,10}$/.test("f154976"))
20308
+ return "f154976";
20309
+ } catch {}
20310
+ try {
20311
+ const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
20312
+ cwd: import.meta.dir,
20313
+ stderr: "ignore"
20314
+ });
20315
+ if (result.exitCode === 0) {
20316
+ const hash2 = result.stdout.toString().trim();
20317
+ if (/^[0-9a-f]{6,10}$/.test(hash2))
20318
+ return hash2;
20319
+ }
20320
+ } catch {}
20321
+ return "dev";
20322
+ })();
20323
+ NAX_BUILD_INFO = NAX_COMMIT === "dev" ? `v${NAX_VERSION}` : `v${NAX_VERSION} (${NAX_COMMIT})`;
20324
+ });
20325
+
19727
20326
  // src/errors.ts
19728
20327
  var NaxError, AgentNotFoundError, AgentNotInstalledError, StoryLimitExceededError, LockAcquisitionError;
19729
20328
  var init_errors3 = __esm(() => {
@@ -20015,6 +20614,11 @@ var init_types2 = __esm(() => {
20015
20614
  safety: "yellow",
20016
20615
  defaultSummary: "Human review required for story {{storyId}} \u2014 skip and continue?"
20017
20616
  },
20617
+ "story-oversized": {
20618
+ defaultFallback: "continue",
20619
+ safety: "yellow",
20620
+ defaultSummary: "Story {{storyId}} is oversized ({{criteriaCount}} acceptance criteria) \u2014 decompose into smaller stories?"
20621
+ },
20018
20622
  "story-ambiguity": {
20019
20623
  defaultFallback: "continue",
20020
20624
  safety: "green",
@@ -21103,6 +21707,20 @@ async function checkReviewGate(context, config2, chain) {
21103
21707
  const response = await executeTrigger("review-gate", context, config2, chain);
21104
21708
  return response.action === "approve";
21105
21709
  }
21710
+ async function checkStoryOversized(context, config2, chain) {
21711
+ if (!isTriggerEnabled("story-oversized", config2))
21712
+ return "continue";
21713
+ try {
21714
+ const response = await executeTrigger("story-oversized", context, config2, chain);
21715
+ if (response.action === "approve")
21716
+ return "decompose";
21717
+ if (response.action === "skip")
21718
+ return "skip";
21719
+ return "continue";
21720
+ } catch {
21721
+ return "continue";
21722
+ }
21723
+ }
21106
21724
  var init_triggers = __esm(() => {
21107
21725
  init_types2();
21108
21726
  });
@@ -21641,7 +22259,8 @@ class ReviewOrchestrator {
21641
22259
  name: reviewer.name,
21642
22260
  passed: result.passed,
21643
22261
  output: result.output,
21644
- exitCode: result.exitCode
22262
+ exitCode: result.exitCode,
22263
+ findings: result.findings
21645
22264
  });
21646
22265
  if (!result.passed) {
21647
22266
  builtIn.pluginReviewers = pluginResults;
@@ -21697,6 +22316,10 @@ var init_review = __esm(() => {
21697
22316
  const result = await reviewOrchestrator.review(ctx.config.review, ctx.workdir, ctx.config.execution, ctx.plugins);
21698
22317
  ctx.reviewResult = result.builtIn;
21699
22318
  if (!result.success) {
22319
+ const allFindings = result.builtIn.pluginReviewers?.flatMap((pr) => pr.findings ?? []) ?? [];
22320
+ if (allFindings.length > 0) {
22321
+ ctx.reviewFindings = allFindings;
22322
+ }
21700
22323
  if (result.pluginFailed) {
21701
22324
  if (ctx.interaction && isTriggerEnabled("security-review", ctx.config)) {
21702
22325
  const shouldContinue = await _reviewDeps.checkSecurityReview({ featureName: ctx.prd.feature, storyId: ctx.story.id }, ctx.config, ctx.interaction);
@@ -22026,7 +22649,7 @@ var init_constitution2 = __esm(() => {
22026
22649
  });
22027
22650
 
22028
22651
  // src/context/auto-detect.ts
22029
- function extractKeywords(title) {
22652
+ function extractKeywords3(title) {
22030
22653
  const stopWords = new Set([
22031
22654
  "the",
22032
22655
  "a",
@@ -22079,7 +22702,7 @@ function extractKeywords(title) {
22079
22702
  async function autoDetectContextFiles(options) {
22080
22703
  const { workdir, storyTitle, maxFiles = 5 } = options;
22081
22704
  const logger = getLogger();
22082
- const keywords = extractKeywords(storyTitle);
22705
+ const keywords = extractKeywords3(storyTitle);
22083
22706
  if (keywords.length === 0) {
22084
22707
  logger.debug("auto-detect", "No keywords extracted from story title", { storyTitle });
22085
22708
  return [];
@@ -22213,6 +22836,20 @@ function formatPriorFailures(failures) {
22213
22836
  }
22214
22837
  }
22215
22838
  }
22839
+ if (failure.reviewFindings && failure.reviewFindings.length > 0) {
22840
+ parts.push(`
22841
+ **Review Findings (fix these issues):**`);
22842
+ for (const finding of failure.reviewFindings) {
22843
+ const source = finding.source ? ` (${finding.source})` : "";
22844
+ parts.push(`
22845
+ - **[${finding.severity}]** \`${finding.file}:${finding.line}\`${source}`);
22846
+ parts.push(` **Rule:** ${finding.ruleId}`);
22847
+ parts.push(` **Issue:** ${finding.message}`);
22848
+ if (finding.url) {
22849
+ parts.push(` **Docs:** ${finding.url}`);
22850
+ }
22851
+ }
22852
+ }
22216
22853
  parts.push("");
22217
22854
  }
22218
22855
  return parts.join(`
@@ -22440,7 +23077,7 @@ async function generateTestCoverageSummary(options) {
22440
23077
  var COMMON_TEST_DIRS;
22441
23078
  var init_test_scanner = __esm(() => {
22442
23079
  init_logger2();
22443
- init_builder2();
23080
+ init_builder3();
22444
23081
  COMMON_TEST_DIRS = ["test", "tests", "__tests__", "src/__tests__", "spec"];
22445
23082
  });
22446
23083
 
@@ -22719,7 +23356,7 @@ ${content}
22719
23356
  }
22720
23357
  }
22721
23358
  var _deps4;
22722
- var init_builder2 = __esm(() => {
23359
+ var init_builder3 = __esm(() => {
22723
23360
  init_logger2();
22724
23361
  init_prd();
22725
23362
  init_auto_detect();
@@ -22733,7 +23370,7 @@ var init_builder2 = __esm(() => {
22733
23370
 
22734
23371
  // src/context/index.ts
22735
23372
  var init_context = __esm(() => {
22736
- init_builder2();
23373
+ init_builder3();
22737
23374
  init_test_scanner();
22738
23375
  init_auto_detect();
22739
23376
  });
@@ -23948,7 +24585,7 @@ ${this._constitution}`);
23948
24585
  sections.push(this._contextMd);
23949
24586
  }
23950
24587
  sections.push(buildConventionsSection());
23951
- return sections.join(SECTION_SEP);
24588
+ return sections.join(SECTION_SEP2);
23952
24589
  }
23953
24590
  async _resolveRoleBody() {
23954
24591
  if (this._workdir && this._loaderConfig) {
@@ -23970,18 +24607,18 @@ ${this._constitution}`);
23970
24607
  return buildRoleTaskSection(this._role, variant);
23971
24608
  }
23972
24609
  }
23973
- var SECTION_SEP = `
24610
+ var SECTION_SEP2 = `
23974
24611
 
23975
24612
  ---
23976
24613
 
23977
24614
  `;
23978
- var init_builder3 = __esm(() => {
24615
+ var init_builder4 = __esm(() => {
23979
24616
  init_isolation2();
23980
24617
  });
23981
24618
 
23982
24619
  // src/prompts/index.ts
23983
24620
  var init_prompts2 = __esm(() => {
23984
- init_builder3();
24621
+ init_builder4();
23985
24622
  });
23986
24623
 
23987
24624
  // src/tdd/session-runner.ts
@@ -25940,9 +26577,25 @@ var init_regression2 = __esm(() => {
25940
26577
  });
25941
26578
 
25942
26579
  // src/pipeline/stages/routing.ts
26580
+ async function runDecompose(story, prd, config2, _workdir) {
26581
+ const naxDecompose = config2.decompose;
26582
+ const builderConfig = {
26583
+ maxSubStories: naxDecompose?.maxSubstories ?? 5,
26584
+ maxComplexity: naxDecompose?.maxSubstoryComplexity ?? "medium",
26585
+ maxRetries: naxDecompose?.maxRetries ?? 2
26586
+ };
26587
+ const adapter = {
26588
+ async decompose(_prompt) {
26589
+ throw new Error("[decompose] No LLM adapter configured for story decomposition");
26590
+ }
26591
+ };
26592
+ return DecomposeBuilder.for(story).prd(prd).config(builderConfig).decompose(adapter);
26593
+ }
25943
26594
  var routingStage, _routingDeps;
25944
26595
  var init_routing2 = __esm(() => {
25945
26596
  init_greenfield();
26597
+ init_builder2();
26598
+ init_triggers();
25946
26599
  init_logger2();
25947
26600
  init_prd();
25948
26601
  init_routing();
@@ -26011,6 +26664,46 @@ var init_routing2 = __esm(() => {
26011
26664
  if (!isBatch) {
26012
26665
  logger.debug("routing", ctx.routing.reasoning);
26013
26666
  }
26667
+ const decomposeConfig = ctx.config.decompose;
26668
+ if (decomposeConfig) {
26669
+ const acCount = ctx.story.acceptanceCriteria.length;
26670
+ const complexity = ctx.routing.complexity;
26671
+ const isOversized = acCount > decomposeConfig.maxAcceptanceCriteria && (complexity === "complex" || complexity === "expert");
26672
+ if (isOversized) {
26673
+ if (decomposeConfig.trigger === "disabled") {
26674
+ logger.warn("routing", `Story ${ctx.story.id} is oversized (${acCount} ACs) but decompose is disabled \u2014 continuing with original`);
26675
+ } else if (decomposeConfig.trigger === "auto") {
26676
+ const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir);
26677
+ if (result.validation.valid) {
26678
+ _routingDeps.applyDecomposition(ctx.prd, result);
26679
+ if (ctx.prdPath) {
26680
+ await _routingDeps.savePRD(ctx.prd, ctx.prdPath);
26681
+ }
26682
+ logger.info("routing", `Story ${ctx.story.id} decomposed into ${result.subStories.length} substories`);
26683
+ return { action: "skip", reason: `Decomposed into ${result.subStories.length} substories` };
26684
+ }
26685
+ logger.warn("routing", `Story ${ctx.story.id} decompose failed after retries \u2014 continuing with original`, {
26686
+ errors: result.validation.errors
26687
+ });
26688
+ } else if (decomposeConfig.trigger === "confirm") {
26689
+ const action = await _routingDeps.checkStoryOversized({ featureName: ctx.prd.feature, storyId: ctx.story.id, criteriaCount: acCount }, ctx.config, ctx.interaction);
26690
+ if (action === "decompose") {
26691
+ const result = await _routingDeps.runDecompose(ctx.story, ctx.prd, ctx.config, ctx.workdir);
26692
+ if (result.validation.valid) {
26693
+ _routingDeps.applyDecomposition(ctx.prd, result);
26694
+ if (ctx.prdPath) {
26695
+ await _routingDeps.savePRD(ctx.prd, ctx.prdPath);
26696
+ }
26697
+ logger.info("routing", `Story ${ctx.story.id} decomposed into ${result.subStories.length} substories`);
26698
+ return { action: "skip", reason: `Decomposed into ${result.subStories.length} substories` };
26699
+ }
26700
+ logger.warn("routing", `Story ${ctx.story.id} decompose failed after retries \u2014 continuing with original`, {
26701
+ errors: result.validation.errors
26702
+ });
26703
+ }
26704
+ }
26705
+ }
26706
+ }
26014
26707
  return { action: "continue" };
26015
26708
  }
26016
26709
  };
@@ -26020,7 +26713,10 @@ var init_routing2 = __esm(() => {
26020
26713
  isGreenfieldStory,
26021
26714
  clearCache,
26022
26715
  savePRD,
26023
- computeStoryContentHash
26716
+ computeStoryContentHash,
26717
+ applyDecomposition,
26718
+ runDecompose,
26719
+ checkStoryOversized
26024
26720
  };
26025
26721
  });
26026
26722
 
@@ -27837,12 +28533,13 @@ var init_tier_outcome = __esm(() => {
27837
28533
  });
27838
28534
 
27839
28535
  // src/execution/escalation/tier-escalation.ts
27840
- function buildEscalationFailure(story, currentTier) {
28536
+ function buildEscalationFailure(story, currentTier, reviewFindings) {
27841
28537
  return {
27842
28538
  attempt: (story.attempts ?? 0) + 1,
27843
28539
  modelTier: currentTier,
27844
28540
  stage: "escalation",
27845
28541
  summary: `Failed with tier ${currentTier}, escalating to next tier`,
28542
+ reviewFindings: reviewFindings && reviewFindings.length > 0 ? reviewFindings : undefined,
27846
28543
  timestamp: new Date().toISOString()
27847
28544
  };
27848
28545
  }
@@ -27885,6 +28582,7 @@ async function handleTierEscalation(ctx) {
27885
28582
  const storiesToEscalate = ctx.isBatchExecution && escalateWholeBatch ? ctx.storiesToExecute : [ctx.story];
27886
28583
  const escalateRetryAsLite = ctx.pipelineResult.context.retryAsLite === true;
27887
28584
  const escalateFailureCategory = ctx.pipelineResult.context.tddFailureCategory;
28585
+ const escalateReviewFindings = ctx.pipelineResult.context.reviewFindings;
27888
28586
  const escalateRetryAsTestAfter = escalateFailureCategory === "greenfield-no-tests";
27889
28587
  const routingMode = ctx.config.routing.llm?.mode ?? "hybrid";
27890
28588
  if (!nextTier || !ctx.config.autoMode.escalation.enabled) {
@@ -27932,7 +28630,7 @@ async function handleTierEscalation(ctx) {
27932
28630
  const currentStoryTier = s.routing?.modelTier ?? ctx.routing.modelTier;
27933
28631
  const isChangingTier = currentStoryTier !== nextTier;
27934
28632
  const shouldResetAttempts = isChangingTier || shouldSwitchToTestAfter;
27935
- const escalationFailure = buildEscalationFailure(s, currentStoryTier);
28633
+ const escalationFailure = buildEscalationFailure(s, currentStoryTier, escalateReviewFindings);
27936
28634
  return {
27937
28635
  ...s,
27938
28636
  attempts: shouldResetAttempts ? 0 : (s.attempts ?? 0) + 1,
@@ -30129,10 +30827,7 @@ async function executeSequential(ctx, initialPrd) {
30129
30827
  logger?.info("execution", "Running post-run pipeline (acceptance tests)");
30130
30828
  await runPipeline(postRunPipeline, { config: ctx.config, prd, workdir: ctx.workdir, story: prd.userStories[0] }, ctx.eventEmitter);
30131
30829
  return buildResult("max-iterations");
30132
- } finally {
30133
- stopHeartbeat();
30134
- writeExitSummary(ctx.logFilePath, totalCost, iterations, storiesCompleted, Date.now() - ctx.startTime);
30135
- }
30830
+ } finally {}
30136
30831
  }
30137
30832
  var init_sequential_executor = __esm(() => {
30138
30833
  init_triggers();
@@ -61663,7 +62358,9 @@ function detectTestPatterns(workdir, dependencies, devDependencies) {
61663
62358
 
61664
62359
  // src/cli/analyze.ts
61665
62360
  init_schema();
62361
+ init_builder2();
61666
62362
  init_logger2();
62363
+ init_prd();
61667
62364
  init_routing();
61668
62365
  init_version();
61669
62366
 
@@ -64013,7 +64710,14 @@ var FIELD_DESCRIPTIONS = {
64013
64710
  "prompts.overrides.test-writer": 'Path to custom test-writer prompt (e.g., ".nax/prompts/test-writer.md")',
64014
64711
  "prompts.overrides.implementer": 'Path to custom implementer prompt (e.g., ".nax/prompts/implementer.md")',
64015
64712
  "prompts.overrides.verifier": 'Path to custom verifier prompt (e.g., ".nax/prompts/verifier.md")',
64016
- "prompts.overrides.single-session": 'Path to custom single-session prompt (e.g., ".nax/prompts/single-session.md")'
64713
+ "prompts.overrides.single-session": 'Path to custom single-session prompt (e.g., ".nax/prompts/single-session.md")',
64714
+ decompose: "Story decomposition configuration (SD-003)",
64715
+ "decompose.trigger": "Decomposition trigger mode: auto | confirm | disabled",
64716
+ "decompose.maxAcceptanceCriteria": "Max acceptance criteria before flagging as oversized (default: 6)",
64717
+ "decompose.maxSubstories": "Max number of substories to generate (default: 5)",
64718
+ "decompose.maxSubstoryComplexity": "Max complexity for any generated substory (default: 'medium')",
64719
+ "decompose.maxRetries": "Max retries on decomposition validation failure (default: 2)",
64720
+ "decompose.model": "Model tier for decomposition LLM calls (default: 'balanced')"
64017
64721
  };
64018
64722
  async function loadConfigFile(path13) {
64019
64723
  if (!existsSync20(path13))