@nathapp/nax 0.70.5 → 0.70.7
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 +444 -257
- package/package.json +1 -1
package/dist/nax.js
CHANGED
|
@@ -24860,9 +24860,10 @@ var init_parser = __esm(() => {
|
|
|
24860
24860
|
|
|
24861
24861
|
// src/test-runners/ac-parser.ts
|
|
24862
24862
|
function parseTestFailures(output) {
|
|
24863
|
-
const
|
|
24863
|
+
const clean = output.replace(ANSI_ESCAPE_PATTERN, "");
|
|
24864
|
+
const framework = detectFramework(clean);
|
|
24864
24865
|
const failedACs = [];
|
|
24865
|
-
const lines =
|
|
24866
|
+
const lines = clean.split(`
|
|
24866
24867
|
`);
|
|
24867
24868
|
for (const line of lines) {
|
|
24868
24869
|
if (framework === "bun" || framework === "unknown") {
|
|
@@ -24896,7 +24897,7 @@ function parseTestFailures(output) {
|
|
|
24896
24897
|
}
|
|
24897
24898
|
}
|
|
24898
24899
|
if (framework === "jest" || framework === "vitest" || framework === "unknown") {
|
|
24899
|
-
if (/[\u25CF\u00D7\u2715]/.test(line)) {
|
|
24900
|
+
if (/[\u25CF\u00D7\u2715]/.test(line) || /^\s*FAIL\s/.test(line)) {
|
|
24900
24901
|
const acMatch = line.match(/AC[-_]?(\d+)/i);
|
|
24901
24902
|
if (acMatch) {
|
|
24902
24903
|
const acId = `AC-${acMatch[1]}`;
|
|
@@ -24913,8 +24914,10 @@ function parseTestFailures(output) {
|
|
|
24913
24914
|
}
|
|
24914
24915
|
return failedACs;
|
|
24915
24916
|
}
|
|
24917
|
+
var ANSI_ESCAPE_PATTERN;
|
|
24916
24918
|
var init_ac_parser = __esm(() => {
|
|
24917
24919
|
init_detector2();
|
|
24920
|
+
ANSI_ESCAPE_PATTERN = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[A-Za-z]`, "g");
|
|
24918
24921
|
});
|
|
24919
24922
|
|
|
24920
24923
|
// src/utils/git.ts
|
|
@@ -30246,11 +30249,7 @@ async function gitLsFiles2(workdir) {
|
|
|
30246
30249
|
return null;
|
|
30247
30250
|
}
|
|
30248
30251
|
}
|
|
30249
|
-
async function
|
|
30250
|
-
const files = await gitLsFiles2(workdir);
|
|
30251
|
-
if (files !== null) {
|
|
30252
|
-
return files.some((f) => isTestFileByPatterns(f, patterns));
|
|
30253
|
-
}
|
|
30252
|
+
async function hasTestFilesOnDisk(workdir, patterns) {
|
|
30254
30253
|
for (const pattern of patterns) {
|
|
30255
30254
|
const g = new Bun.Glob(pattern);
|
|
30256
30255
|
for await (const path3 of g.scan({ cwd: workdir, onlyFiles: true })) {
|
|
@@ -30261,6 +30260,13 @@ async function hasTestFiles(workdir, patterns) {
|
|
|
30261
30260
|
}
|
|
30262
30261
|
return false;
|
|
30263
30262
|
}
|
|
30263
|
+
async function hasTestFiles(workdir, patterns) {
|
|
30264
|
+
const files = await gitLsFiles2(workdir);
|
|
30265
|
+
if (files !== null) {
|
|
30266
|
+
return files.some((f) => isTestFileByPatterns(f, patterns));
|
|
30267
|
+
}
|
|
30268
|
+
return hasTestFilesOnDisk(workdir, patterns);
|
|
30269
|
+
}
|
|
30264
30270
|
async function isGreenfieldStory(_story, workdir, patterns) {
|
|
30265
30271
|
try {
|
|
30266
30272
|
return !await hasTestFiles(workdir, patterns ?? DEFAULT_TEST_FILE_PATTERNS);
|
|
@@ -30291,6 +30297,7 @@ var init_greenfield = __esm(() => {
|
|
|
30291
30297
|
"out",
|
|
30292
30298
|
"tmp",
|
|
30293
30299
|
"temp",
|
|
30300
|
+
".nax",
|
|
30294
30301
|
".git"
|
|
30295
30302
|
]);
|
|
30296
30303
|
});
|
|
@@ -32865,13 +32872,13 @@ var init_semantic_helpers = __esm(() => {
|
|
|
32865
32872
|
|
|
32866
32873
|
// src/review/semantic-evidence.ts
|
|
32867
32874
|
import { isAbsolute as isAbsolute8 } from "path";
|
|
32868
|
-
async function substantiateSemanticEvidence(findings, diffMode, workdir, storyId, blockingThreshold = "error") {
|
|
32875
|
+
async function substantiateSemanticEvidence(findings, diffMode, workdir, storyId, blockingThreshold = "error", repoRoot) {
|
|
32869
32876
|
if (diffMode !== "ref")
|
|
32870
32877
|
return findings;
|
|
32871
32878
|
return Promise.all(findings.map(async (finding) => {
|
|
32872
32879
|
if (!isBlockingSeverity(finding.severity, blockingThreshold))
|
|
32873
32880
|
return finding;
|
|
32874
|
-
const evidence = await checkFindingEvidence({ finding, workdir });
|
|
32881
|
+
const evidence = await checkFindingEvidence({ finding, workdir, repoRoot });
|
|
32875
32882
|
if (evidence.status !== "unmatched")
|
|
32876
32883
|
return finding;
|
|
32877
32884
|
return downgradeUnsubstantiatedFinding({ finding, storyId, ...evidence });
|
|
@@ -32883,7 +32890,8 @@ async function checkFindingEvidence(opts) {
|
|
|
32883
32890
|
const line = opts.finding.verifiedBy?.line ?? opts.finding.line;
|
|
32884
32891
|
if (!observed)
|
|
32885
32892
|
return { status: "missing-observed", file: file3, line };
|
|
32886
|
-
const
|
|
32893
|
+
const roots = opts.repoRoot && opts.repoRoot !== opts.workdir ? [opts.repoRoot, opts.workdir] : [opts.workdir];
|
|
32894
|
+
const contents = await readSafeFile(roots, file3);
|
|
32887
32895
|
if (contents === null)
|
|
32888
32896
|
return { status: "unreadable", file: file3, line, observed };
|
|
32889
32897
|
return matchesEvidence(contents, observed, line) ? { status: "matched", file: file3, line, observed } : { status: "unmatched", file: file3, line, observed };
|
|
@@ -32912,13 +32920,13 @@ function downgradeUnsubstantiatedFinding(opts) {
|
|
|
32912
32920
|
});
|
|
32913
32921
|
return { ...opts.finding, severity: "unverifiable" };
|
|
32914
32922
|
}
|
|
32915
|
-
async function readSafeFile(
|
|
32916
|
-
const
|
|
32917
|
-
|
|
32918
|
-
|
|
32919
|
-
|
|
32920
|
-
|
|
32921
|
-
|
|
32923
|
+
async function readSafeFile(roots, file3) {
|
|
32924
|
+
for (const root of roots) {
|
|
32925
|
+
const validated = validateModulePath(file3, [root]);
|
|
32926
|
+
if (validated.valid && validated.absolutePath) {
|
|
32927
|
+
try {
|
|
32928
|
+
return await Bun.file(validated.absolutePath).text();
|
|
32929
|
+
} catch {}
|
|
32922
32930
|
}
|
|
32923
32931
|
}
|
|
32924
32932
|
if (isAbsolute8(file3)) {
|
|
@@ -32963,11 +32971,11 @@ function hasInspectionTrail(raw) {
|
|
|
32963
32971
|
return Array.isArray(files) && files.some((f) => typeof f === "string" && f.trim().length > 0);
|
|
32964
32972
|
}
|
|
32965
32973
|
async function substantiateAdversarialFindings(opts) {
|
|
32966
|
-
const { findings, workdir, storyId, blockingThreshold } = opts;
|
|
32974
|
+
const { findings, workdir, storyId, blockingThreshold, repoRoot } = opts;
|
|
32967
32975
|
return Promise.all(findings.map(async (finding) => {
|
|
32968
32976
|
if (!isBlockingSeverity(finding.severity, blockingThreshold))
|
|
32969
32977
|
return finding;
|
|
32970
|
-
const evidence = await checkFindingEvidence({ finding, workdir });
|
|
32978
|
+
const evidence = await checkFindingEvidence({ finding, workdir, repoRoot });
|
|
32971
32979
|
if (evidence.status !== "unmatched" && evidence.status !== "missing-observed")
|
|
32972
32980
|
return finding;
|
|
32973
32981
|
return downgradeUnsubstantiatedFinding({
|
|
@@ -33072,7 +33080,11 @@ async function requoteBlockingAdversarialFindings(findings, ctx) {
|
|
|
33072
33080
|
for (const [index, finding] of next.entries()) {
|
|
33073
33081
|
if (!isBlockingSeverity(finding.severity, threshold))
|
|
33074
33082
|
continue;
|
|
33075
|
-
const initialEvidence = await checkFindingEvidence({
|
|
33083
|
+
const initialEvidence = await checkFindingEvidence({
|
|
33084
|
+
finding,
|
|
33085
|
+
workdir: ctx.input.workdir,
|
|
33086
|
+
repoRoot: ctx.input.repoRoot
|
|
33087
|
+
});
|
|
33076
33088
|
if (initialEvidence.status !== "unmatched")
|
|
33077
33089
|
continue;
|
|
33078
33090
|
if (used >= maxRequotes)
|
|
@@ -33101,7 +33113,8 @@ async function requoteBlockingAdversarialFindings(findings, ctx) {
|
|
|
33101
33113
|
};
|
|
33102
33114
|
const requotedEvidence = await checkFindingEvidence({
|
|
33103
33115
|
finding: updatedFinding,
|
|
33104
|
-
workdir: ctx.input.workdir
|
|
33116
|
+
workdir: ctx.input.workdir,
|
|
33117
|
+
repoRoot: ctx.input.repoRoot
|
|
33105
33118
|
});
|
|
33106
33119
|
if (requotedEvidence.status === "matched") {
|
|
33107
33120
|
getSafeLogger()?.info("review", "Recovered adversarial finding via same-session requote", {
|
|
@@ -33329,6 +33342,7 @@ var init_adversarial_review = __esm(() => {
|
|
|
33329
33342
|
const substantiated = await substantiateAdversarialFindings({
|
|
33330
33343
|
findings,
|
|
33331
33344
|
workdir: input.workdir,
|
|
33345
|
+
repoRoot: input.repoRoot,
|
|
33332
33346
|
storyId: input.story.id,
|
|
33333
33347
|
blockingThreshold: threshold
|
|
33334
33348
|
});
|
|
@@ -34026,6 +34040,7 @@ async function runAdversarialReview(opts) {
|
|
|
34026
34040
|
try {
|
|
34027
34041
|
opResult = await _adversarialDeps.callOp(callCtx, adversarialReviewOp, {
|
|
34028
34042
|
workdir,
|
|
34043
|
+
repoRoot: projectDir ?? workdir,
|
|
34029
34044
|
story,
|
|
34030
34045
|
adversarialConfig,
|
|
34031
34046
|
mode: diffMode,
|
|
@@ -35548,6 +35563,10 @@ var init_decompose2 = __esm(() => {
|
|
|
35548
35563
|
});
|
|
35549
35564
|
|
|
35550
35565
|
// src/routing/classify.ts
|
|
35566
|
+
function isSecurityCriticalStory(title, tags = []) {
|
|
35567
|
+
const text = [title, ...tags ?? []].join(" ").toLowerCase();
|
|
35568
|
+
return SECURITY_KEYWORDS.some((kw) => text.includes(kw)) || PUBLIC_API_KEYWORDS.some((kw) => text.includes(kw));
|
|
35569
|
+
}
|
|
35551
35570
|
function classifyComplexity(title, _description, acceptanceCriteria, tags = []) {
|
|
35552
35571
|
const text = [title, ...acceptanceCriteria ?? [], ...tags ?? []].join(" ").toLowerCase();
|
|
35553
35572
|
if (EXPERT_KEYWORDS.some((kw) => text.includes(kw)))
|
|
@@ -35565,10 +35584,7 @@ function determineTestStrategy(complexity, title, _description, tags = [], tddSt
|
|
|
35565
35584
|
return "tdd-simple";
|
|
35566
35585
|
if (tddStrategy === "off")
|
|
35567
35586
|
return "test-after";
|
|
35568
|
-
|
|
35569
|
-
const isSecurityCritical = SECURITY_KEYWORDS.some((kw) => text.includes(kw));
|
|
35570
|
-
const isPublicApi = PUBLIC_API_KEYWORDS.some((kw) => text.includes(kw));
|
|
35571
|
-
if (isSecurityCritical || isPublicApi)
|
|
35587
|
+
if (isSecurityCriticalStory(title, tags))
|
|
35572
35588
|
return "three-session-tdd";
|
|
35573
35589
|
if (complexity === "expert")
|
|
35574
35590
|
return "three-session-tdd";
|
|
@@ -35956,6 +35972,7 @@ __export(exports_routing, {
|
|
|
35956
35972
|
routeTask: () => routeTask,
|
|
35957
35973
|
routeStory: () => routeStory,
|
|
35958
35974
|
resolveRouting: () => resolveRouting,
|
|
35975
|
+
isSecurityCriticalStory: () => isSecurityCriticalStory,
|
|
35959
35976
|
determineTestStrategy: () => determineTestStrategy,
|
|
35960
35977
|
complexityToModelTier: () => complexityToModelTier,
|
|
35961
35978
|
clearCache: () => clearCache,
|
|
@@ -36801,7 +36818,11 @@ async function requoteBlockingFindings(findings, ctx) {
|
|
|
36801
36818
|
for (const [index, finding] of next.entries()) {
|
|
36802
36819
|
if (!isBlockingSeverity(finding.severity, threshold))
|
|
36803
36820
|
continue;
|
|
36804
|
-
const initialEvidence = await checkFindingEvidence({
|
|
36821
|
+
const initialEvidence = await checkFindingEvidence({
|
|
36822
|
+
finding,
|
|
36823
|
+
workdir: ctx.input.workdir,
|
|
36824
|
+
repoRoot: ctx.input.repoRoot
|
|
36825
|
+
});
|
|
36805
36826
|
if (initialEvidence.status !== "unmatched")
|
|
36806
36827
|
continue;
|
|
36807
36828
|
if (used >= maxRequotes)
|
|
@@ -36831,7 +36852,8 @@ async function requoteBlockingFindings(findings, ctx) {
|
|
|
36831
36852
|
};
|
|
36832
36853
|
const requotedEvidence = await checkFindingEvidence({
|
|
36833
36854
|
finding: updatedFinding,
|
|
36834
|
-
workdir: ctx.input.workdir
|
|
36855
|
+
workdir: ctx.input.workdir,
|
|
36856
|
+
repoRoot: ctx.input.repoRoot
|
|
36835
36857
|
});
|
|
36836
36858
|
if (requotedEvidence.status === "matched") {
|
|
36837
36859
|
getSafeLogger()?.info("review", "Recovered semantic finding via same-session requote", {
|
|
@@ -36967,7 +36989,7 @@ var init_semantic_review = __esm(() => {
|
|
|
36967
36989
|
const threshold = input.blockingThreshold ?? "error";
|
|
36968
36990
|
const findings = parsed.findings;
|
|
36969
36991
|
const sanitized = sanitizeRefModeFindings(findings, input.mode, threshold);
|
|
36970
|
-
const substantiated = await substantiateSemanticEvidence(sanitized, input.mode, input.workdir, input.story.id, threshold);
|
|
36992
|
+
const substantiated = await substantiateSemanticEvidence(sanitized, input.mode, input.workdir, input.story.id, threshold, input.repoRoot);
|
|
36971
36993
|
const { accepted, dropped } = filterByAcGroundingMinimal(substantiated, input.story.acceptanceCriteria);
|
|
36972
36994
|
const blocking = accepted.filter((f) => isBlockingSeverity(f.severity, threshold));
|
|
36973
36995
|
const passed = parsed.passed && blocking.length === 0;
|
|
@@ -38975,8 +38997,13 @@ var init_greenfield_gate = __esm(() => {
|
|
|
38975
38997
|
config: greenfieldGateConfigSelector,
|
|
38976
38998
|
async execute(input, _ctx) {
|
|
38977
38999
|
const globs = input.resolvedTestPatterns.globs;
|
|
38978
|
-
|
|
38979
|
-
|
|
39000
|
+
let hasTests;
|
|
39001
|
+
try {
|
|
39002
|
+
hasTests = await hasTestFilesOnDisk(input.workdir, globs);
|
|
39003
|
+
} catch {
|
|
39004
|
+
return { success: true, hasPreExistingTests: true };
|
|
39005
|
+
}
|
|
39006
|
+
if (!hasTests) {
|
|
38980
39007
|
return { success: false, hasPreExistingTests: false, pauseReason: "greenfield-no-tests" };
|
|
38981
39008
|
}
|
|
38982
39009
|
return { success: true, hasPreExistingTests: true };
|
|
@@ -38984,6 +39011,33 @@ var init_greenfield_gate = __esm(() => {
|
|
|
38984
39011
|
};
|
|
38985
39012
|
});
|
|
38986
39013
|
|
|
39014
|
+
// src/operations/test-presence-gate.ts
|
|
39015
|
+
var testPresenceGateConfigSelector, testPresenceGateOp;
|
|
39016
|
+
var init_test_presence_gate = __esm(() => {
|
|
39017
|
+
init_config();
|
|
39018
|
+
init_greenfield();
|
|
39019
|
+
testPresenceGateConfigSelector = pickSelector("test-presence-gate", "execution");
|
|
39020
|
+
testPresenceGateOp = {
|
|
39021
|
+
kind: "deterministic",
|
|
39022
|
+
name: "test-presence-gate",
|
|
39023
|
+
stage: "verify",
|
|
39024
|
+
config: testPresenceGateConfigSelector,
|
|
39025
|
+
async execute(input, _ctx) {
|
|
39026
|
+
const globs = input.resolvedTestPatterns.globs;
|
|
39027
|
+
let hasTests;
|
|
39028
|
+
try {
|
|
39029
|
+
hasTests = await hasTestFilesOnDisk(input.workdir, globs);
|
|
39030
|
+
} catch {
|
|
39031
|
+
return { success: true, hasTests: true };
|
|
39032
|
+
}
|
|
39033
|
+
if (!hasTests) {
|
|
39034
|
+
return { success: false, hasTests: false, pauseReason: "no-tests-authored" };
|
|
39035
|
+
}
|
|
39036
|
+
return { success: true, hasTests: true };
|
|
39037
|
+
}
|
|
39038
|
+
};
|
|
39039
|
+
});
|
|
39040
|
+
|
|
38987
39041
|
// src/utils/command-argv.ts
|
|
38988
39042
|
function parseCommandToArgv(command) {
|
|
38989
39043
|
const safeEnv = buildAllowedEnv();
|
|
@@ -40667,6 +40721,7 @@ var init_operations = __esm(() => {
|
|
|
40667
40721
|
init_plan_critic_llm();
|
|
40668
40722
|
init_execution_gates();
|
|
40669
40723
|
init_greenfield_gate();
|
|
40724
|
+
init_test_presence_gate();
|
|
40670
40725
|
init_full_suite_gate();
|
|
40671
40726
|
init_full_suite_rectify();
|
|
40672
40727
|
init_full_suite_rectify_op();
|
|
@@ -42048,6 +42103,7 @@ async function runSemanticReview(opts) {
|
|
|
42048
42103
|
try {
|
|
42049
42104
|
opResult = await _semanticDeps.callOp(callCtx, semanticReviewOp, {
|
|
42050
42105
|
workdir,
|
|
42106
|
+
repoRoot: projectDir ?? workdir,
|
|
42051
42107
|
story,
|
|
42052
42108
|
semanticConfig,
|
|
42053
42109
|
mode: diffMode,
|
|
@@ -42602,6 +42658,7 @@ var init_runner2 = __esm(() => {
|
|
|
42602
42658
|
var init_review = __esm(() => {
|
|
42603
42659
|
init_semantic_helpers();
|
|
42604
42660
|
init_category_fix_target();
|
|
42661
|
+
init_finding_filters();
|
|
42605
42662
|
init_ac_quote_validator();
|
|
42606
42663
|
init_ac_structural_counterfactual();
|
|
42607
42664
|
init_adversarial();
|
|
@@ -42629,6 +42686,13 @@ UNRESOLVED: <brief explanation of which findings conflicted and why they cannot
|
|
|
42629
42686
|
|
|
42630
42687
|
Before emitting UNRESOLVED, confirm none of Exceptions 1\u2013${count} apply.
|
|
42631
42688
|
|
|
42689
|
+
**A missing-test or \`test-gap\` finding is never a false positive because a \`.nax/\` file exists.**
|
|
42690
|
+
\`.nax/\` is nax's own artifact directory; \`.nax-acceptance.test.ts\` is generated scaffolding for the
|
|
42691
|
+
acceptance gate \u2014 it is NOT source-tree test coverage. You may NOT cite any \`.nax/\`-resident file as
|
|
42692
|
+
evidence that an acceptance criterion is already tested, and you may NOT emit UNRESOLVED on that basis.
|
|
42693
|
+
The only valid response to a missing-test finding is to
|
|
42694
|
+
author a real test under the package's resolved test path.
|
|
42695
|
+
|
|
42632
42696
|
## Test-file edit exceptions
|
|
42633
42697
|
|
|
42634
42698
|
The "do not modify test files" rule has ${countWord} narrow escape valves. Each requires a
|
|
@@ -53992,7 +54056,7 @@ ${stderr}`;
|
|
|
53992
54056
|
errorExitCode = exitCode;
|
|
53993
54057
|
allFailedACs.push("AC-ERROR");
|
|
53994
54058
|
allFindings.push(acSentinelToFinding("AC-ERROR", output));
|
|
53995
|
-
failedPackages.push({ testPath, packageDir, testFramework, commandOverride });
|
|
54059
|
+
failedPackages.push({ testPath, packageDir, testFramework, commandOverride, output, failedACs: ["AC-ERROR"] });
|
|
53996
54060
|
continue;
|
|
53997
54061
|
}
|
|
53998
54062
|
for (const acId of actualFailures) {
|
|
@@ -54002,7 +54066,14 @@ ${stderr}`;
|
|
|
54002
54066
|
}
|
|
54003
54067
|
}
|
|
54004
54068
|
if (actualFailures.length > 0) {
|
|
54005
|
-
failedPackages.push({
|
|
54069
|
+
failedPackages.push({
|
|
54070
|
+
testPath,
|
|
54071
|
+
packageDir,
|
|
54072
|
+
testFramework,
|
|
54073
|
+
commandOverride,
|
|
54074
|
+
output,
|
|
54075
|
+
failedACs: actualFailures
|
|
54076
|
+
});
|
|
54006
54077
|
logger.error("acceptance", "Acceptance tests failed", {
|
|
54007
54078
|
storyId: ctx.story.id,
|
|
54008
54079
|
failedACs: actualFailures,
|
|
@@ -55441,6 +55512,7 @@ var init_types9 = __esm(() => {
|
|
|
55441
55512
|
"test-writer",
|
|
55442
55513
|
"greenfield-gate",
|
|
55443
55514
|
"implementer",
|
|
55515
|
+
"test-presence-gate",
|
|
55444
55516
|
"full-suite-gate",
|
|
55445
55517
|
"verifier",
|
|
55446
55518
|
"verify-scoped",
|
|
@@ -55453,6 +55525,7 @@ var init_types9 = __esm(() => {
|
|
|
55453
55525
|
"test-writer": "testWriter",
|
|
55454
55526
|
"greenfield-gate": "greenfieldGate",
|
|
55455
55527
|
implementer: "implementer",
|
|
55528
|
+
"test-presence-gate": "testPresenceGate",
|
|
55456
55529
|
"full-suite-gate": "fullSuiteGate",
|
|
55457
55530
|
verifier: "verifier",
|
|
55458
55531
|
"verify-scoped": "verifyScoped",
|
|
@@ -55595,6 +55668,8 @@ function collectOrderedPhases(state) {
|
|
|
55595
55668
|
return [state.greenfieldGate];
|
|
55596
55669
|
if (kind === "implementer" && state.implementer)
|
|
55597
55670
|
return [state.implementer];
|
|
55671
|
+
if (kind === "test-presence-gate" && state.testPresenceGate)
|
|
55672
|
+
return [state.testPresenceGate];
|
|
55598
55673
|
if (kind === "full-suite-gate" && state.fullSuiteGate)
|
|
55599
55674
|
return [state.fullSuiteGate];
|
|
55600
55675
|
if (kind === "verifier" && state.verifier)
|
|
@@ -56219,10 +56294,11 @@ class ExecutionPlan {
|
|
|
56219
56294
|
}
|
|
56220
56295
|
}
|
|
56221
56296
|
}
|
|
56297
|
+
const storyCurrentlyGreen = !rectResult.rectificationExhausted && Object.entries(phaseOutputs).every(([name, output]) => phasePassed(name, output, this.ctx.storyId));
|
|
56222
56298
|
const advCfg = this.state.adversarialReview ? this.state.nonBlockingFix : undefined;
|
|
56223
56299
|
const advisoryOut = phaseOutputs["adversarial-review"];
|
|
56224
56300
|
const advisoryFindings = advisoryOut?.advisoryFindings ?? [];
|
|
56225
|
-
if (advCfg && this.state.rectification && this.ctx.storyId && shouldRunNonBlockingFix(advCfg, advisoryFindings.length)) {
|
|
56301
|
+
if (advCfg && storyCurrentlyGreen && this.state.rectification && this.ctx.storyId && shouldRunNonBlockingFix(advCfg, advisoryFindings.length)) {
|
|
56226
56302
|
await _storyOrchestratorDeps.runNonBlockingFix({
|
|
56227
56303
|
workdir: this.ctx.packageDir,
|
|
56228
56304
|
storyId: this.ctx.storyId,
|
|
@@ -56333,6 +56409,10 @@ class StoryOrchestratorBuilder {
|
|
|
56333
56409
|
setPhase(this.state, "greenfield-gate", isSlot(value) ? value : { op: greenfieldGateOp, input: value });
|
|
56334
56410
|
return this;
|
|
56335
56411
|
}
|
|
56412
|
+
addTestPresenceGate(value) {
|
|
56413
|
+
setPhase(this.state, "test-presence-gate", isSlot(value) ? value : { op: testPresenceGateOp, input: value });
|
|
56414
|
+
return this;
|
|
56415
|
+
}
|
|
56336
56416
|
addVerifier(value) {
|
|
56337
56417
|
setPhase(this.state, "verifier", isSlot(value) ? value : { op: verifierOp, input: value });
|
|
56338
56418
|
return this;
|
|
@@ -56418,6 +56498,9 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
|
|
|
56418
56498
|
if (inputs.implementer) {
|
|
56419
56499
|
builder.addImplementer(inputs.implementer);
|
|
56420
56500
|
}
|
|
56501
|
+
if (!isThreeSession && inputs.testPresenceGate) {
|
|
56502
|
+
builder.addTestPresenceGate(inputs.testPresenceGate);
|
|
56503
|
+
}
|
|
56421
56504
|
const regressionMode = config2.execution?.regressionGate?.mode ?? "deferred";
|
|
56422
56505
|
if (inputs.fullSuiteGate && (isThreeSession || regressionMode === "per-story")) {
|
|
56423
56506
|
builder.addFullSuiteGate(inputs.fullSuiteGate);
|
|
@@ -56491,7 +56574,12 @@ async function buildPlanForStrategy(ctx, story, config2, testStrategy, inputs) {
|
|
|
56491
56574
|
const nbStrategies = [];
|
|
56492
56575
|
if (nbf?.enabled && inputs.adversarialReview) {
|
|
56493
56576
|
const nbSink = makeDeclarationSink();
|
|
56494
|
-
if (
|
|
56577
|
+
if (!isThreeSession) {
|
|
56578
|
+
nbStrategies.push(makeAutofixImplementerStrategy(story, config2, nbSink, {
|
|
56579
|
+
includeAdversarialReview: true,
|
|
56580
|
+
promptSeverityFloor: "info"
|
|
56581
|
+
}));
|
|
56582
|
+
} else if (nbf.scope === "source") {
|
|
56495
56583
|
nbStrategies.push(makeAutofixImplementerStrategy(story, config2, nbSink, {
|
|
56496
56584
|
includeAdversarialReview: true,
|
|
56497
56585
|
promptSeverityFloor: "info"
|
|
@@ -56637,6 +56725,7 @@ async function assemblePlanInputsFromCtx(ctx) {
|
|
|
56637
56725
|
featureContextMarkdown: ctx.featureContextMarkdown,
|
|
56638
56726
|
constitution: ctx.constitution?.content
|
|
56639
56727
|
};
|
|
56728
|
+
const testPresenceGateInput = isSingleSessionTestOwningStrategy(ctx.routing.testStrategy) && resolvedTestPatterns ? { story, workdir: ctx.workdir, resolvedTestPatterns } : undefined;
|
|
56640
56729
|
const _regressionMode = ctx.config.execution?.regressionGate?.mode;
|
|
56641
56730
|
const fullSuiteGateInput = _isTdd || _regressionMode === "per-story" ? {
|
|
56642
56731
|
story,
|
|
@@ -56741,6 +56830,7 @@ async function assemblePlanInputsFromCtx(ctx) {
|
|
|
56741
56830
|
testWriter: testWriterInput,
|
|
56742
56831
|
greenfieldGate: greenfieldGateInput,
|
|
56743
56832
|
implementer: implementerInput,
|
|
56833
|
+
testPresenceGate: testPresenceGateInput,
|
|
56744
56834
|
fullSuiteGate: fullSuiteGateInput,
|
|
56745
56835
|
verifier: verifierInput,
|
|
56746
56836
|
verifyScoped: verifyScopedInput,
|
|
@@ -56798,6 +56888,11 @@ function routeTddFailure(failureCategory, isLiteMode, ctx, reviewReason, failure
|
|
|
56798
56888
|
case "review-incomplete":
|
|
56799
56889
|
case "greenfield-no-tests":
|
|
56800
56890
|
return { action: "escalate", reason: buildReason(failureCategory) };
|
|
56891
|
+
case "no-tests-authored":
|
|
56892
|
+
return {
|
|
56893
|
+
action: "escalate",
|
|
56894
|
+
reason: "No test files were authored for this story. You MUST write tests covering every acceptance criterion under the package's resolved test path before implementation is considered complete."
|
|
56895
|
+
};
|
|
56801
56896
|
case "dependency-prep":
|
|
56802
56897
|
return pauseFallback;
|
|
56803
56898
|
default:
|
|
@@ -56902,6 +56997,10 @@ function deriveTddFailureCategory(phaseOutputs, unfixedFindings, gateRegressedDu
|
|
|
56902
56997
|
if (greenfieldOutput?.success === false && greenfieldOutput?.pauseReason === "greenfield-no-tests") {
|
|
56903
56998
|
return "greenfield-no-tests";
|
|
56904
56999
|
}
|
|
57000
|
+
const testPresenceOutput = phaseOutputs[testPresenceGateOp.name];
|
|
57001
|
+
if (testPresenceOutput?.success === false && testPresenceOutput?.pauseReason === "no-tests-authored") {
|
|
57002
|
+
return "no-tests-authored";
|
|
57003
|
+
}
|
|
56905
57004
|
const verifierOutput = phaseOutputs[verifierOp.name];
|
|
56906
57005
|
if (verifierOutput?.success === false) {
|
|
56907
57006
|
if (verifierOutput.failureCategory) {
|
|
@@ -57941,13 +58040,21 @@ var init_routing2 = __esm(() => {
|
|
|
57941
58040
|
});
|
|
57942
58041
|
const isGreenfield = await _routingDeps.isGreenfieldStory(ctx.story, greenfieldScanDir, resolved?.globs);
|
|
57943
58042
|
if (isGreenfield) {
|
|
57944
|
-
|
|
57945
|
-
|
|
57946
|
-
|
|
57947
|
-
|
|
57948
|
-
|
|
57949
|
-
|
|
57950
|
-
|
|
58043
|
+
if (isSecurityCriticalStory(ctx.story.title, ctx.story.tags)) {
|
|
58044
|
+
logger.info("routing", "Greenfield + security-critical \u2014 keeping three-session strategy", {
|
|
58045
|
+
storyId: ctx.story.id,
|
|
58046
|
+
strategy: routing.testStrategy,
|
|
58047
|
+
scanDir: greenfieldScanDir
|
|
58048
|
+
});
|
|
58049
|
+
} else {
|
|
58050
|
+
logger.info("routing", "Greenfield detected \u2014 forcing tdd-simple strategy", {
|
|
58051
|
+
storyId: ctx.story.id,
|
|
58052
|
+
originalStrategy: routing.testStrategy,
|
|
58053
|
+
scanDir: greenfieldScanDir
|
|
58054
|
+
});
|
|
58055
|
+
routing.testStrategy = "tdd-simple";
|
|
58056
|
+
routing.reasoning = `${routing.reasoning} [GREENFIELD OVERRIDE: No test files exist, using tdd-simple (test-first, single-session) instead of three-session TDD]`;
|
|
58057
|
+
}
|
|
57951
58058
|
}
|
|
57952
58059
|
}
|
|
57953
58060
|
ctx.routing = routing;
|
|
@@ -60861,7 +60968,7 @@ var init_loader4 = __esm(() => {
|
|
|
60861
60968
|
});
|
|
60862
60969
|
|
|
60863
60970
|
// src/hooks/runner.ts
|
|
60864
|
-
import { join as
|
|
60971
|
+
import { join as join78 } from "path";
|
|
60865
60972
|
function createDrainDeadline2(deadlineMs) {
|
|
60866
60973
|
let timeoutId;
|
|
60867
60974
|
const promise2 = new Promise((resolve16) => {
|
|
@@ -60880,14 +60987,14 @@ async function loadHooksConfig(projectDir, globalDir) {
|
|
|
60880
60987
|
let globalHooks = { hooks: {} };
|
|
60881
60988
|
let projectHooks = { hooks: {} };
|
|
60882
60989
|
let skipGlobal = false;
|
|
60883
|
-
const projectPath =
|
|
60990
|
+
const projectPath = join78(projectDir, "hooks.json");
|
|
60884
60991
|
const projectData = await loadJsonFile(projectPath, "hooks");
|
|
60885
60992
|
if (projectData) {
|
|
60886
60993
|
projectHooks = projectData;
|
|
60887
60994
|
skipGlobal = projectData.skipGlobal ?? false;
|
|
60888
60995
|
}
|
|
60889
60996
|
if (!skipGlobal && globalDir) {
|
|
60890
|
-
const globalPath =
|
|
60997
|
+
const globalPath = join78(globalDir, "hooks.json");
|
|
60891
60998
|
const globalData = await loadJsonFile(globalPath, "hooks");
|
|
60892
60999
|
if (globalData) {
|
|
60893
61000
|
globalHooks = globalData;
|
|
@@ -61057,7 +61164,7 @@ var package_default;
|
|
|
61057
61164
|
var init_package = __esm(() => {
|
|
61058
61165
|
package_default = {
|
|
61059
61166
|
name: "@nathapp/nax",
|
|
61060
|
-
version: "0.70.
|
|
61167
|
+
version: "0.70.7",
|
|
61061
61168
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
61062
61169
|
type: "module",
|
|
61063
61170
|
bin: {
|
|
@@ -61157,8 +61264,8 @@ var init_version = __esm(() => {
|
|
|
61157
61264
|
NAX_VERSION = package_default.version;
|
|
61158
61265
|
NAX_COMMIT = (() => {
|
|
61159
61266
|
try {
|
|
61160
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
61161
|
-
return "
|
|
61267
|
+
if (/^[0-9a-f]{6,10}$/.test("20a6d5ac"))
|
|
61268
|
+
return "20a6d5ac";
|
|
61162
61269
|
} catch {}
|
|
61163
61270
|
try {
|
|
61164
61271
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -61756,12 +61863,12 @@ __export(exports_acceptance_loop, {
|
|
|
61756
61863
|
isTestLevelFailure: () => isTestLevelFailure,
|
|
61757
61864
|
isStubTestFile: () => isStubTestFile,
|
|
61758
61865
|
buildResult: () => buildResult,
|
|
61866
|
+
_runAcceptanceTestsOnceDeps: () => _runAcceptanceTestsOnceDeps,
|
|
61759
61867
|
_regenerateDeps: () => _regenerateDeps,
|
|
61760
61868
|
_acceptanceLoopDeps: () => _acceptanceLoopDeps,
|
|
61761
61869
|
_acceptanceFixCycleDeps: () => _acceptanceFixCycleDeps
|
|
61762
61870
|
});
|
|
61763
|
-
function resolveAcceptanceFixTarget(acceptanceTestPaths,
|
|
61764
|
-
const failedPackage = failedPackages?.[0];
|
|
61871
|
+
function resolveAcceptanceFixTarget(acceptanceTestPaths, failedPackage, config2) {
|
|
61765
61872
|
const matchedEntry = failedPackage ? acceptanceTestPaths?.find((entry) => entry.testPath === failedPackage.testPath || entry.packageDir === failedPackage.packageDir) : undefined;
|
|
61766
61873
|
const selectedPathEntry = matchedEntry ?? acceptanceTestPaths?.[0];
|
|
61767
61874
|
return {
|
|
@@ -61792,11 +61899,11 @@ function findingsForDiagnosis(failedACs, testOutput, diagnosis) {
|
|
|
61792
61899
|
{ ...f, fixTarget: "test" }
|
|
61793
61900
|
]);
|
|
61794
61901
|
}
|
|
61795
|
-
function buildFixCycleCtx(ctx, runtime, storyId) {
|
|
61902
|
+
function buildFixCycleCtx(ctx, runtime, storyId, packageDir) {
|
|
61796
61903
|
return {
|
|
61797
61904
|
runtime,
|
|
61798
|
-
packageView: runtime.packages.resolve(
|
|
61799
|
-
packageDir
|
|
61905
|
+
packageView: runtime.packages.resolve(packageDir),
|
|
61906
|
+
packageDir,
|
|
61800
61907
|
storyId,
|
|
61801
61908
|
featureName: ctx.feature,
|
|
61802
61909
|
agentName: ctx.agentManager?.getDefault() ?? "claude"
|
|
@@ -61830,9 +61937,10 @@ function buildAcceptanceContext(ctx, prd) {
|
|
|
61830
61937
|
abortSignal: ctx.abortSignal
|
|
61831
61938
|
};
|
|
61832
61939
|
}
|
|
61833
|
-
async function runAcceptanceTestsOnce(ctx, prd) {
|
|
61834
|
-
const
|
|
61835
|
-
const
|
|
61940
|
+
async function runAcceptanceTestsOnce(ctx, prd, packageFilter) {
|
|
61941
|
+
const baseCtx = packageFilter ? { ...ctx, acceptanceTestPaths: packageFilter } : ctx;
|
|
61942
|
+
const acceptanceContext = buildAcceptanceContext(baseCtx, prd);
|
|
61943
|
+
const { acceptanceStage: acceptanceStage2 } = await _runAcceptanceTestsOnceDeps.importAcceptanceStage();
|
|
61836
61944
|
const result = await acceptanceStage2.execute(acceptanceContext);
|
|
61837
61945
|
if (result.action !== "fail")
|
|
61838
61946
|
return { passed: true, failedACs: [], testOutput: "" };
|
|
@@ -61846,7 +61954,7 @@ async function runAcceptanceTestsOnce(ctx, prd) {
|
|
|
61846
61954
|
failedPackages: failures.failedPackages
|
|
61847
61955
|
};
|
|
61848
61956
|
}
|
|
61849
|
-
async function runAcceptanceFixCycle(ctx, prd, initialFailures, diagnosis, acceptanceTestPath, testCommand) {
|
|
61957
|
+
async function runAcceptanceFixCycle(ctx, prd, initialFailures, diagnosis, acceptanceTestPath, testCommand, fixTarget) {
|
|
61850
61958
|
const runtime = ctx.runtime;
|
|
61851
61959
|
if (!runtime) {
|
|
61852
61960
|
return { iterations: [], finalFindings: [], exitReason: "no-strategy" };
|
|
@@ -61854,7 +61962,7 @@ async function runAcceptanceFixCycle(ctx, prd, initialFailures, diagnosis, accep
|
|
|
61854
61962
|
let currentTestOutput = initialFailures.testOutput;
|
|
61855
61963
|
let currentFailedACs = initialFailures.failedACs;
|
|
61856
61964
|
const storyId = prd.userStories[0]?.id ?? "unknown";
|
|
61857
|
-
const cycleCtx = buildFixCycleCtx(ctx, runtime, storyId);
|
|
61965
|
+
const cycleCtx = buildFixCycleCtx(ctx, runtime, storyId, fixTarget?.packageDir ?? ctx.workdir);
|
|
61858
61966
|
const cycle = {
|
|
61859
61967
|
findings: findingsForDiagnosis(initialFailures.failedACs, initialFailures.testOutput, diagnosis),
|
|
61860
61968
|
iterations: [],
|
|
@@ -61892,7 +62000,8 @@ async function runAcceptanceFixCycle(ctx, prd, initialFailures, diagnosis, accep
|
|
|
61892
62000
|
}
|
|
61893
62001
|
],
|
|
61894
62002
|
validate: async (_ctx, _opts) => {
|
|
61895
|
-
const
|
|
62003
|
+
const packageFilter = fixTarget ? ctx.acceptanceTestPaths?.filter((entry) => entry.packageDir === fixTarget.packageDir) : undefined;
|
|
62004
|
+
const result = await runAcceptanceTestsOnce(ctx, prd, packageFilter);
|
|
61896
62005
|
if (result.passed)
|
|
61897
62006
|
return [];
|
|
61898
62007
|
currentTestOutput = result.testOutput;
|
|
@@ -61918,7 +62027,7 @@ async function runAcceptanceLoop(ctx) {
|
|
|
61918
62027
|
const storiesCompleted = ctx.storiesCompleted;
|
|
61919
62028
|
const prdDirty = false;
|
|
61920
62029
|
logger?.info("acceptance", "All stories complete, running acceptance validation");
|
|
61921
|
-
const { acceptanceStage: acceptanceStage2 } = await
|
|
62030
|
+
const { acceptanceStage: acceptanceStage2 } = await _runAcceptanceTestsOnceDeps.importAcceptanceStage();
|
|
61922
62031
|
while (acceptanceRetries < maxRetries) {
|
|
61923
62032
|
const firstStory = prd.userStories[0];
|
|
61924
62033
|
const acceptanceContext = buildAcceptanceContext(ctx, prd);
|
|
@@ -61981,40 +62090,55 @@ async function runAcceptanceLoop(ctx) {
|
|
|
61981
62090
|
logger?.error("acceptance", "Runtime not found for diagnosis", { storyId: firstStory?.id });
|
|
61982
62091
|
return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty, failures.failedACs, acceptanceRetries);
|
|
61983
62092
|
}
|
|
61984
|
-
const {
|
|
61985
|
-
const testEntries = ctx.acceptanceTestPaths ? await loadAcceptanceTestContent(ctx.acceptanceTestPaths.map((p) => p.testPath)) : [];
|
|
61986
|
-
const effectiveAcceptanceTestPath = acceptanceTestPath || testEntries[0]?.testPath || "";
|
|
61987
|
-
const selectedTestEntry = testEntries.find((entry) => entry.testPath === effectiveAcceptanceTestPath);
|
|
61988
|
-
const testFileContent = selectedTestEntry?.content ?? testEntries[0]?.content ?? "";
|
|
62093
|
+
const failedPkgs = failures.failedPackages && failures.failedPackages.length > 0 ? failures.failedPackages : [{ testPath: "", packageDir: ctx.workdir, output: failures.testOutput, failedACs: failures.failedACs }];
|
|
61989
62094
|
const strategy = ctx.config.acceptance.fix?.strategy ?? "diagnose-first";
|
|
61990
|
-
const
|
|
61991
|
-
|
|
61992
|
-
|
|
61993
|
-
|
|
61994
|
-
|
|
61995
|
-
|
|
61996
|
-
|
|
61997
|
-
|
|
61998
|
-
|
|
61999
|
-
|
|
62000
|
-
|
|
62001
|
-
|
|
62002
|
-
|
|
62003
|
-
|
|
62004
|
-
|
|
62005
|
-
|
|
62006
|
-
|
|
62007
|
-
|
|
62008
|
-
|
|
62009
|
-
|
|
62010
|
-
|
|
62011
|
-
|
|
62012
|
-
|
|
62013
|
-
|
|
62095
|
+
const testEntries = ctx.acceptanceTestPaths ? await _acceptanceLoopDeps.loadAcceptanceTestContent(ctx.acceptanceTestPaths.map((p) => p.testPath)) : [];
|
|
62096
|
+
const remainingFindings = [];
|
|
62097
|
+
let totalInternalIterations = 0;
|
|
62098
|
+
for (const pkg of failedPkgs) {
|
|
62099
|
+
const { acceptanceTestPath, testCommand } = resolveAcceptanceFixTarget(ctx.acceptanceTestPaths, pkg, ctx.config);
|
|
62100
|
+
const effectivePath = acceptanceTestPath || pkg.testPath || testEntries[0]?.testPath || "";
|
|
62101
|
+
const testFileContent = testEntries.find((entry) => entry.testPath === effectivePath)?.content ?? "";
|
|
62102
|
+
const pkgFailures = { failedACs: pkg.failedACs, testOutput: pkg.output };
|
|
62103
|
+
const diagnosis = await resolveAcceptanceDiagnosis({
|
|
62104
|
+
ctx,
|
|
62105
|
+
failures: pkgFailures,
|
|
62106
|
+
totalACs,
|
|
62107
|
+
strategy,
|
|
62108
|
+
semanticVerdicts,
|
|
62109
|
+
diagnosisOpts: {
|
|
62110
|
+
testOutput: pkg.output,
|
|
62111
|
+
testFileContent,
|
|
62112
|
+
acceptanceTestPath: effectivePath,
|
|
62113
|
+
workdir: pkg.packageDir,
|
|
62114
|
+
storyId: firstStory?.id
|
|
62115
|
+
}
|
|
62116
|
+
});
|
|
62117
|
+
logger?.info("acceptance.diagnosis", "Diagnosis resolved", {
|
|
62118
|
+
storyId: firstStory?.id,
|
|
62119
|
+
packageDir: pkg.packageDir,
|
|
62120
|
+
verdict: diagnosis.verdict,
|
|
62121
|
+
confidence: diagnosis.confidence,
|
|
62122
|
+
attempt: acceptanceRetries
|
|
62123
|
+
});
|
|
62124
|
+
const cycleResult = await runAcceptanceFixCycle(ctx, prd, pkgFailures, diagnosis, effectivePath, testCommand, {
|
|
62125
|
+
packageDir: pkg.packageDir,
|
|
62126
|
+
testPath: effectivePath
|
|
62127
|
+
});
|
|
62128
|
+
totalCost += cycleResult.costUsd ?? 0;
|
|
62129
|
+
totalInternalIterations += cycleResult.iterations.length;
|
|
62130
|
+
const pkgResolved = cycleResult.exitReason === "resolved" || cycleResult.finalFindings.length === 0;
|
|
62131
|
+
if (!pkgResolved)
|
|
62132
|
+
remainingFindings.push(...cycleResult.finalFindings);
|
|
62133
|
+
}
|
|
62134
|
+
const finalCheck = await runAcceptanceTestsOnce(ctx, prd);
|
|
62135
|
+
const success2 = finalCheck.passed && remainingFindings.length === 0;
|
|
62136
|
+
const failureMessages = !success2 ? finalCheck.failedACs.length > 0 ? finalCheck.failedACs : remainingFindings.length > 0 ? remainingFindings.map((f) => f.message) : ["acceptance validation failed (unknown cause)"] : undefined;
|
|
62137
|
+
return buildResult(success2, prd, totalCost, iterations, storiesCompleted, prdDirty, failureMessages, acceptanceRetries + totalInternalIterations);
|
|
62014
62138
|
}
|
|
62015
62139
|
return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
|
|
62016
62140
|
}
|
|
62017
|
-
var _acceptanceLoopDeps, _acceptanceFixCycleDeps, MAX_STUB_REGENS = 2;
|
|
62141
|
+
var _acceptanceLoopDeps, _acceptanceFixCycleDeps, _runAcceptanceTestsOnceDeps, MAX_STUB_REGENS = 2;
|
|
62018
62142
|
var init_acceptance_loop = __esm(() => {
|
|
62019
62143
|
init_acceptance2();
|
|
62020
62144
|
init_findings();
|
|
@@ -62027,24 +62151,28 @@ var init_acceptance_loop = __esm(() => {
|
|
|
62027
62151
|
init_acceptance_helpers();
|
|
62028
62152
|
init_acceptance_helpers();
|
|
62029
62153
|
_acceptanceLoopDeps = {
|
|
62030
|
-
loadSemanticVerdicts
|
|
62154
|
+
loadSemanticVerdicts,
|
|
62155
|
+
loadAcceptanceTestContent
|
|
62031
62156
|
};
|
|
62032
62157
|
_acceptanceFixCycleDeps = {
|
|
62033
62158
|
runFixCycle
|
|
62034
62159
|
};
|
|
62160
|
+
_runAcceptanceTestsOnceDeps = {
|
|
62161
|
+
importAcceptanceStage: () => Promise.resolve().then(() => (init_acceptance3(), exports_acceptance2))
|
|
62162
|
+
};
|
|
62035
62163
|
});
|
|
62036
62164
|
|
|
62037
62165
|
// src/session/scratch-purge.ts
|
|
62038
62166
|
import { mkdir as mkdir12, rename, rm } from "fs/promises";
|
|
62039
|
-
import { dirname as dirname12, join as
|
|
62167
|
+
import { dirname as dirname12, join as join79 } from "path";
|
|
62040
62168
|
async function purgeStaleScratch(projectDir, featureName, retentionDays, archiveInsteadOfDelete = false) {
|
|
62041
|
-
const sessionsDir =
|
|
62169
|
+
const sessionsDir = join79(projectDir, ".nax", "features", featureName, "sessions");
|
|
62042
62170
|
const sessionIds = await _scratchPurgeDeps.listSessionDirs(sessionsDir);
|
|
62043
62171
|
const cutoffMs = _scratchPurgeDeps.now() - retentionDays * 86400000;
|
|
62044
62172
|
let purged = 0;
|
|
62045
62173
|
for (const sessionId of sessionIds) {
|
|
62046
|
-
const sessionDir =
|
|
62047
|
-
const descriptorPath =
|
|
62174
|
+
const sessionDir = join79(sessionsDir, sessionId);
|
|
62175
|
+
const descriptorPath = join79(sessionDir, "descriptor.json");
|
|
62048
62176
|
if (!await _scratchPurgeDeps.fileExists(descriptorPath))
|
|
62049
62177
|
continue;
|
|
62050
62178
|
let lastActivityAt;
|
|
@@ -62060,7 +62188,7 @@ async function purgeStaleScratch(projectDir, featureName, retentionDays, archive
|
|
|
62060
62188
|
if (new Date(lastActivityAt).getTime() >= cutoffMs)
|
|
62061
62189
|
continue;
|
|
62062
62190
|
if (archiveInsteadOfDelete) {
|
|
62063
|
-
const archiveDest =
|
|
62191
|
+
const archiveDest = join79(projectDir, ".nax", "features", featureName, "_archive", "sessions", sessionId);
|
|
62064
62192
|
await _scratchPurgeDeps.move(sessionDir, archiveDest);
|
|
62065
62193
|
} else {
|
|
62066
62194
|
await _scratchPurgeDeps.remove(sessionDir);
|
|
@@ -62811,12 +62939,12 @@ var DEFAULT_MAX_BATCH_SIZE = 4;
|
|
|
62811
62939
|
|
|
62812
62940
|
// src/pipeline/subscribers/events-writer.ts
|
|
62813
62941
|
import { appendFile as appendFile4, mkdir as mkdir13 } from "fs/promises";
|
|
62814
|
-
import { basename as basename13, join as
|
|
62942
|
+
import { basename as basename13, join as join80 } from "path";
|
|
62815
62943
|
function wireEventsWriter(bus, feature, runId, workdir) {
|
|
62816
62944
|
const logger = getSafeLogger();
|
|
62817
62945
|
const project = basename13(workdir);
|
|
62818
|
-
const eventsDir =
|
|
62819
|
-
const eventsFile =
|
|
62946
|
+
const eventsDir = join80(getEventsRootDir(), project);
|
|
62947
|
+
const eventsFile = join80(eventsDir, "events.jsonl");
|
|
62820
62948
|
let dirReady = false;
|
|
62821
62949
|
const write = (line) => {
|
|
62822
62950
|
return (async () => {
|
|
@@ -62997,12 +63125,12 @@ var init_interaction2 = __esm(() => {
|
|
|
62997
63125
|
|
|
62998
63126
|
// src/pipeline/subscribers/registry.ts
|
|
62999
63127
|
import { mkdir as mkdir14, writeFile as writeFile2 } from "fs/promises";
|
|
63000
|
-
import { basename as basename14, join as
|
|
63128
|
+
import { basename as basename14, join as join81 } from "path";
|
|
63001
63129
|
function wireRegistry(bus, feature, runId, workdir, outputDir) {
|
|
63002
63130
|
const logger = getSafeLogger();
|
|
63003
63131
|
const project = basename14(workdir);
|
|
63004
|
-
const runDir =
|
|
63005
|
-
const metaFile =
|
|
63132
|
+
const runDir = join81(getRunsDir(), `${project}-${feature}-${runId}`);
|
|
63133
|
+
const metaFile = join81(runDir, "meta.json");
|
|
63006
63134
|
const unsub = bus.on("run:started", (_ev) => {
|
|
63007
63135
|
return (async () => {
|
|
63008
63136
|
try {
|
|
@@ -63012,8 +63140,8 @@ function wireRegistry(bus, feature, runId, workdir, outputDir) {
|
|
|
63012
63140
|
project,
|
|
63013
63141
|
feature,
|
|
63014
63142
|
workdir,
|
|
63015
|
-
statusPath:
|
|
63016
|
-
eventsDir:
|
|
63143
|
+
statusPath: join81(outputDir, "features", feature, "status.json"),
|
|
63144
|
+
eventsDir: join81(outputDir, "features", feature, "runs"),
|
|
63017
63145
|
registeredAt: new Date().toISOString()
|
|
63018
63146
|
};
|
|
63019
63147
|
await writeFile2(metaFile, JSON.stringify(meta3, null, 2));
|
|
@@ -63258,8 +63386,8 @@ var init_types10 = __esm(() => {
|
|
|
63258
63386
|
});
|
|
63259
63387
|
|
|
63260
63388
|
// src/worktree/dependencies.ts
|
|
63261
|
-
import { existsSync as
|
|
63262
|
-
import { join as
|
|
63389
|
+
import { existsSync as existsSync32 } from "fs";
|
|
63390
|
+
import { join as join82 } from "path";
|
|
63263
63391
|
async function prepareWorktreeDependencies(options) {
|
|
63264
63392
|
const mode = options.config.execution.worktreeDependencies.mode;
|
|
63265
63393
|
const resolvedCwd = resolveDependencyCwd(options);
|
|
@@ -63273,7 +63401,7 @@ async function prepareWorktreeDependencies(options) {
|
|
|
63273
63401
|
}
|
|
63274
63402
|
}
|
|
63275
63403
|
function resolveDependencyCwd(options) {
|
|
63276
|
-
return options.storyWorkdir ?
|
|
63404
|
+
return options.storyWorkdir ? join82(options.worktreeRoot, options.storyWorkdir) : options.worktreeRoot;
|
|
63277
63405
|
}
|
|
63278
63406
|
function resolveInheritedDependencies(options, resolvedCwd) {
|
|
63279
63407
|
if (hasDependencyManifests(options.worktreeRoot, resolvedCwd)) {
|
|
@@ -63283,7 +63411,7 @@ function resolveInheritedDependencies(options, resolvedCwd) {
|
|
|
63283
63411
|
}
|
|
63284
63412
|
function hasDependencyManifests(worktreeRoot, resolvedCwd) {
|
|
63285
63413
|
const directories = resolvedCwd === worktreeRoot ? [worktreeRoot] : [worktreeRoot, resolvedCwd];
|
|
63286
|
-
return directories.some((directory) => PHASE_ONE_INHERIT_UNSUPPORTED_FILES.some((filename) => _worktreeDependencyDeps.existsSync(
|
|
63414
|
+
return directories.some((directory) => PHASE_ONE_INHERIT_UNSUPPORTED_FILES.some((filename) => _worktreeDependencyDeps.existsSync(join82(directory, filename))));
|
|
63287
63415
|
}
|
|
63288
63416
|
async function provisionDependencies(config2, worktreeRoot, resolvedCwd) {
|
|
63289
63417
|
const setupCommand2 = config2.execution.worktreeDependencies.setupCommand;
|
|
@@ -63334,7 +63462,7 @@ var init_dependencies = __esm(() => {
|
|
|
63334
63462
|
"build.gradle.kts"
|
|
63335
63463
|
];
|
|
63336
63464
|
_worktreeDependencyDeps = {
|
|
63337
|
-
existsSync:
|
|
63465
|
+
existsSync: existsSync32,
|
|
63338
63466
|
spawn
|
|
63339
63467
|
};
|
|
63340
63468
|
});
|
|
@@ -63345,19 +63473,19 @@ __export(exports_manager, {
|
|
|
63345
63473
|
_managerDeps: () => _managerDeps,
|
|
63346
63474
|
WorktreeManager: () => WorktreeManager
|
|
63347
63475
|
});
|
|
63348
|
-
import { existsSync as
|
|
63476
|
+
import { existsSync as existsSync33, symlinkSync } from "fs";
|
|
63349
63477
|
import { mkdir as mkdir15 } from "fs/promises";
|
|
63350
|
-
import { join as
|
|
63478
|
+
import { join as join83 } from "path";
|
|
63351
63479
|
|
|
63352
63480
|
class WorktreeManager {
|
|
63353
63481
|
async ensureGitExcludes(projectRoot) {
|
|
63354
63482
|
const logger = getSafeLogger();
|
|
63355
|
-
const infoDir =
|
|
63356
|
-
const excludePath =
|
|
63483
|
+
const infoDir = join83(projectRoot, ".git", "info");
|
|
63484
|
+
const excludePath = join83(infoDir, "exclude");
|
|
63357
63485
|
try {
|
|
63358
63486
|
await mkdir15(infoDir, { recursive: true });
|
|
63359
63487
|
let existing = "";
|
|
63360
|
-
if (
|
|
63488
|
+
if (existsSync33(excludePath)) {
|
|
63361
63489
|
existing = await Bun.file(excludePath).text();
|
|
63362
63490
|
}
|
|
63363
63491
|
const missing = NAX_GITIGNORE_ENTRIES.filter((entry) => !existing.includes(entry));
|
|
@@ -63380,7 +63508,7 @@ ${missing.join(`
|
|
|
63380
63508
|
}
|
|
63381
63509
|
async create(projectRoot, storyId) {
|
|
63382
63510
|
validateStoryId(storyId);
|
|
63383
|
-
const worktreePath =
|
|
63511
|
+
const worktreePath = join83(projectRoot, ".nax-wt", storyId);
|
|
63384
63512
|
const branchName = `nax/${storyId}`;
|
|
63385
63513
|
try {
|
|
63386
63514
|
const pruneProc = _managerDeps.spawn(["git", "worktree", "prune"], {
|
|
@@ -63441,9 +63569,9 @@ ${missing.join(`
|
|
|
63441
63569
|
projectRoot
|
|
63442
63570
|
});
|
|
63443
63571
|
}
|
|
63444
|
-
const envSource =
|
|
63445
|
-
if (
|
|
63446
|
-
const envTarget =
|
|
63572
|
+
const envSource = join83(projectRoot, ".env");
|
|
63573
|
+
if (existsSync33(envSource)) {
|
|
63574
|
+
const envTarget = join83(worktreePath, ".env");
|
|
63447
63575
|
try {
|
|
63448
63576
|
symlinkSync(envSource, envTarget, "file");
|
|
63449
63577
|
} catch (error48) {
|
|
@@ -63459,7 +63587,7 @@ ${missing.join(`
|
|
|
63459
63587
|
}
|
|
63460
63588
|
async remove(projectRoot, storyId) {
|
|
63461
63589
|
validateStoryId(storyId);
|
|
63462
|
-
const worktreePath =
|
|
63590
|
+
const worktreePath = join83(projectRoot, ".nax-wt", storyId);
|
|
63463
63591
|
const branchName = `nax/${storyId}`;
|
|
63464
63592
|
try {
|
|
63465
63593
|
const proc = _managerDeps.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
|
|
@@ -64063,6 +64191,7 @@ function resolveMaxAttemptsOutcome(failureCategory) {
|
|
|
64063
64191
|
case "isolation-violation":
|
|
64064
64192
|
case "verifier-rejected":
|
|
64065
64193
|
case "greenfield-no-tests":
|
|
64194
|
+
case "no-tests-authored":
|
|
64066
64195
|
return "pause";
|
|
64067
64196
|
case "runtime-crash":
|
|
64068
64197
|
return "pause";
|
|
@@ -64098,7 +64227,7 @@ async function handleTierEscalation(ctx) {
|
|
|
64098
64227
|
const escalateRetryAsLite = ctx.pipelineResult.context.retryAsLite === true;
|
|
64099
64228
|
const escalateFailureCategory = ctx.pipelineResult.context.tddFailureCategory;
|
|
64100
64229
|
const escalateReviewFindings = ctx.pipelineResult.context.reviewFindings;
|
|
64101
|
-
const
|
|
64230
|
+
const escalateRetryAsTddSimple = escalateFailureCategory === "greenfield-no-tests";
|
|
64102
64231
|
const routingMode = ctx.config.routing.llm?.mode ?? "hybrid";
|
|
64103
64232
|
if (!escalationResult || !ctx.config.autoMode.escalation.enabled) {
|
|
64104
64233
|
return await handleNoTierAvailable(ctx, escalateFailureCategory);
|
|
@@ -64111,12 +64240,12 @@ async function handleTierEscalation(ctx) {
|
|
|
64111
64240
|
const escalatedTier = escalationResult.tier;
|
|
64112
64241
|
for (const s of storiesToEscalate) {
|
|
64113
64242
|
const currentTestStrategy = s.routing?.testStrategy ?? ctx.routing.testStrategy;
|
|
64114
|
-
const
|
|
64115
|
-
if (
|
|
64116
|
-
logger?.warn("escalation", "Switching strategy to
|
|
64243
|
+
const shouldSwitchToTddSimple = escalateRetryAsTddSimple && isThreeSessionStrategy(currentTestStrategy);
|
|
64244
|
+
if (shouldSwitchToTddSimple) {
|
|
64245
|
+
logger?.warn("escalation", "Switching strategy to tdd-simple (greenfield-no-tests fallback)", {
|
|
64117
64246
|
storyId: s.id,
|
|
64118
64247
|
fromStrategy: currentTestStrategy,
|
|
64119
|
-
toStrategy: "
|
|
64248
|
+
toStrategy: "tdd-simple"
|
|
64120
64249
|
});
|
|
64121
64250
|
} else {
|
|
64122
64251
|
logger?.warn("escalation", "Escalating story to next tier", {
|
|
@@ -64137,19 +64266,19 @@ async function handleTierEscalation(ctx) {
|
|
|
64137
64266
|
if (!shouldEscalate)
|
|
64138
64267
|
return s;
|
|
64139
64268
|
const currentTestStrategy = s.routing?.testStrategy ?? ctx.routing.testStrategy;
|
|
64140
|
-
const
|
|
64269
|
+
const shouldSwitchToTddSimple = escalateRetryAsTddSimple && isThreeSessionStrategy(currentTestStrategy);
|
|
64141
64270
|
const baseRouting = s.routing ?? { ...ctx.routing };
|
|
64142
64271
|
const updatedRouting = {
|
|
64143
64272
|
...baseRouting,
|
|
64144
|
-
modelTier:
|
|
64273
|
+
modelTier: shouldSwitchToTddSimple ? baseRouting.modelTier : escalatedTier,
|
|
64145
64274
|
...nextAgent !== undefined ? { agent: nextAgent } : {},
|
|
64146
64275
|
...escalateRetryAsLite ? { testStrategy: "three-session-tdd-lite" } : {},
|
|
64147
|
-
...
|
|
64276
|
+
...shouldSwitchToTddSimple ? { testStrategy: "tdd-simple" } : {}
|
|
64148
64277
|
};
|
|
64149
64278
|
const currentStoryTier = s.routing?.modelTier ?? ctx.routing.modelTier;
|
|
64150
64279
|
const isChangingTier = currentStoryTier !== escalatedTier;
|
|
64151
|
-
const shouldResetAttempts = isChangingTier ||
|
|
64152
|
-
const escalationRecord = isChangingTier ||
|
|
64280
|
+
const shouldResetAttempts = isChangingTier || shouldSwitchToTddSimple;
|
|
64281
|
+
const escalationRecord = isChangingTier || shouldSwitchToTddSimple ? buildEscalationRecord(currentStoryTier, shouldSwitchToTddSimple ? currentStoryTier : escalatedTier, ctx.pipelineResult.reason ?? "Escalated to next retry path", { fromAgent: s.routing?.agent, toAgent: nextAgent }) : undefined;
|
|
64153
64282
|
const escalationFailure = buildEscalationFailure(s, currentStoryTier, escalateReviewFindings, ctx.attemptCost, verifiedPipelineReason, escalateFailureCategory);
|
|
64154
64283
|
return {
|
|
64155
64284
|
...s,
|
|
@@ -64188,6 +64317,7 @@ async function handleTierEscalation(ctx) {
|
|
|
64188
64317
|
var _tierEscalationDeps;
|
|
64189
64318
|
var init_tier_escalation = __esm(() => {
|
|
64190
64319
|
init_pipeline();
|
|
64320
|
+
init_config();
|
|
64191
64321
|
init_hooks();
|
|
64192
64322
|
init_logger2();
|
|
64193
64323
|
init_prd();
|
|
@@ -64306,10 +64436,10 @@ var init_merge_conflict_rectify = __esm(() => {
|
|
|
64306
64436
|
});
|
|
64307
64437
|
|
|
64308
64438
|
// src/execution/pipeline-result-handler.ts
|
|
64309
|
-
import { join as
|
|
64439
|
+
import { join as join84 } from "path";
|
|
64310
64440
|
async function removeWorktreeDirectory(projectRoot, storyId) {
|
|
64311
64441
|
const logger = getSafeLogger();
|
|
64312
|
-
const worktreePath =
|
|
64442
|
+
const worktreePath = join84(projectRoot, ".nax-wt", storyId);
|
|
64313
64443
|
try {
|
|
64314
64444
|
const proc = _resultHandlerDeps.spawn(["git", "worktree", "remove", worktreePath, "--force"], {
|
|
64315
64445
|
cwd: projectRoot,
|
|
@@ -64525,8 +64655,8 @@ var init_pipeline_result_handler = __esm(() => {
|
|
|
64525
64655
|
});
|
|
64526
64656
|
|
|
64527
64657
|
// src/execution/iteration-runner.ts
|
|
64528
|
-
import { existsSync as
|
|
64529
|
-
import { join as
|
|
64658
|
+
import { existsSync as existsSync34 } from "fs";
|
|
64659
|
+
import { join as join85 } from "path";
|
|
64530
64660
|
async function runIteration(ctx, prd, selection, iterations, totalCost, allStoryMetrics) {
|
|
64531
64661
|
const { story, storiesToExecute, routing, isBatchExecution } = selection;
|
|
64532
64662
|
if (ctx.dryRun) {
|
|
@@ -64551,7 +64681,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
|
|
|
64551
64681
|
const storyStartTime = Date.now();
|
|
64552
64682
|
let effectiveWorkdir = ctx.workdir;
|
|
64553
64683
|
if (ctx.config.execution.storyIsolation === "worktree") {
|
|
64554
|
-
const worktreePath =
|
|
64684
|
+
const worktreePath = join85(ctx.workdir, ".nax-wt", story.id);
|
|
64555
64685
|
const worktreeExists = _iterationRunnerDeps.existsSync(worktreePath);
|
|
64556
64686
|
if (!worktreeExists) {
|
|
64557
64687
|
await _iterationRunnerDeps.worktreeManager.ensureGitExcludes(ctx.workdir);
|
|
@@ -64571,7 +64701,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
|
|
|
64571
64701
|
}
|
|
64572
64702
|
const accumulatedAttemptCost = (story.priorFailures || []).reduce((sum, f) => sum + (f.cost || 0), 0);
|
|
64573
64703
|
const profileOverride = profileOverrideFromConfig(ctx.config);
|
|
64574
|
-
const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(
|
|
64704
|
+
const effectiveConfig = story.workdir ? await _iterationRunnerDeps.loadConfigForWorkdir(join85(ctx.workdir, ".nax", "config.json"), story.workdir, profileOverride) : ctx.config;
|
|
64575
64705
|
let dependencyContext;
|
|
64576
64706
|
if (ctx.config.execution.storyIsolation === "worktree") {
|
|
64577
64707
|
try {
|
|
@@ -64598,7 +64728,7 @@ async function runIteration(ctx, prd, selection, iterations, totalCost, allStory
|
|
|
64598
64728
|
};
|
|
64599
64729
|
}
|
|
64600
64730
|
}
|
|
64601
|
-
const resolvedWorkdir = dependencyContext?.cwd ? dependencyContext.cwd : ctx.config.execution.storyIsolation === "worktree" ? story.workdir ?
|
|
64731
|
+
const resolvedWorkdir = dependencyContext?.cwd ? dependencyContext.cwd : ctx.config.execution.storyIsolation === "worktree" ? story.workdir ? join85(effectiveWorkdir, story.workdir) : effectiveWorkdir : story.workdir ? join85(ctx.workdir, story.workdir) : ctx.workdir;
|
|
64602
64732
|
const pipelineContext = {
|
|
64603
64733
|
config: effectiveConfig,
|
|
64604
64734
|
rootConfig: ctx.config,
|
|
@@ -64731,7 +64861,7 @@ var init_iteration_runner = __esm(() => {
|
|
|
64731
64861
|
loadConfigForWorkdir,
|
|
64732
64862
|
prepareWorktreeDependencies,
|
|
64733
64863
|
runPipeline,
|
|
64734
|
-
existsSync:
|
|
64864
|
+
existsSync: existsSync34,
|
|
64735
64865
|
worktreeManager: new WorktreeManager
|
|
64736
64866
|
};
|
|
64737
64867
|
});
|
|
@@ -64801,7 +64931,7 @@ __export(exports_parallel_worker, {
|
|
|
64801
64931
|
buildWorktreePipelineContext: () => buildWorktreePipelineContext,
|
|
64802
64932
|
_parallelWorkerDeps: () => _parallelWorkerDeps
|
|
64803
64933
|
});
|
|
64804
|
-
import { join as
|
|
64934
|
+
import { join as join86 } from "path";
|
|
64805
64935
|
function buildWorktreePipelineContext(base, _story) {
|
|
64806
64936
|
return { ...base, prd: structuredClone(base.prd) };
|
|
64807
64937
|
}
|
|
@@ -64824,7 +64954,7 @@ async function executeStoryInWorktree(story, worktreePath, dependencyContext, co
|
|
|
64824
64954
|
story,
|
|
64825
64955
|
stories: [story],
|
|
64826
64956
|
projectDir: context.projectDir,
|
|
64827
|
-
workdir: dependencyContext.cwd ?? (story.workdir ?
|
|
64957
|
+
workdir: dependencyContext.cwd ?? (story.workdir ? join86(worktreePath, story.workdir) : worktreePath),
|
|
64828
64958
|
worktreeDependencyContext: dependencyContext,
|
|
64829
64959
|
routing,
|
|
64830
64960
|
storyGitRef: storyGitRef ?? undefined
|
|
@@ -65724,7 +65854,7 @@ async function writeStatusFile(filePath, status) {
|
|
|
65724
65854
|
var init_status_file = () => {};
|
|
65725
65855
|
|
|
65726
65856
|
// src/execution/status-writer.ts
|
|
65727
|
-
import { join as
|
|
65857
|
+
import { join as join87 } from "path";
|
|
65728
65858
|
|
|
65729
65859
|
class StatusWriter {
|
|
65730
65860
|
statusFile;
|
|
@@ -65843,7 +65973,7 @@ class StatusWriter {
|
|
|
65843
65973
|
if (!this._prd)
|
|
65844
65974
|
return;
|
|
65845
65975
|
const safeLogger = getSafeLogger();
|
|
65846
|
-
const featureStatusPath =
|
|
65976
|
+
const featureStatusPath = join87(featureDir, "status.json");
|
|
65847
65977
|
const write = async () => {
|
|
65848
65978
|
try {
|
|
65849
65979
|
const base = this.getSnapshot(totalCost, iterations);
|
|
@@ -65874,11 +66004,11 @@ __export(exports_migrate, {
|
|
|
65874
66004
|
migrateCommand: () => migrateCommand,
|
|
65875
66005
|
detectGeneratedContent: () => detectGeneratedContent
|
|
65876
66006
|
});
|
|
65877
|
-
import { existsSync as
|
|
66007
|
+
import { existsSync as existsSync35 } from "fs";
|
|
65878
66008
|
import { mkdir as mkdir16, readdir as readdir5, rename as rename3 } from "fs/promises";
|
|
65879
66009
|
import path25 from "path";
|
|
65880
66010
|
async function detectGeneratedContent(naxDir) {
|
|
65881
|
-
if (!
|
|
66011
|
+
if (!existsSync35(naxDir))
|
|
65882
66012
|
return [];
|
|
65883
66013
|
const candidates = [];
|
|
65884
66014
|
let entries = [];
|
|
@@ -65893,7 +66023,7 @@ async function detectGeneratedContent(naxDir) {
|
|
|
65893
66023
|
}
|
|
65894
66024
|
}
|
|
65895
66025
|
const featuresDir = path25.join(naxDir, "features");
|
|
65896
|
-
if (
|
|
66026
|
+
if (existsSync35(featuresDir)) {
|
|
65897
66027
|
let featureDirs = [];
|
|
65898
66028
|
try {
|
|
65899
66029
|
featureDirs = await readdir5(featuresDir);
|
|
@@ -65955,7 +66085,7 @@ async function migrateCommand(options) {
|
|
|
65955
66085
|
});
|
|
65956
66086
|
}
|
|
65957
66087
|
const src = path25.join(globalConfigDir(), options.reclaim);
|
|
65958
|
-
if (!
|
|
66088
|
+
if (!existsSync35(src)) {
|
|
65959
66089
|
throw new NaxError(`Nothing to reclaim: ~/.nax/${options.reclaim} does not exist`, "MIGRATE_RECLAIM_NOT_FOUND", {
|
|
65960
66090
|
stage: "migrate",
|
|
65961
66091
|
name: options.reclaim
|
|
@@ -66001,7 +66131,7 @@ async function migrateCommand(options) {
|
|
|
66001
66131
|
}
|
|
66002
66132
|
const naxDir = path25.join(options.workdir, ".nax");
|
|
66003
66133
|
const configPath = path25.join(naxDir, "config.json");
|
|
66004
|
-
if (!
|
|
66134
|
+
if (!existsSync35(configPath)) {
|
|
66005
66135
|
throw new NaxError("No .nax/config.json found \u2014 run nax init first", "MIGRATE_NO_CONFIG", {
|
|
66006
66136
|
stage: "migrate",
|
|
66007
66137
|
workdir: options.workdir
|
|
@@ -66036,7 +66166,7 @@ async function migrateCommand(options) {
|
|
|
66036
66166
|
for (const candidate of candidates) {
|
|
66037
66167
|
const dest = path25.join(destBase, candidate.name);
|
|
66038
66168
|
await mkdir16(path25.dirname(dest), { recursive: true });
|
|
66039
|
-
if (
|
|
66169
|
+
if (existsSync35(dest)) {
|
|
66040
66170
|
throw new NaxError(`Migration conflict: destination already exists.
|
|
66041
66171
|
Source: ${candidate.srcPath}
|
|
66042
66172
|
Destination: ${dest}
|
|
@@ -66287,7 +66417,7 @@ __export(exports_run_initialization, {
|
|
|
66287
66417
|
initializeRun: () => initializeRun,
|
|
66288
66418
|
_reconcileDeps: () => _reconcileDeps
|
|
66289
66419
|
});
|
|
66290
|
-
import { join as
|
|
66420
|
+
import { join as join88 } from "path";
|
|
66291
66421
|
async function reconcileState(prd, prdPath, workdir, config2) {
|
|
66292
66422
|
const logger = getSafeLogger();
|
|
66293
66423
|
let reconciledCount = 0;
|
|
@@ -66304,7 +66434,7 @@ async function reconcileState(prd, prdPath, workdir, config2) {
|
|
|
66304
66434
|
});
|
|
66305
66435
|
continue;
|
|
66306
66436
|
}
|
|
66307
|
-
const effectiveWorkdir = story.workdir ?
|
|
66437
|
+
const effectiveWorkdir = story.workdir ? join88(workdir, story.workdir) : workdir;
|
|
66308
66438
|
try {
|
|
66309
66439
|
const reviewResult = await _reconcileDeps.runReview(config2.review, effectiveWorkdir, config2.execution);
|
|
66310
66440
|
if (!reviewResult.success) {
|
|
@@ -96135,7 +96265,7 @@ __export(exports_curator, {
|
|
|
96135
96265
|
});
|
|
96136
96266
|
import { readdirSync as readdirSync9 } from "fs";
|
|
96137
96267
|
import { unlink as unlink4 } from "fs/promises";
|
|
96138
|
-
import { basename as basename15, join as
|
|
96268
|
+
import { basename as basename15, join as join90 } from "path";
|
|
96139
96269
|
function getProjectKey(config2, projectDir) {
|
|
96140
96270
|
return config2.name?.trim() || basename15(projectDir);
|
|
96141
96271
|
}
|
|
@@ -96218,7 +96348,7 @@ async function curatorStatus(options) {
|
|
|
96218
96348
|
const config2 = await _curatorCmdDeps.loadConfig(resolved.projectDir);
|
|
96219
96349
|
const projectKey = getProjectKey(config2, resolved.projectDir);
|
|
96220
96350
|
const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
|
|
96221
|
-
const runsDir =
|
|
96351
|
+
const runsDir = join90(outputDir, "runs");
|
|
96222
96352
|
const runIds = listRunIds(runsDir);
|
|
96223
96353
|
let runId;
|
|
96224
96354
|
if (options.run) {
|
|
@@ -96235,8 +96365,8 @@ async function curatorStatus(options) {
|
|
|
96235
96365
|
runId = runIds[runIds.length - 1];
|
|
96236
96366
|
}
|
|
96237
96367
|
console.log(`Run: ${runId}`);
|
|
96238
|
-
const runDir =
|
|
96239
|
-
const observationsPath =
|
|
96368
|
+
const runDir = join90(runsDir, runId);
|
|
96369
|
+
const observationsPath = join90(runDir, "observations.jsonl");
|
|
96240
96370
|
const observations = await parseObservations(observationsPath);
|
|
96241
96371
|
const counts = new Map;
|
|
96242
96372
|
for (const obs of observations) {
|
|
@@ -96246,7 +96376,7 @@ async function curatorStatus(options) {
|
|
|
96246
96376
|
for (const [kind, count] of counts.entries()) {
|
|
96247
96377
|
console.log(` ${kind}: ${count}`);
|
|
96248
96378
|
}
|
|
96249
|
-
const proposalsPath =
|
|
96379
|
+
const proposalsPath = join90(runDir, "curator-proposals.md");
|
|
96250
96380
|
const proposalText = await _curatorCmdDeps.readFile(proposalsPath).catch(() => null);
|
|
96251
96381
|
if (proposalText !== null) {
|
|
96252
96382
|
console.log("");
|
|
@@ -96260,8 +96390,8 @@ async function curatorCommit(options) {
|
|
|
96260
96390
|
const config2 = await _curatorCmdDeps.loadConfig(resolved.projectDir);
|
|
96261
96391
|
const projectKey = getProjectKey(config2, resolved.projectDir);
|
|
96262
96392
|
const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
|
|
96263
|
-
const runDir =
|
|
96264
|
-
const proposalsPath =
|
|
96393
|
+
const runDir = join90(outputDir, "runs", options.runId);
|
|
96394
|
+
const proposalsPath = join90(runDir, "curator-proposals.md");
|
|
96265
96395
|
const proposalText = await _curatorCmdDeps.readFile(proposalsPath).catch(() => null);
|
|
96266
96396
|
if (proposalText === null) {
|
|
96267
96397
|
console.log(`curator-proposals.md not found for run ${options.runId}.`);
|
|
@@ -96277,7 +96407,7 @@ async function curatorCommit(options) {
|
|
|
96277
96407
|
const dropFileState = new Map;
|
|
96278
96408
|
const skippedDrops = new Set;
|
|
96279
96409
|
for (const drop2 of drops) {
|
|
96280
|
-
const targetPath =
|
|
96410
|
+
const targetPath = join90(resolved.projectDir, drop2.canonicalFile);
|
|
96281
96411
|
if (!dropFileState.has(targetPath)) {
|
|
96282
96412
|
const fileExists2 = await Bun.file(targetPath).exists();
|
|
96283
96413
|
const existing = fileExists2 ? await _curatorCmdDeps.readFile(targetPath).catch(() => "") : "";
|
|
@@ -96311,7 +96441,7 @@ async function curatorCommit(options) {
|
|
|
96311
96441
|
if (skippedDrops.has(drop2)) {
|
|
96312
96442
|
continue;
|
|
96313
96443
|
}
|
|
96314
|
-
const targetPath =
|
|
96444
|
+
const targetPath = join90(resolved.projectDir, drop2.canonicalFile);
|
|
96315
96445
|
const existing = await _curatorCmdDeps.readFile(targetPath).catch(() => "");
|
|
96316
96446
|
const filtered = filterDropContent(existing, drop2.description);
|
|
96317
96447
|
await _curatorCmdDeps.writeFile(targetPath, filtered);
|
|
@@ -96320,7 +96450,7 @@ async function curatorCommit(options) {
|
|
|
96320
96450
|
}
|
|
96321
96451
|
const adds = proposals.filter((p) => p.action === "add" || p.action === "advisory");
|
|
96322
96452
|
for (const add2 of adds) {
|
|
96323
|
-
const targetPath =
|
|
96453
|
+
const targetPath = join90(resolved.projectDir, add2.canonicalFile);
|
|
96324
96454
|
const content = buildAddContent(add2);
|
|
96325
96455
|
await _curatorCmdDeps.appendFile(targetPath, content);
|
|
96326
96456
|
modifiedFiles.add(targetPath);
|
|
@@ -96357,7 +96487,7 @@ async function curatorDryrun(options) {
|
|
|
96357
96487
|
const config2 = await _curatorCmdDeps.loadConfig(resolved.projectDir);
|
|
96358
96488
|
const projectKey = getProjectKey(config2, resolved.projectDir);
|
|
96359
96489
|
const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
|
|
96360
|
-
const runsDir =
|
|
96490
|
+
const runsDir = join90(outputDir, "runs");
|
|
96361
96491
|
const runIds = listRunIds(runsDir);
|
|
96362
96492
|
if (runIds.length === 0) {
|
|
96363
96493
|
console.log("No runs found.");
|
|
@@ -96368,7 +96498,7 @@ async function curatorDryrun(options) {
|
|
|
96368
96498
|
console.log(`Run ${options.run} not found in ${runsDir}.`);
|
|
96369
96499
|
return;
|
|
96370
96500
|
}
|
|
96371
|
-
const observationsPath =
|
|
96501
|
+
const observationsPath = join90(runsDir, runId, "observations.jsonl");
|
|
96372
96502
|
const observations = await parseObservations(observationsPath);
|
|
96373
96503
|
const thresholds = getThresholds(config2);
|
|
96374
96504
|
const proposals = runHeuristics(observations, thresholds);
|
|
@@ -96409,12 +96539,12 @@ async function curatorGc(options) {
|
|
|
96409
96539
|
await _curatorCmdDeps.writeFile(rollupPath, newContent);
|
|
96410
96540
|
const projectKey = getProjectKey(config2, resolved.projectDir);
|
|
96411
96541
|
const outputDir = _curatorCmdDeps.projectOutputDir(projectKey, config2.outputDir);
|
|
96412
|
-
const perRunsDir =
|
|
96542
|
+
const perRunsDir = join90(outputDir, "runs");
|
|
96413
96543
|
for (const runId of uniqueRunIds) {
|
|
96414
96544
|
if (!keepSet.has(runId)) {
|
|
96415
|
-
const runDir =
|
|
96416
|
-
await _curatorCmdDeps.removeFile(
|
|
96417
|
-
await _curatorCmdDeps.removeFile(
|
|
96545
|
+
const runDir = join90(perRunsDir, runId);
|
|
96546
|
+
await _curatorCmdDeps.removeFile(join90(runDir, "observations.jsonl"));
|
|
96547
|
+
await _curatorCmdDeps.removeFile(join90(runDir, "curator-proposals.md"));
|
|
96418
96548
|
}
|
|
96419
96549
|
}
|
|
96420
96550
|
console.log(`[gc] Pruned rollup to ${keep} most recent runs (was ${uniqueRunIds.length}).`);
|
|
@@ -96457,9 +96587,9 @@ var init_curator2 = __esm(() => {
|
|
|
96457
96587
|
|
|
96458
96588
|
// bin/nax.ts
|
|
96459
96589
|
init_source();
|
|
96460
|
-
import { existsSync as
|
|
96590
|
+
import { existsSync as existsSync37, mkdirSync as mkdirSync7 } from "fs";
|
|
96461
96591
|
import { homedir as homedir3 } from "os";
|
|
96462
|
-
import { basename as basename16, join as
|
|
96592
|
+
import { basename as basename16, join as join91 } from "path";
|
|
96463
96593
|
|
|
96464
96594
|
// node_modules/commander/esm.mjs
|
|
96465
96595
|
var import__ = __toESM(require_commander(), 1);
|
|
@@ -97923,7 +98053,7 @@ var FIELD_DESCRIPTIONS = {
|
|
|
97923
98053
|
"tdd.sessionTiers.verifier": "Model tier for verifier session",
|
|
97924
98054
|
"tdd.testWriterAllowedPaths": "Glob patterns for files test-writer can modify",
|
|
97925
98055
|
"tdd.rollbackOnFailure": "Rollback git changes when TDD fails",
|
|
97926
|
-
"tdd.greenfieldDetection": "Force
|
|
98056
|
+
"tdd.greenfieldDetection": "Force tdd-simple on projects with no test files",
|
|
97927
98057
|
constitution: "Constitution settings (core rules and constraints)",
|
|
97928
98058
|
"constitution.enabled": "Enable constitution loading and injection",
|
|
97929
98059
|
"constitution.path": "Path to constitution file (relative to nax/ directory)",
|
|
@@ -98733,64 +98863,119 @@ async function resolveRunProfileOverride(opts) {
|
|
|
98733
98863
|
}
|
|
98734
98864
|
// src/cli/features-resolve.ts
|
|
98735
98865
|
init_config();
|
|
98736
|
-
import { existsSync as
|
|
98866
|
+
import { existsSync as existsSync28, readdirSync as readdirSync6 } from "fs";
|
|
98867
|
+
import { join as join70, relative as relative15 } from "path";
|
|
98868
|
+
|
|
98869
|
+
// src/cli/features-acceptance.ts
|
|
98870
|
+
init_acceptance2();
|
|
98871
|
+
init_config();
|
|
98872
|
+
init_logger2();
|
|
98873
|
+
init_prd();
|
|
98874
|
+
import { existsSync as existsSync27 } from "fs";
|
|
98737
98875
|
import { join as join69, relative as relative14 } from "path";
|
|
98876
|
+
async function resolveFeatureAcceptance(featureName, workdir) {
|
|
98877
|
+
let enabled = true;
|
|
98878
|
+
try {
|
|
98879
|
+
const naxDir = findProjectDir(workdir);
|
|
98880
|
+
if (!naxDir) {
|
|
98881
|
+
return { status: "no-prd", enabled, groups: [] };
|
|
98882
|
+
}
|
|
98883
|
+
const repoRoot = join69(naxDir, "..");
|
|
98884
|
+
const config2 = await loadConfig(workdir);
|
|
98885
|
+
enabled = config2.acceptance?.enabled ?? true;
|
|
98886
|
+
if (!enabled) {
|
|
98887
|
+
return { status: "disabled", enabled: false, groups: [] };
|
|
98888
|
+
}
|
|
98889
|
+
const prdPath = join69(naxDir, "features", featureName, "prd.json");
|
|
98890
|
+
if (!existsSync27(prdPath)) {
|
|
98891
|
+
return { status: "no-prd", enabled, groups: [] };
|
|
98892
|
+
}
|
|
98893
|
+
const prd = await loadPRD(prdPath);
|
|
98894
|
+
const testGroups = await groupStoriesByPackage(prd, repoRoot, featureName, config2.acceptance?.testPath, config2.project?.language);
|
|
98895
|
+
const groups = await Promise.all(testGroups.map(async (g) => {
|
|
98896
|
+
const packageDir = relative14(repoRoot, g.packageDir);
|
|
98897
|
+
const command = await resolveGroupCommand(repoRoot, packageDir, config2.acceptance?.command);
|
|
98898
|
+
return {
|
|
98899
|
+
packageDir,
|
|
98900
|
+
testPath: relative14(repoRoot, g.testPath),
|
|
98901
|
+
exists: await Bun.file(g.testPath).exists(),
|
|
98902
|
+
command,
|
|
98903
|
+
language: g.language
|
|
98904
|
+
};
|
|
98905
|
+
}));
|
|
98906
|
+
return { status: "ok", enabled, groups };
|
|
98907
|
+
} catch (err) {
|
|
98908
|
+
getSafeLogger()?.warn("acceptance", "Failed to resolve feature acceptance targets", {
|
|
98909
|
+
featureName,
|
|
98910
|
+
cause: errorMessage(err)
|
|
98911
|
+
});
|
|
98912
|
+
return { status: "no-prd", enabled, groups: [] };
|
|
98913
|
+
}
|
|
98914
|
+
}
|
|
98915
|
+
async function resolveGroupCommand(repoRoot, packageDir, rootCommand) {
|
|
98916
|
+
if (packageDir === "")
|
|
98917
|
+
return rootCommand;
|
|
98918
|
+
const override = await loadPackageOverride(repoRoot, packageDir);
|
|
98919
|
+
return override?.acceptance?.command ?? rootCommand;
|
|
98920
|
+
}
|
|
98921
|
+
|
|
98922
|
+
// src/cli/features-resolve.ts
|
|
98738
98923
|
async function isNonEmptyFile(absolutePath) {
|
|
98739
|
-
if (!
|
|
98924
|
+
if (!existsSync28(absolutePath))
|
|
98740
98925
|
return false;
|
|
98741
98926
|
const content = await Bun.file(absolutePath).text();
|
|
98742
98927
|
return content.trim().length > 0;
|
|
98743
98928
|
}
|
|
98744
98929
|
async function searchSpecSource(naxDir, repoRoot, name) {
|
|
98745
98930
|
const candidates = [
|
|
98746
|
-
{ abs:
|
|
98747
|
-
{ abs:
|
|
98931
|
+
{ abs: join70(naxDir, "features", name, "spec.md"), kind: "markdown" },
|
|
98932
|
+
{ abs: join70(naxDir, "specs", `${name}.md`), kind: "markdown" }
|
|
98748
98933
|
];
|
|
98749
|
-
const docsSpecExact =
|
|
98934
|
+
const docsSpecExact = join70(repoRoot, "docs", "specs", `SPEC-${name}.md`);
|
|
98750
98935
|
candidates.push({ abs: docsSpecExact, kind: "markdown" });
|
|
98751
|
-
const checked = candidates.map((c) =>
|
|
98936
|
+
const checked = candidates.map((c) => relative15(repoRoot, c.abs));
|
|
98752
98937
|
for (const { abs, kind } of candidates.slice(0, 2)) {
|
|
98753
98938
|
if (kind === "markdown") {
|
|
98754
98939
|
const nonEmpty = await isNonEmptyFile(abs);
|
|
98755
98940
|
if (nonEmpty) {
|
|
98756
|
-
return { source: { kind, path:
|
|
98941
|
+
return { source: { kind, path: relative15(repoRoot, abs) }, checked };
|
|
98757
98942
|
}
|
|
98758
98943
|
}
|
|
98759
98944
|
}
|
|
98760
98945
|
if (await isNonEmptyFile(docsSpecExact)) {
|
|
98761
|
-
return { source: { kind: "markdown", path:
|
|
98946
|
+
return { source: { kind: "markdown", path: relative15(repoRoot, docsSpecExact) }, checked };
|
|
98762
98947
|
}
|
|
98763
|
-
const docsSpecsDir =
|
|
98764
|
-
if (
|
|
98948
|
+
const docsSpecsDir = join70(repoRoot, "docs", "specs");
|
|
98949
|
+
if (existsSync28(docsSpecsDir)) {
|
|
98765
98950
|
const glob = new Bun.Glob(`*${name}*.md`);
|
|
98766
98951
|
for (const match of glob.scanSync({ cwd: docsSpecsDir, absolute: false })) {
|
|
98767
|
-
const abs =
|
|
98952
|
+
const abs = join70(docsSpecsDir, match);
|
|
98768
98953
|
if (await isNonEmptyFile(abs)) {
|
|
98769
|
-
const relPath =
|
|
98954
|
+
const relPath = relative15(repoRoot, abs);
|
|
98770
98955
|
if (!checked.includes(relPath))
|
|
98771
98956
|
checked.push(relPath);
|
|
98772
98957
|
return { source: { kind: "markdown", path: relPath }, checked };
|
|
98773
98958
|
}
|
|
98774
98959
|
}
|
|
98775
98960
|
}
|
|
98776
|
-
const prdAbs =
|
|
98777
|
-
const prdRel =
|
|
98961
|
+
const prdAbs = join70(naxDir, "features", name, "prd.json");
|
|
98962
|
+
const prdRel = relative15(repoRoot, prdAbs);
|
|
98778
98963
|
if (!checked.includes(prdRel))
|
|
98779
98964
|
checked.push(prdRel);
|
|
98780
|
-
if (
|
|
98965
|
+
if (existsSync28(prdAbs)) {
|
|
98781
98966
|
return { source: { kind: "prd", path: prdRel }, checked };
|
|
98782
98967
|
}
|
|
98783
98968
|
return { source: null, checked };
|
|
98784
98969
|
}
|
|
98785
98970
|
function discoverCandidates(naxDir) {
|
|
98786
|
-
const featuresDir =
|
|
98787
|
-
if (!
|
|
98971
|
+
const featuresDir = join70(naxDir, "features");
|
|
98972
|
+
if (!existsSync28(featuresDir))
|
|
98788
98973
|
return [];
|
|
98789
98974
|
return readdirSync6(featuresDir, { withFileTypes: true }).filter((e) => {
|
|
98790
98975
|
if (!e.isDirectory())
|
|
98791
98976
|
return false;
|
|
98792
|
-
const dir =
|
|
98793
|
-
return
|
|
98977
|
+
const dir = join70(featuresDir, e.name);
|
|
98978
|
+
return existsSync28(join70(dir, "prd.json")) || existsSync28(join70(dir, "spec.md"));
|
|
98794
98979
|
}).map((e) => e.name).sort();
|
|
98795
98980
|
}
|
|
98796
98981
|
async function resolveFeatureSpec(name, workdir) {
|
|
@@ -98801,10 +98986,10 @@ async function resolveFeatureSpec(name, workdir) {
|
|
|
98801
98986
|
message: `not a nax repo: no .nax/config.json found from ${workdir}`
|
|
98802
98987
|
};
|
|
98803
98988
|
}
|
|
98804
|
-
const repoRoot =
|
|
98989
|
+
const repoRoot = join70(naxDir, "..");
|
|
98805
98990
|
if (name !== undefined && (name.startsWith("./") || name.startsWith("/") || name.endsWith(".md"))) {
|
|
98806
|
-
const abs = name.startsWith("/") ? name :
|
|
98807
|
-
if (!
|
|
98991
|
+
const abs = name.startsWith("/") ? name : join70(workdir, name);
|
|
98992
|
+
if (!existsSync28(abs)) {
|
|
98808
98993
|
return {
|
|
98809
98994
|
status: "missing",
|
|
98810
98995
|
featureName: null,
|
|
@@ -98826,8 +99011,8 @@ async function resolveFeatureSpec(name, workdir) {
|
|
|
98826
99011
|
return {
|
|
98827
99012
|
status: "ok",
|
|
98828
99013
|
featureName: null,
|
|
98829
|
-
specSource: { kind: "markdown", path:
|
|
98830
|
-
message: `resolved spec: ${
|
|
99014
|
+
specSource: { kind: "markdown", path: relative15(repoRoot, abs) },
|
|
99015
|
+
message: `resolved spec: ${relative15(repoRoot, abs)}`
|
|
98831
99016
|
};
|
|
98832
99017
|
}
|
|
98833
99018
|
if (name !== undefined && name.trim() !== "") {
|
|
@@ -98838,11 +99023,12 @@ async function resolveFeatureSpec(name, workdir) {
|
|
|
98838
99023
|
status: "ok",
|
|
98839
99024
|
featureName: name,
|
|
98840
99025
|
specSource: source2,
|
|
99026
|
+
acceptance: await resolveFeatureAcceptance(name, workdir),
|
|
98841
99027
|
message: `resolved spec: ${source2.path}`
|
|
98842
99028
|
};
|
|
98843
99029
|
}
|
|
98844
|
-
const featureDir =
|
|
98845
|
-
if (
|
|
99030
|
+
const featureDir = join70(naxDir, "features", name);
|
|
99031
|
+
if (existsSync28(featureDir)) {
|
|
98846
99032
|
return {
|
|
98847
99033
|
status: "missing",
|
|
98848
99034
|
featureName: name,
|
|
@@ -98883,6 +99069,7 @@ async function resolveFeatureSpec(name, workdir) {
|
|
|
98883
99069
|
status: "ok",
|
|
98884
99070
|
featureName: onlyName,
|
|
98885
99071
|
specSource: source,
|
|
99072
|
+
acceptance: await resolveFeatureAcceptance(onlyName, workdir),
|
|
98886
99073
|
message: `resolved spec: ${source.path}`
|
|
98887
99074
|
};
|
|
98888
99075
|
}
|
|
@@ -98901,7 +99088,7 @@ init_logger2();
|
|
|
98901
99088
|
init_detect2();
|
|
98902
99089
|
init_workspace();
|
|
98903
99090
|
init_common();
|
|
98904
|
-
import { join as
|
|
99091
|
+
import { join as join71 } from "path";
|
|
98905
99092
|
function resolveEffective(detected, configPatterns) {
|
|
98906
99093
|
if (configPatterns !== undefined)
|
|
98907
99094
|
return "config";
|
|
@@ -98986,7 +99173,7 @@ async function detectCommand(options) {
|
|
|
98986
99173
|
const rootDetected = detectionMap[""] ?? { patterns: [], confidence: "empty", sources: [] };
|
|
98987
99174
|
const pkgEntries = await Promise.all(packageDirs.map(async (dir) => {
|
|
98988
99175
|
const det = detectionMap[dir] ?? { patterns: [], confidence: "empty", sources: [] };
|
|
98989
|
-
const pkgConfigPath =
|
|
99176
|
+
const pkgConfigPath = join71(workdir, ".nax", "mono", dir, "config.json");
|
|
98990
99177
|
const pkgRaw = await loadRawConfig(pkgConfigPath);
|
|
98991
99178
|
const pkgPatterns = deepGet(pkgRaw, TEST_PATTERNS_KEY);
|
|
98992
99179
|
const effective = Array.isArray(pkgPatterns) ? pkgPatterns : undefined;
|
|
@@ -99040,13 +99227,13 @@ async function detectCommand(options) {
|
|
|
99040
99227
|
if (rootDetected.confidence === "empty") {
|
|
99041
99228
|
console.log(source_default.yellow(" root: skipped (empty detection)"));
|
|
99042
99229
|
} else {
|
|
99043
|
-
const rootConfigPath =
|
|
99230
|
+
const rootConfigPath = join71(workdir, ".nax", "config.json");
|
|
99044
99231
|
try {
|
|
99045
99232
|
const status = await applyToConfig(rootConfigPath, rootDetected.patterns, options.force ?? false);
|
|
99046
99233
|
if (status === "skipped") {
|
|
99047
99234
|
console.log(source_default.dim(" root: skipped (testFilePatterns already set; use --force to overwrite)"));
|
|
99048
99235
|
} else {
|
|
99049
|
-
console.log(source_default.green(` root: ${status} \u2192 ${
|
|
99236
|
+
console.log(source_default.green(` root: ${status} \u2192 ${join71(".nax", "config.json")}`));
|
|
99050
99237
|
}
|
|
99051
99238
|
} catch (err) {
|
|
99052
99239
|
console.error(source_default.red(` root: write failed \u2014 ${err.message}`));
|
|
@@ -99059,13 +99246,13 @@ async function detectCommand(options) {
|
|
|
99059
99246
|
console.log(source_default.dim(` ${dir}: skipped (empty detection)`));
|
|
99060
99247
|
continue;
|
|
99061
99248
|
}
|
|
99062
|
-
const pkgConfigPath =
|
|
99249
|
+
const pkgConfigPath = join71(workdir, ".nax", "mono", dir, "config.json");
|
|
99063
99250
|
try {
|
|
99064
99251
|
const status = await applyToConfig(pkgConfigPath, det.patterns, options.force ?? false);
|
|
99065
99252
|
if (status === "skipped") {
|
|
99066
99253
|
console.log(source_default.dim(` ${dir}: skipped (already set)`));
|
|
99067
99254
|
} else {
|
|
99068
|
-
console.log(source_default.green(` ${dir}: ${status} \u2192 ${
|
|
99255
|
+
console.log(source_default.green(` ${dir}: ${status} \u2192 ${join71(".nax", "mono", dir, "config.json")}`));
|
|
99069
99256
|
}
|
|
99070
99257
|
} catch (err) {
|
|
99071
99258
|
console.error(source_default.red(` ${dir}: write failed \u2014 ${err.message}`));
|
|
@@ -99082,20 +99269,20 @@ async function detectCommand(options) {
|
|
|
99082
99269
|
|
|
99083
99270
|
// src/commands/logs.ts
|
|
99084
99271
|
init_common();
|
|
99085
|
-
import { existsSync as
|
|
99086
|
-
import { join as
|
|
99272
|
+
import { existsSync as existsSync30 } from "fs";
|
|
99273
|
+
import { join as join74 } from "path";
|
|
99087
99274
|
|
|
99088
99275
|
// src/commands/logs-formatter.ts
|
|
99089
99276
|
init_source();
|
|
99090
99277
|
init_formatter();
|
|
99091
99278
|
import { readdirSync as readdirSync8 } from "fs";
|
|
99092
|
-
import { join as
|
|
99279
|
+
import { join as join73 } from "path";
|
|
99093
99280
|
|
|
99094
99281
|
// src/commands/logs-reader.ts
|
|
99095
99282
|
init_paths3();
|
|
99096
|
-
import { existsSync as
|
|
99283
|
+
import { existsSync as existsSync29, readdirSync as readdirSync7 } from "fs";
|
|
99097
99284
|
import { readdir as readdir3 } from "fs/promises";
|
|
99098
|
-
import { join as
|
|
99285
|
+
import { join as join72 } from "path";
|
|
99099
99286
|
var _logsReaderDeps = {
|
|
99100
99287
|
getRunsDir
|
|
99101
99288
|
};
|
|
@@ -99109,7 +99296,7 @@ async function resolveRunFileFromRegistry(runId) {
|
|
|
99109
99296
|
}
|
|
99110
99297
|
let matched = null;
|
|
99111
99298
|
for (const entry of entries) {
|
|
99112
|
-
const metaPath =
|
|
99299
|
+
const metaPath = join72(runsDir, entry, "meta.json");
|
|
99113
99300
|
try {
|
|
99114
99301
|
const meta3 = await Bun.file(metaPath).json();
|
|
99115
99302
|
if (meta3.runId === runId || meta3.runId.startsWith(runId)) {
|
|
@@ -99121,7 +99308,7 @@ async function resolveRunFileFromRegistry(runId) {
|
|
|
99121
99308
|
if (!matched) {
|
|
99122
99309
|
throw new Error(`Run not found in registry: ${runId}`);
|
|
99123
99310
|
}
|
|
99124
|
-
if (!
|
|
99311
|
+
if (!existsSync29(matched.eventsDir)) {
|
|
99125
99312
|
console.log(`Log directory unavailable for run: ${runId}`);
|
|
99126
99313
|
return null;
|
|
99127
99314
|
}
|
|
@@ -99131,14 +99318,14 @@ async function resolveRunFileFromRegistry(runId) {
|
|
|
99131
99318
|
return null;
|
|
99132
99319
|
}
|
|
99133
99320
|
const specificFile = files.find((f) => f === `${matched.runId}.jsonl`);
|
|
99134
|
-
return
|
|
99321
|
+
return join72(matched.eventsDir, specificFile ?? files[0]);
|
|
99135
99322
|
}
|
|
99136
99323
|
async function selectRunFile(runsDir) {
|
|
99137
99324
|
const files = readdirSync7(runsDir).filter((f) => f.endsWith(".jsonl") && f !== "latest.jsonl").sort().reverse();
|
|
99138
99325
|
if (files.length === 0) {
|
|
99139
99326
|
return null;
|
|
99140
99327
|
}
|
|
99141
|
-
return
|
|
99328
|
+
return join72(runsDir, files[0]);
|
|
99142
99329
|
}
|
|
99143
99330
|
async function extractRunSummary(filePath) {
|
|
99144
99331
|
const file3 = Bun.file(filePath);
|
|
@@ -99224,7 +99411,7 @@ Runs:
|
|
|
99224
99411
|
console.log(source_default.gray(" Timestamp Stories Duration Cost Status"));
|
|
99225
99412
|
console.log(source_default.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
|
|
99226
99413
|
for (const file3 of files) {
|
|
99227
|
-
const filePath =
|
|
99414
|
+
const filePath = join73(runsDir, file3);
|
|
99228
99415
|
const summary = await extractRunSummary(filePath);
|
|
99229
99416
|
const timestamp = file3.replace(".jsonl", "");
|
|
99230
99417
|
const stories = summary ? `${summary.passed}/${summary.total}` : "?/?";
|
|
@@ -99338,7 +99525,7 @@ async function logsCommand(options) {
|
|
|
99338
99525
|
return;
|
|
99339
99526
|
}
|
|
99340
99527
|
const resolved = resolveProject({ dir: options.dir });
|
|
99341
|
-
const naxDir =
|
|
99528
|
+
const naxDir = join74(resolved.projectDir, ".nax");
|
|
99342
99529
|
const configPath = resolved.configPath;
|
|
99343
99530
|
const configFile = Bun.file(configPath);
|
|
99344
99531
|
const config2 = await configFile.json();
|
|
@@ -99346,9 +99533,9 @@ async function logsCommand(options) {
|
|
|
99346
99533
|
if (!featureName) {
|
|
99347
99534
|
throw new Error("No feature specified in config.json");
|
|
99348
99535
|
}
|
|
99349
|
-
const featureDir =
|
|
99350
|
-
const runsDir =
|
|
99351
|
-
if (!
|
|
99536
|
+
const featureDir = join74(naxDir, "features", featureName);
|
|
99537
|
+
const runsDir = join74(featureDir, "runs");
|
|
99538
|
+
if (!existsSync30(runsDir)) {
|
|
99352
99539
|
throw new Error(`No runs directory found for feature: ${featureName}`);
|
|
99353
99540
|
}
|
|
99354
99541
|
if (options.list) {
|
|
@@ -99372,8 +99559,8 @@ init_config();
|
|
|
99372
99559
|
init_prd();
|
|
99373
99560
|
init_precheck();
|
|
99374
99561
|
init_common();
|
|
99375
|
-
import { existsSync as
|
|
99376
|
-
import { join as
|
|
99562
|
+
import { existsSync as existsSync31 } from "fs";
|
|
99563
|
+
import { join as join75 } from "path";
|
|
99377
99564
|
async function precheckCommand(options) {
|
|
99378
99565
|
const resolved = resolveProject({
|
|
99379
99566
|
dir: options.dir,
|
|
@@ -99395,14 +99582,14 @@ async function precheckCommand(options) {
|
|
|
99395
99582
|
process.exit(1);
|
|
99396
99583
|
}
|
|
99397
99584
|
}
|
|
99398
|
-
const naxDir =
|
|
99399
|
-
const featureDir =
|
|
99400
|
-
const prdPath =
|
|
99401
|
-
if (!
|
|
99585
|
+
const naxDir = join75(resolved.projectDir, ".nax");
|
|
99586
|
+
const featureDir = join75(naxDir, "features", featureName);
|
|
99587
|
+
const prdPath = join75(featureDir, "prd.json");
|
|
99588
|
+
if (!existsSync31(featureDir)) {
|
|
99402
99589
|
console.error(source_default.red(`Feature not found: ${featureName}`));
|
|
99403
99590
|
process.exit(1);
|
|
99404
99591
|
}
|
|
99405
|
-
if (!
|
|
99592
|
+
if (!existsSync31(prdPath)) {
|
|
99406
99593
|
console.error(source_default.red(`Missing prd.json for feature: ${featureName}`));
|
|
99407
99594
|
console.error(source_default.dim(`Run: nax plan -f ${featureName} --from spec.md --auto`));
|
|
99408
99595
|
process.exit(EXIT_CODES.INVALID_PRD);
|
|
@@ -99420,7 +99607,7 @@ async function precheckCommand(options) {
|
|
|
99420
99607
|
init_source();
|
|
99421
99608
|
init_paths3();
|
|
99422
99609
|
import { readdir as readdir4 } from "fs/promises";
|
|
99423
|
-
import { join as
|
|
99610
|
+
import { join as join76 } from "path";
|
|
99424
99611
|
var DEFAULT_LIMIT = 20;
|
|
99425
99612
|
var _runsCmdDeps = {
|
|
99426
99613
|
getRunsDir
|
|
@@ -99475,7 +99662,7 @@ async function runsCommand(options = {}) {
|
|
|
99475
99662
|
}
|
|
99476
99663
|
const rows = [];
|
|
99477
99664
|
for (const entry of entries) {
|
|
99478
|
-
const metaPath =
|
|
99665
|
+
const metaPath = join76(runsDir, entry, "meta.json");
|
|
99479
99666
|
let meta3;
|
|
99480
99667
|
try {
|
|
99481
99668
|
meta3 = await Bun.file(metaPath).json();
|
|
@@ -99552,7 +99739,7 @@ async function runsCommand(options = {}) {
|
|
|
99552
99739
|
|
|
99553
99740
|
// src/commands/unlock.ts
|
|
99554
99741
|
init_source();
|
|
99555
|
-
import { join as
|
|
99742
|
+
import { join as join77 } from "path";
|
|
99556
99743
|
function isProcessAlive2(pid) {
|
|
99557
99744
|
try {
|
|
99558
99745
|
process.kill(pid, 0);
|
|
@@ -99567,7 +99754,7 @@ function formatLockAge(ageMs) {
|
|
|
99567
99754
|
}
|
|
99568
99755
|
async function unlockCommand(options) {
|
|
99569
99756
|
const workdir = options.dir ?? process.cwd();
|
|
99570
|
-
const lockPath =
|
|
99757
|
+
const lockPath = join77(workdir, "nax.lock");
|
|
99571
99758
|
const lockFile = Bun.file(lockPath);
|
|
99572
99759
|
const exists = await lockFile.exists();
|
|
99573
99760
|
if (!exists) {
|
|
@@ -108077,8 +108264,8 @@ Next: nax generate --package ${options.package}`));
|
|
|
108077
108264
|
}
|
|
108078
108265
|
return;
|
|
108079
108266
|
}
|
|
108080
|
-
const naxDir =
|
|
108081
|
-
if (
|
|
108267
|
+
const naxDir = join91(workdir, ".nax");
|
|
108268
|
+
if (existsSync37(naxDir) && !options.force) {
|
|
108082
108269
|
console.log(source_default.yellow("nax already initialized. Use --force to overwrite."));
|
|
108083
108270
|
return;
|
|
108084
108271
|
}
|
|
@@ -108106,11 +108293,11 @@ Next: nax generate --package ${options.package}`));
|
|
|
108106
108293
|
}
|
|
108107
108294
|
}
|
|
108108
108295
|
}
|
|
108109
|
-
mkdirSync7(
|
|
108110
|
-
mkdirSync7(
|
|
108296
|
+
mkdirSync7(join91(naxDir, "features"), { recursive: true });
|
|
108297
|
+
mkdirSync7(join91(naxDir, "hooks"), { recursive: true });
|
|
108111
108298
|
const initConfig = options.name ? { ...DEFAULT_CONFIG, name: options.name } : DEFAULT_CONFIG;
|
|
108112
|
-
await Bun.write(
|
|
108113
|
-
await Bun.write(
|
|
108299
|
+
await Bun.write(join91(naxDir, "config.json"), JSON.stringify(initConfig, null, 2));
|
|
108300
|
+
await Bun.write(join91(naxDir, "hooks.json"), JSON.stringify({
|
|
108114
108301
|
hooks: {
|
|
108115
108302
|
"on-start": { command: 'echo "nax started: $NAX_FEATURE"', enabled: false },
|
|
108116
108303
|
"on-complete": { command: 'echo "nax complete: $NAX_FEATURE"', enabled: false },
|
|
@@ -108118,12 +108305,12 @@ Next: nax generate --package ${options.package}`));
|
|
|
108118
108305
|
"on-error": { command: 'echo "nax error: $NAX_REASON"', enabled: false }
|
|
108119
108306
|
}
|
|
108120
108307
|
}, null, 2));
|
|
108121
|
-
await Bun.write(
|
|
108308
|
+
await Bun.write(join91(naxDir, ".gitignore"), `# nax temp files
|
|
108122
108309
|
*.tmp
|
|
108123
108310
|
.paused.json
|
|
108124
108311
|
.nax-verifier-verdict.json
|
|
108125
108312
|
`);
|
|
108126
|
-
await Bun.write(
|
|
108313
|
+
await Bun.write(join91(naxDir, "context.md"), `# Project Context
|
|
108127
108314
|
|
|
108128
108315
|
This document defines coding standards, architectural decisions, and forbidden patterns for this project.
|
|
108129
108316
|
Run \`nax generate\` to regenerate agent config files (CLAUDE.md, AGENTS.md, .cursorrules, etc.) from this file.
|
|
@@ -108238,7 +108425,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
108238
108425
|
console.error(source_default.red("Error: --plan requires --from <spec-path>"));
|
|
108239
108426
|
process.exit(1);
|
|
108240
108427
|
}
|
|
108241
|
-
if (options.from && !
|
|
108428
|
+
if (options.from && !existsSync37(options.from)) {
|
|
108242
108429
|
console.error(source_default.red(`Error: File not found: ${options.from} (required with --plan)`));
|
|
108243
108430
|
process.exit(1);
|
|
108244
108431
|
}
|
|
@@ -108265,7 +108452,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
108265
108452
|
const cliOverrides = {};
|
|
108266
108453
|
const cliProfiles = options.profile ?? [];
|
|
108267
108454
|
const profileOverride = naxDir ? await resolveRunProfileOverride({
|
|
108268
|
-
prdPath:
|
|
108455
|
+
prdPath: join91(naxDir, "features", options.feature, "prd.json"),
|
|
108269
108456
|
projectRoot: workdir,
|
|
108270
108457
|
cliProfile: cliProfiles,
|
|
108271
108458
|
envProfile: process.env.NAX_PROFILE
|
|
@@ -108278,10 +108465,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
108278
108465
|
console.error(source_default.red("nax not initialized. Run: nax init"));
|
|
108279
108466
|
process.exit(1);
|
|
108280
108467
|
}
|
|
108281
|
-
const featureDir =
|
|
108282
|
-
const prdPath =
|
|
108468
|
+
const featureDir = join91(naxDir, "features", options.feature);
|
|
108469
|
+
const prdPath = join91(featureDir, "prd.json");
|
|
108283
108470
|
if (options.plan && options.from) {
|
|
108284
|
-
if (
|
|
108471
|
+
if (existsSync37(prdPath) && !options.force) {
|
|
108285
108472
|
console.error(source_default.red(`Error: prd.json already exists for feature "${options.feature}".`));
|
|
108286
108473
|
console.error(source_default.dim(" Use --force to overwrite, or run without --plan to use the existing PRD."));
|
|
108287
108474
|
process.exit(1);
|
|
@@ -108301,10 +108488,10 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
108301
108488
|
}
|
|
108302
108489
|
}
|
|
108303
108490
|
try {
|
|
108304
|
-
const planLogDir =
|
|
108491
|
+
const planLogDir = join91(featureDir, "plan");
|
|
108305
108492
|
mkdirSync7(planLogDir, { recursive: true });
|
|
108306
108493
|
const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
|
|
108307
|
-
const planLogPath =
|
|
108494
|
+
const planLogPath = join91(planLogDir, `${planLogId}.jsonl`);
|
|
108308
108495
|
initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
|
|
108309
108496
|
console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
|
|
108310
108497
|
console.log(source_default.dim(" [Planning phase: generating PRD from spec]"));
|
|
@@ -108343,17 +108530,17 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
108343
108530
|
process.exit(1);
|
|
108344
108531
|
}
|
|
108345
108532
|
}
|
|
108346
|
-
if (!
|
|
108533
|
+
if (!existsSync37(prdPath)) {
|
|
108347
108534
|
console.error(source_default.red(`Feature "${options.feature}" not found or missing prd.json`));
|
|
108348
108535
|
process.exit(1);
|
|
108349
108536
|
}
|
|
108350
108537
|
resetLogger();
|
|
108351
108538
|
const projectKey = config2.name?.trim() || basename16(workdir);
|
|
108352
108539
|
const outputDir = projectOutputDir(projectKey, config2.outputDir);
|
|
108353
|
-
const runsDir =
|
|
108540
|
+
const runsDir = join91(outputDir, "features", options.feature, "runs");
|
|
108354
108541
|
mkdirSync7(runsDir, { recursive: true });
|
|
108355
108542
|
const runId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
|
|
108356
|
-
const logFilePath =
|
|
108543
|
+
const logFilePath = join91(runsDir, `${runId}.jsonl`);
|
|
108357
108544
|
const isTTY = process.stdout.isTTY ?? false;
|
|
108358
108545
|
const headlessFlag = options.headless ?? false;
|
|
108359
108546
|
const headlessEnv = process.env.NAX_HEADLESS === "1";
|
|
@@ -108371,7 +108558,7 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
108371
108558
|
config2.agent.default = options.agent;
|
|
108372
108559
|
}
|
|
108373
108560
|
config2.execution.maxIterations = Number.parseInt(options.maxIterations, 10);
|
|
108374
|
-
const globalNaxDir =
|
|
108561
|
+
const globalNaxDir = join91(homedir3(), ".nax");
|
|
108375
108562
|
const hooks = await loadHooksConfig(naxDir, globalNaxDir);
|
|
108376
108563
|
const eventEmitter = new PipelineEventEmitter;
|
|
108377
108564
|
const agentStreamEvents = useHeadless ? undefined : new AgentStreamEventBus;
|
|
@@ -108391,12 +108578,12 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
108391
108578
|
events: eventEmitter,
|
|
108392
108579
|
ptyOptions: null,
|
|
108393
108580
|
agentStreamEvents,
|
|
108394
|
-
queueFilePath:
|
|
108581
|
+
queueFilePath: join91(workdir, ".queue.txt")
|
|
108395
108582
|
});
|
|
108396
108583
|
} else {
|
|
108397
108584
|
console.log(source_default.dim(" [Headless mode \u2014 pipe output]"));
|
|
108398
108585
|
}
|
|
108399
|
-
const statusFilePath =
|
|
108586
|
+
const statusFilePath = join91(outputDir, "status.json");
|
|
108400
108587
|
let parallel;
|
|
108401
108588
|
if (options.parallel !== undefined) {
|
|
108402
108589
|
parallel = Number.parseInt(options.parallel, 10);
|
|
@@ -108423,9 +108610,9 @@ program2.command("run").description("Run the orchestration loop for a feature").
|
|
|
108423
108610
|
skipPrecheck: options.skipPrecheck ?? false,
|
|
108424
108611
|
agentStreamEvents
|
|
108425
108612
|
});
|
|
108426
|
-
const latestSymlink =
|
|
108613
|
+
const latestSymlink = join91(runsDir, "latest.jsonl");
|
|
108427
108614
|
try {
|
|
108428
|
-
if (
|
|
108615
|
+
if (existsSync37(latestSymlink)) {
|
|
108429
108616
|
Bun.spawnSync(["rm", latestSymlink]);
|
|
108430
108617
|
}
|
|
108431
108618
|
Bun.spawnSync(["ln", "-s", `${runId}.jsonl`, latestSymlink], {
|
|
@@ -108520,9 +108707,9 @@ features.command("create <name>").description("Create a new feature").option("-d
|
|
|
108520
108707
|
console.error(source_default.red("nax not initialized. Run: nax init"));
|
|
108521
108708
|
process.exit(1);
|
|
108522
108709
|
}
|
|
108523
|
-
const featureDir =
|
|
108710
|
+
const featureDir = join91(naxDir, "features", name);
|
|
108524
108711
|
mkdirSync7(featureDir, { recursive: true });
|
|
108525
|
-
await Bun.write(
|
|
108712
|
+
await Bun.write(join91(featureDir, "spec.md"), `# Feature: ${name}
|
|
108526
108713
|
|
|
108527
108714
|
## Overview
|
|
108528
108715
|
|
|
@@ -108555,7 +108742,7 @@ features.command("create <name>").description("Create a new feature").option("-d
|
|
|
108555
108742
|
|
|
108556
108743
|
<!-- What this feature explicitly does NOT cover. -->
|
|
108557
108744
|
`);
|
|
108558
|
-
await Bun.write(
|
|
108745
|
+
await Bun.write(join91(featureDir, "progress.txt"), `# Progress: ${name}
|
|
108559
108746
|
|
|
108560
108747
|
Created: ${new Date().toISOString()}
|
|
108561
108748
|
|
|
@@ -108581,8 +108768,8 @@ features.command("list").description("List all features").option("-d, --dir <pat
|
|
|
108581
108768
|
console.error(source_default.red("nax not initialized."));
|
|
108582
108769
|
process.exit(1);
|
|
108583
108770
|
}
|
|
108584
|
-
const featuresDir =
|
|
108585
|
-
if (!
|
|
108771
|
+
const featuresDir = join91(naxDir, "features");
|
|
108772
|
+
if (!existsSync37(featuresDir)) {
|
|
108586
108773
|
console.log(source_default.dim("No features yet."));
|
|
108587
108774
|
return;
|
|
108588
108775
|
}
|
|
@@ -108596,8 +108783,8 @@ features.command("list").description("List all features").option("-d, --dir <pat
|
|
|
108596
108783
|
Features:
|
|
108597
108784
|
`));
|
|
108598
108785
|
for (const name of entries) {
|
|
108599
|
-
const prdPath =
|
|
108600
|
-
if (
|
|
108786
|
+
const prdPath = join91(featuresDir, name, "prd.json");
|
|
108787
|
+
if (existsSync37(prdPath)) {
|
|
108601
108788
|
const prd = await loadPRD(prdPath);
|
|
108602
108789
|
const c = countStories(prd);
|
|
108603
108790
|
console.log(` ${name} \u2014 ${c.passed}/${c.total} stories done`);
|
|
@@ -108669,10 +108856,10 @@ Use: nax plan -f <feature> --from <spec>`));
|
|
|
108669
108856
|
cliOverrides.profile = cliProfiles;
|
|
108670
108857
|
}
|
|
108671
108858
|
const config2 = await loadConfig(workdir, cliOverrides);
|
|
108672
|
-
const featureLogDir =
|
|
108859
|
+
const featureLogDir = join91(naxDir, "features", options.feature, "plan");
|
|
108673
108860
|
mkdirSync7(featureLogDir, { recursive: true });
|
|
108674
108861
|
const planLogId = new Date().toISOString().replace(/:/g, "-").replace(/\..+/, "");
|
|
108675
|
-
const planLogPath =
|
|
108862
|
+
const planLogPath = join91(featureLogDir, `${planLogId}.jsonl`);
|
|
108676
108863
|
initLogger({ level: "info", filePath: planLogPath, useChalk: false, headless: true });
|
|
108677
108864
|
console.log(source_default.dim(` [Plan log: ${planLogPath}]`));
|
|
108678
108865
|
try {
|