@nathapp/nax 0.32.1 → 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.
- package/dist/nax.js +823 -105
- package/package.json +1 -1
- package/src/cli/analyze.ts +145 -0
- package/src/cli/config.ts +9 -0
- package/src/config/defaults.ts +8 -0
- package/src/config/schema.ts +1 -0
- package/src/config/schemas.ts +10 -0
- package/src/config/types.ts +18 -0
- package/src/context/elements.ts +13 -0
- package/src/context/greenfield.ts +1 -1
- package/src/decompose/apply.ts +44 -0
- package/src/decompose/builder.ts +181 -0
- package/src/decompose/index.ts +8 -0
- package/src/decompose/sections/codebase.ts +26 -0
- package/src/decompose/sections/constraints.ts +32 -0
- package/src/decompose/sections/index.ts +4 -0
- package/src/decompose/sections/sibling-stories.ts +25 -0
- package/src/decompose/sections/target-story.ts +31 -0
- package/src/decompose/types.ts +55 -0
- package/src/decompose/validators/complexity.ts +45 -0
- package/src/decompose/validators/coverage.ts +134 -0
- package/src/decompose/validators/dependency.ts +91 -0
- package/src/decompose/validators/index.ts +35 -0
- package/src/decompose/validators/overlap.ts +128 -0
- package/src/execution/escalation/tier-escalation.ts +9 -2
- package/src/execution/sequential-executor.ts +4 -3
- package/src/interaction/index.ts +1 -0
- package/src/interaction/triggers.ts +21 -0
- package/src/interaction/types.ts +7 -0
- package/src/pipeline/stages/review.ts +6 -0
- package/src/pipeline/stages/routing.ts +89 -0
- package/src/pipeline/types.ts +2 -0
- package/src/plugins/types.ts +33 -0
- package/src/prd/index.ts +5 -1
- package/src/prd/types.ts +11 -1
- package/src/review/orchestrator.ts +1 -0
- package/src/review/types.ts +2 -0
- package/src/tdd/isolation.ts +1 -1
- package/src/tdd/rectification-gate.ts +23 -2
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
|
-
//
|
|
19530
|
-
|
|
19531
|
-
|
|
19532
|
-
|
|
19533
|
-
|
|
19534
|
-
|
|
19535
|
-
|
|
19536
|
-
|
|
19537
|
-
|
|
19538
|
-
|
|
19539
|
-
|
|
19540
|
-
|
|
19541
|
-
|
|
19542
|
-
|
|
19543
|
-
|
|
19544
|
-
|
|
19545
|
-
|
|
19546
|
-
|
|
19547
|
-
|
|
19548
|
-
|
|
19549
|
-
|
|
19550
|
-
|
|
19551
|
-
|
|
19552
|
-
|
|
19553
|
-
|
|
19554
|
-
|
|
19555
|
-
|
|
19556
|
-
|
|
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/
|
|
19590
|
-
|
|
19591
|
-
|
|
19592
|
-
|
|
19593
|
-
|
|
19594
|
-
|
|
19595
|
-
|
|
19596
|
-
|
|
19597
|
-
|
|
19598
|
-
|
|
19599
|
-
|
|
19600
|
-
|
|
19601
|
-
|
|
19602
|
-
|
|
19603
|
-
|
|
19604
|
-
|
|
19605
|
-
|
|
19606
|
-
|
|
19607
|
-
|
|
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
|
-
}
|
|
19610
|
-
|
|
19611
|
-
|
|
19612
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
23373
|
+
init_builder3();
|
|
22737
23374
|
init_test_scanner();
|
|
22738
23375
|
init_auto_detect();
|
|
22739
23376
|
});
|
|
@@ -23625,7 +24262,21 @@ async function runFullSuiteGate(story, config2, workdir, agent, implementerTier,
|
|
|
23625
24262
|
if (testSummary.failed > 0) {
|
|
23626
24263
|
return await runRectificationLoop(story, config2, workdir, agent, implementerTier, contextMarkdown, lite, logger, testSummary, rectificationConfig, testCmd, fullSuiteTimeout);
|
|
23627
24264
|
}
|
|
23628
|
-
|
|
24265
|
+
if (testSummary.passed > 0) {
|
|
24266
|
+
logger.info("tdd", "Full suite gate passed (non-zero exit, 0 failures, tests detected)", {
|
|
24267
|
+
storyId: story.id,
|
|
24268
|
+
exitCode: fullSuiteResult.exitCode,
|
|
24269
|
+
passedTests: testSummary.passed
|
|
24270
|
+
});
|
|
24271
|
+
return true;
|
|
24272
|
+
}
|
|
24273
|
+
logger.warn("tdd", "Full suite gate inconclusive \u2014 no test results parsed from output (possible crash/OOM)", {
|
|
24274
|
+
storyId: story.id,
|
|
24275
|
+
exitCode: fullSuiteResult.exitCode,
|
|
24276
|
+
outputLength: fullSuiteResult.output.length,
|
|
24277
|
+
outputTail: fullSuiteResult.output.slice(-200)
|
|
24278
|
+
});
|
|
24279
|
+
return false;
|
|
23629
24280
|
}
|
|
23630
24281
|
if (fullSuitePassed) {
|
|
23631
24282
|
logger.info("tdd", "Full suite gate passed", { storyId: story.id });
|
|
@@ -23934,7 +24585,7 @@ ${this._constitution}`);
|
|
|
23934
24585
|
sections.push(this._contextMd);
|
|
23935
24586
|
}
|
|
23936
24587
|
sections.push(buildConventionsSection());
|
|
23937
|
-
return sections.join(
|
|
24588
|
+
return sections.join(SECTION_SEP2);
|
|
23938
24589
|
}
|
|
23939
24590
|
async _resolveRoleBody() {
|
|
23940
24591
|
if (this._workdir && this._loaderConfig) {
|
|
@@ -23956,18 +24607,18 @@ ${this._constitution}`);
|
|
|
23956
24607
|
return buildRoleTaskSection(this._role, variant);
|
|
23957
24608
|
}
|
|
23958
24609
|
}
|
|
23959
|
-
var
|
|
24610
|
+
var SECTION_SEP2 = `
|
|
23960
24611
|
|
|
23961
24612
|
---
|
|
23962
24613
|
|
|
23963
24614
|
`;
|
|
23964
|
-
var
|
|
24615
|
+
var init_builder4 = __esm(() => {
|
|
23965
24616
|
init_isolation2();
|
|
23966
24617
|
});
|
|
23967
24618
|
|
|
23968
24619
|
// src/prompts/index.ts
|
|
23969
24620
|
var init_prompts2 = __esm(() => {
|
|
23970
|
-
|
|
24621
|
+
init_builder4();
|
|
23971
24622
|
});
|
|
23972
24623
|
|
|
23973
24624
|
// src/tdd/session-runner.ts
|
|
@@ -25926,9 +26577,25 @@ var init_regression2 = __esm(() => {
|
|
|
25926
26577
|
});
|
|
25927
26578
|
|
|
25928
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
|
+
}
|
|
25929
26594
|
var routingStage, _routingDeps;
|
|
25930
26595
|
var init_routing2 = __esm(() => {
|
|
25931
26596
|
init_greenfield();
|
|
26597
|
+
init_builder2();
|
|
26598
|
+
init_triggers();
|
|
25932
26599
|
init_logger2();
|
|
25933
26600
|
init_prd();
|
|
25934
26601
|
init_routing();
|
|
@@ -25997,6 +26664,46 @@ var init_routing2 = __esm(() => {
|
|
|
25997
26664
|
if (!isBatch) {
|
|
25998
26665
|
logger.debug("routing", ctx.routing.reasoning);
|
|
25999
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
|
+
}
|
|
26000
26707
|
return { action: "continue" };
|
|
26001
26708
|
}
|
|
26002
26709
|
};
|
|
@@ -26006,7 +26713,10 @@ var init_routing2 = __esm(() => {
|
|
|
26006
26713
|
isGreenfieldStory,
|
|
26007
26714
|
clearCache,
|
|
26008
26715
|
savePRD,
|
|
26009
|
-
computeStoryContentHash
|
|
26716
|
+
computeStoryContentHash,
|
|
26717
|
+
applyDecomposition,
|
|
26718
|
+
runDecompose,
|
|
26719
|
+
checkStoryOversized
|
|
26010
26720
|
};
|
|
26011
26721
|
});
|
|
26012
26722
|
|
|
@@ -27823,12 +28533,13 @@ var init_tier_outcome = __esm(() => {
|
|
|
27823
28533
|
});
|
|
27824
28534
|
|
|
27825
28535
|
// src/execution/escalation/tier-escalation.ts
|
|
27826
|
-
function buildEscalationFailure(story, currentTier) {
|
|
28536
|
+
function buildEscalationFailure(story, currentTier, reviewFindings) {
|
|
27827
28537
|
return {
|
|
27828
28538
|
attempt: (story.attempts ?? 0) + 1,
|
|
27829
28539
|
modelTier: currentTier,
|
|
27830
28540
|
stage: "escalation",
|
|
27831
28541
|
summary: `Failed with tier ${currentTier}, escalating to next tier`,
|
|
28542
|
+
reviewFindings: reviewFindings && reviewFindings.length > 0 ? reviewFindings : undefined,
|
|
27832
28543
|
timestamp: new Date().toISOString()
|
|
27833
28544
|
};
|
|
27834
28545
|
}
|
|
@@ -27871,6 +28582,7 @@ async function handleTierEscalation(ctx) {
|
|
|
27871
28582
|
const storiesToEscalate = ctx.isBatchExecution && escalateWholeBatch ? ctx.storiesToExecute : [ctx.story];
|
|
27872
28583
|
const escalateRetryAsLite = ctx.pipelineResult.context.retryAsLite === true;
|
|
27873
28584
|
const escalateFailureCategory = ctx.pipelineResult.context.tddFailureCategory;
|
|
28585
|
+
const escalateReviewFindings = ctx.pipelineResult.context.reviewFindings;
|
|
27874
28586
|
const escalateRetryAsTestAfter = escalateFailureCategory === "greenfield-no-tests";
|
|
27875
28587
|
const routingMode = ctx.config.routing.llm?.mode ?? "hybrid";
|
|
27876
28588
|
if (!nextTier || !ctx.config.autoMode.escalation.enabled) {
|
|
@@ -27918,7 +28630,7 @@ async function handleTierEscalation(ctx) {
|
|
|
27918
28630
|
const currentStoryTier = s.routing?.modelTier ?? ctx.routing.modelTier;
|
|
27919
28631
|
const isChangingTier = currentStoryTier !== nextTier;
|
|
27920
28632
|
const shouldResetAttempts = isChangingTier || shouldSwitchToTestAfter;
|
|
27921
|
-
const escalationFailure = buildEscalationFailure(s, currentStoryTier);
|
|
28633
|
+
const escalationFailure = buildEscalationFailure(s, currentStoryTier, escalateReviewFindings);
|
|
27922
28634
|
return {
|
|
27923
28635
|
...s,
|
|
27924
28636
|
attempts: shouldResetAttempts ? 0 : (s.attempts ?? 0) + 1,
|
|
@@ -30115,10 +30827,7 @@ async function executeSequential(ctx, initialPrd) {
|
|
|
30115
30827
|
logger?.info("execution", "Running post-run pipeline (acceptance tests)");
|
|
30116
30828
|
await runPipeline(postRunPipeline, { config: ctx.config, prd, workdir: ctx.workdir, story: prd.userStories[0] }, ctx.eventEmitter);
|
|
30117
30829
|
return buildResult("max-iterations");
|
|
30118
|
-
} finally {
|
|
30119
|
-
stopHeartbeat();
|
|
30120
|
-
writeExitSummary(ctx.logFilePath, totalCost, iterations, storiesCompleted, Date.now() - ctx.startTime);
|
|
30121
|
-
}
|
|
30830
|
+
} finally {}
|
|
30122
30831
|
}
|
|
30123
30832
|
var init_sequential_executor = __esm(() => {
|
|
30124
30833
|
init_triggers();
|
|
@@ -61649,7 +62358,9 @@ function detectTestPatterns(workdir, dependencies, devDependencies) {
|
|
|
61649
62358
|
|
|
61650
62359
|
// src/cli/analyze.ts
|
|
61651
62360
|
init_schema();
|
|
62361
|
+
init_builder2();
|
|
61652
62362
|
init_logger2();
|
|
62363
|
+
init_prd();
|
|
61653
62364
|
init_routing();
|
|
61654
62365
|
init_version();
|
|
61655
62366
|
|
|
@@ -63999,7 +64710,14 @@ var FIELD_DESCRIPTIONS = {
|
|
|
63999
64710
|
"prompts.overrides.test-writer": 'Path to custom test-writer prompt (e.g., ".nax/prompts/test-writer.md")',
|
|
64000
64711
|
"prompts.overrides.implementer": 'Path to custom implementer prompt (e.g., ".nax/prompts/implementer.md")',
|
|
64001
64712
|
"prompts.overrides.verifier": 'Path to custom verifier prompt (e.g., ".nax/prompts/verifier.md")',
|
|
64002
|
-
"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')"
|
|
64003
64721
|
};
|
|
64004
64722
|
async function loadConfigFile(path13) {
|
|
64005
64723
|
if (!existsSync20(path13))
|