@nathapp/nax 0.56.5 → 0.57.1-canary.1
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 +558 -42
- package/package.json +1 -1
package/dist/nax.js
CHANGED
|
@@ -17991,7 +17991,13 @@ var init_defaults = __esm(() => {
|
|
|
17991
17991
|
model: "fast",
|
|
17992
17992
|
refinement: true,
|
|
17993
17993
|
redGate: true,
|
|
17994
|
-
timeoutMs: 1800000
|
|
17994
|
+
timeoutMs: 1800000,
|
|
17995
|
+
fix: {
|
|
17996
|
+
diagnoseModel: "fast",
|
|
17997
|
+
fixModel: "balanced",
|
|
17998
|
+
strategy: "diagnose-first",
|
|
17999
|
+
maxRetries: 2
|
|
18000
|
+
}
|
|
17995
18001
|
},
|
|
17996
18002
|
context: {
|
|
17997
18003
|
fileInjection: "disabled",
|
|
@@ -18042,31 +18048,36 @@ var init_defaults = __esm(() => {
|
|
|
18042
18048
|
enabled: true,
|
|
18043
18049
|
resolver: { type: "synthesis" },
|
|
18044
18050
|
sessionMode: "stateful",
|
|
18045
|
-
rounds: 3
|
|
18051
|
+
rounds: 3,
|
|
18052
|
+
timeoutSeconds: 600
|
|
18046
18053
|
},
|
|
18047
18054
|
review: {
|
|
18048
18055
|
enabled: true,
|
|
18049
18056
|
resolver: { type: "majority-fail-closed" },
|
|
18050
18057
|
sessionMode: "one-shot",
|
|
18051
|
-
rounds: 2
|
|
18058
|
+
rounds: 2,
|
|
18059
|
+
timeoutSeconds: 600
|
|
18052
18060
|
},
|
|
18053
18061
|
acceptance: {
|
|
18054
18062
|
enabled: false,
|
|
18055
18063
|
resolver: { type: "majority-fail-closed" },
|
|
18056
18064
|
sessionMode: "one-shot",
|
|
18057
|
-
rounds: 1
|
|
18065
|
+
rounds: 1,
|
|
18066
|
+
timeoutSeconds: 600
|
|
18058
18067
|
},
|
|
18059
18068
|
rectification: {
|
|
18060
18069
|
enabled: false,
|
|
18061
18070
|
resolver: { type: "synthesis" },
|
|
18062
18071
|
sessionMode: "one-shot",
|
|
18063
|
-
rounds: 1
|
|
18072
|
+
rounds: 1,
|
|
18073
|
+
timeoutSeconds: 600
|
|
18064
18074
|
},
|
|
18065
18075
|
escalation: {
|
|
18066
18076
|
enabled: false,
|
|
18067
18077
|
resolver: { type: "majority-fail-closed" },
|
|
18068
18078
|
sessionMode: "one-shot",
|
|
18069
|
-
rounds: 1
|
|
18079
|
+
rounds: 1,
|
|
18080
|
+
timeoutSeconds: 600
|
|
18070
18081
|
}
|
|
18071
18082
|
}
|
|
18072
18083
|
}
|
|
@@ -18086,7 +18097,7 @@ function isLegacyFlatModels(val) {
|
|
|
18086
18097
|
}
|
|
18087
18098
|
return false;
|
|
18088
18099
|
}
|
|
18089
|
-
var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, PerAgentModelMapSchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, SemanticReviewConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, ProjectProfileSchema, VALID_AGENT_TYPES, GenerateConfigSchema, DebaterSchema, toObject = (val) => val === undefined || val === null ? {} : val, RESOLVER_TYPES, makeResolverSchema = (defaultType) => exports_external.preprocess(toObject, exports_external.object({
|
|
18100
|
+
var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, PerAgentModelMapSchema, ModelMapSchema, ModelTierSchema, TierConfigSchema, AutoModeConfigSchema, RectificationConfigSchema, RegressionGateConfigSchema, SmartTestRunnerConfigSchema, SMART_TEST_RUNNER_DEFAULT, smartTestRunnerFieldSchema, ExecutionConfigSchema, QualityConfigSchema, TddConfigSchema, ConstitutionConfigSchema, AnalyzeConfigSchema, SemanticReviewConfigSchema, ReviewConfigSchema, PlanConfigSchema, AcceptanceFixConfigSchema, AcceptanceConfigSchema, TestCoverageConfigSchema, ContextAutoDetectConfigSchema, ContextConfigSchema, LlmRoutingConfigSchema, RoutingConfigSchema, OptimizerConfigSchema, PluginConfigEntrySchema, HooksConfigSchema, InteractionConfigSchema, StorySizeGateConfigSchema, AgentConfigSchema, PrecheckConfigSchema, PromptsConfigSchema, ProjectProfileSchema, VALID_AGENT_TYPES, GenerateConfigSchema, DebaterSchema, toObject = (val) => val === undefined || val === null ? {} : val, RESOLVER_TYPES, makeResolverSchema = (defaultType) => exports_external.preprocess(toObject, exports_external.object({
|
|
18090
18101
|
type: exports_external.enum(RESOLVER_TYPES).default(defaultType),
|
|
18091
18102
|
agent: exports_external.string().min(1).optional(),
|
|
18092
18103
|
tieBreaker: exports_external.string().min(1).optional(),
|
|
@@ -18096,7 +18107,8 @@ var TokenPricingSchema, ModelDefSchema, ModelEntrySchema, PerAgentModelMapSchema
|
|
|
18096
18107
|
resolver: makeResolverSchema(defaults.resolverType),
|
|
18097
18108
|
sessionMode: exports_external.enum(["one-shot", "stateful"]).default(defaults.sessionMode),
|
|
18098
18109
|
rounds: exports_external.number().int().min(1).default(defaults.rounds),
|
|
18099
|
-
debaters: exports_external.array(DebaterSchema).min(2, "debaters must have at least 2 entries").optional()
|
|
18110
|
+
debaters: exports_external.array(DebaterSchema).min(2, "debaters must have at least 2 entries").optional(),
|
|
18111
|
+
timeoutSeconds: exports_external.number().int().positive().default(600)
|
|
18100
18112
|
})), DebateConfigSchema, NaxConfigSchema;
|
|
18101
18113
|
var init_schemas3 = __esm(() => {
|
|
18102
18114
|
init_zod();
|
|
@@ -18297,7 +18309,14 @@ var init_schemas3 = __esm(() => {
|
|
|
18297
18309
|
});
|
|
18298
18310
|
PlanConfigSchema = exports_external.object({
|
|
18299
18311
|
model: ModelTierSchema,
|
|
18300
|
-
outputPath: exports_external.string().min(1, "plan.outputPath must be non-empty")
|
|
18312
|
+
outputPath: exports_external.string().min(1, "plan.outputPath must be non-empty"),
|
|
18313
|
+
timeoutSeconds: exports_external.number().int().positive().default(600)
|
|
18314
|
+
});
|
|
18315
|
+
AcceptanceFixConfigSchema = exports_external.object({
|
|
18316
|
+
diagnoseModel: exports_external.string().min(1, "acceptance.fix.diagnoseModel must be non-empty"),
|
|
18317
|
+
fixModel: exports_external.string().min(1, "acceptance.fix.fixModel must be non-empty"),
|
|
18318
|
+
strategy: exports_external.enum(["diagnose-first", "implement-only"]),
|
|
18319
|
+
maxRetries: exports_external.number().int().nonnegative()
|
|
18301
18320
|
});
|
|
18302
18321
|
AcceptanceConfigSchema = exports_external.object({
|
|
18303
18322
|
enabled: exports_external.boolean(),
|
|
@@ -18310,7 +18329,13 @@ var init_schemas3 = __esm(() => {
|
|
|
18310
18329
|
redGate: exports_external.boolean().default(true),
|
|
18311
18330
|
testStrategy: exports_external.enum(["unit", "component", "cli", "e2e", "snapshot"]).optional(),
|
|
18312
18331
|
testFramework: exports_external.string().min(1, "acceptance.testFramework must be non-empty").optional(),
|
|
18313
|
-
timeoutMs: exports_external.number().int().min(30000).max(3600000).default(1800000)
|
|
18332
|
+
timeoutMs: exports_external.number().int().min(30000).max(3600000).default(1800000),
|
|
18333
|
+
fix: AcceptanceFixConfigSchema.optional().default({
|
|
18334
|
+
diagnoseModel: "fast",
|
|
18335
|
+
fixModel: "balanced",
|
|
18336
|
+
strategy: "diagnose-first",
|
|
18337
|
+
maxRetries: 2
|
|
18338
|
+
})
|
|
18314
18339
|
});
|
|
18315
18340
|
TestCoverageConfigSchema = exports_external.object({
|
|
18316
18341
|
enabled: exports_external.boolean().default(true),
|
|
@@ -18982,7 +19007,8 @@ class AcpAgentAdapter {
|
|
|
18982
19007
|
const tryOneAgent = async (agentName) => {
|
|
18983
19008
|
const model = await resolveModel2(agentName);
|
|
18984
19009
|
const cmdStr = `acpx --model ${model} ${agentName}`;
|
|
18985
|
-
const
|
|
19010
|
+
const timeoutSeconds = Math.ceil(timeoutMs / 1000);
|
|
19011
|
+
const client = _acpAdapterDeps.createClient(cmdStr, workdir, timeoutSeconds);
|
|
18986
19012
|
await client.start();
|
|
18987
19013
|
let session = null;
|
|
18988
19014
|
let hadError = false;
|
|
@@ -19827,9 +19853,9 @@ async function runPlan(binary, options, pidRegistry, buildAllowedEnv2) {
|
|
|
19827
19853
|
modelDef,
|
|
19828
19854
|
prompt: "",
|
|
19829
19855
|
modelTier: options.modelTier || "balanced",
|
|
19830
|
-
timeoutSeconds: 600
|
|
19856
|
+
timeoutSeconds: options.timeoutSeconds ?? 600
|
|
19831
19857
|
};
|
|
19832
|
-
const
|
|
19858
|
+
const planTimeoutMs = (options.timeoutSeconds ?? 600) * 1000;
|
|
19833
19859
|
if (options.interactive) {
|
|
19834
19860
|
const proc = Bun.spawn(cmd, {
|
|
19835
19861
|
cwd: options.workdir,
|
|
@@ -19841,7 +19867,7 @@ async function runPlan(binary, options, pidRegistry, buildAllowedEnv2) {
|
|
|
19841
19867
|
await pidRegistry.register(proc.pid);
|
|
19842
19868
|
let exitCode;
|
|
19843
19869
|
try {
|
|
19844
|
-
const timeoutResult = await withProcessTimeout(proc,
|
|
19870
|
+
const timeoutResult = await withProcessTimeout(proc, planTimeoutMs, {
|
|
19845
19871
|
graceMs: 5000
|
|
19846
19872
|
});
|
|
19847
19873
|
exitCode = timeoutResult.exitCode;
|
|
@@ -19867,7 +19893,7 @@ async function runPlan(binary, options, pidRegistry, buildAllowedEnv2) {
|
|
|
19867
19893
|
await pidRegistry.register(proc.pid);
|
|
19868
19894
|
let exitCode;
|
|
19869
19895
|
try {
|
|
19870
|
-
const timeoutResult = await withProcessTimeout(proc,
|
|
19896
|
+
const timeoutResult = await withProcessTimeout(proc, planTimeoutMs, {
|
|
19871
19897
|
graceMs: 5000
|
|
19872
19898
|
});
|
|
19873
19899
|
exitCode = timeoutResult.exitCode;
|
|
@@ -20909,7 +20935,8 @@ async function refineAcceptanceCriteria(criteria, context) {
|
|
|
20909
20935
|
featureName,
|
|
20910
20936
|
storyId,
|
|
20911
20937
|
workdir,
|
|
20912
|
-
sessionRole: "refine"
|
|
20938
|
+
sessionRole: "refine",
|
|
20939
|
+
timeoutMs: config2.acceptance?.timeoutMs ?? 120000
|
|
20913
20940
|
});
|
|
20914
20941
|
response = typeof completeResult === "string" ? completeResult : completeResult.output;
|
|
20915
20942
|
} catch (error48) {
|
|
@@ -20958,6 +20985,7 @@ var init_refinement = __esm(() => {
|
|
|
20958
20985
|
// src/acceptance/generator.ts
|
|
20959
20986
|
var exports_generator = {};
|
|
20960
20987
|
__export(exports_generator, {
|
|
20988
|
+
resolveAcceptanceTestFile: () => resolveAcceptanceTestFile,
|
|
20961
20989
|
parseAcceptanceCriteria: () => parseAcceptanceCriteria,
|
|
20962
20990
|
generateSkeletonTests: () => generateSkeletonTests,
|
|
20963
20991
|
generateFromPRD: () => generateFromPRD,
|
|
@@ -20993,6 +21021,9 @@ function acceptanceTestFilename(language) {
|
|
|
20993
21021
|
return ".nax-acceptance.test.ts";
|
|
20994
21022
|
}
|
|
20995
21023
|
}
|
|
21024
|
+
function resolveAcceptanceTestFile(language, testPathConfig) {
|
|
21025
|
+
return testPathConfig ?? acceptanceTestFilename(language);
|
|
21026
|
+
}
|
|
20996
21027
|
function buildAcceptanceRunCommand(testPath, testFramework, commandOverride) {
|
|
20997
21028
|
if (commandOverride) {
|
|
20998
21029
|
const resolved = commandOverride.replace(/\{\{files\}\}/g, testPath).replace(/\{\{file\}\}/g, testPath).replace(/\{\{FILE\}\}/g, testPath);
|
|
@@ -21061,7 +21092,7 @@ Rules:
|
|
|
21061
21092
|
- Every test MUST have real assertions that PASS when the feature is correctly implemented and FAIL when it is broken
|
|
21062
21093
|
- **Prefer behavioral tests** \u2014 import functions and call them rather than reading source files. For example, to verify "getPostRunActions() returns empty array", import PluginRegistry and call getPostRunActions(), don't grep the source file for the method name.
|
|
21063
21094
|
- **File output (REQUIRED)**: Write the acceptance test file DIRECTLY to the path shown below. Do NOT output the test code in your response. After writing the file, reply with a brief confirmation.
|
|
21064
|
-
- **Path anchor (CRITICAL)**: Write the test file to this exact path: \`${join5(options.workdir, ".nax", "features", options.featureName,
|
|
21095
|
+
- **Path anchor (CRITICAL)**: Write the test file to this exact path: \`${join5(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile(options.language, options.config?.acceptance?.testPath))}\`. Import from package sources using relative paths like \`../../../src/...\` (3 levels up from \`.nax/features/<name>/\` to the package root).`;
|
|
21065
21096
|
const prompt = basePrompt;
|
|
21066
21097
|
logger.info("acceptance", "Generating tests from PRD refined criteria", { count: refinedCriteria.length });
|
|
21067
21098
|
const completeResult = await (options.adapter ?? _generatorPRDDeps.adapter).complete(prompt, {
|
|
@@ -21080,7 +21111,7 @@ Rules:
|
|
|
21080
21111
|
outputPreview: rawOutput.slice(0, 300)
|
|
21081
21112
|
});
|
|
21082
21113
|
if (!testCode) {
|
|
21083
|
-
const targetPath = join5(options.workdir, ".nax", "features", options.featureName,
|
|
21114
|
+
const targetPath = join5(options.workdir, ".nax", "features", options.featureName, resolveAcceptanceTestFile(options.language, options.config?.acceptance?.testPath));
|
|
21084
21115
|
let recoveryFailed = false;
|
|
21085
21116
|
logger.debug("acceptance", "BUG-076 recovery: checking for agent-written file", { targetPath });
|
|
21086
21117
|
try {
|
|
@@ -22132,7 +22163,7 @@ var package_default;
|
|
|
22132
22163
|
var init_package = __esm(() => {
|
|
22133
22164
|
package_default = {
|
|
22134
22165
|
name: "@nathapp/nax",
|
|
22135
|
-
version: "0.
|
|
22166
|
+
version: "0.57.1-canary.1",
|
|
22136
22167
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
22137
22168
|
type: "module",
|
|
22138
22169
|
bin: {
|
|
@@ -22211,8 +22242,8 @@ var init_version = __esm(() => {
|
|
|
22211
22242
|
NAX_VERSION = package_default.version;
|
|
22212
22243
|
NAX_COMMIT = (() => {
|
|
22213
22244
|
try {
|
|
22214
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
22215
|
-
return "
|
|
22245
|
+
if (/^[0-9a-f]{6,10}$/.test("814ed29f"))
|
|
22246
|
+
return "814ed29f";
|
|
22216
22247
|
} catch {}
|
|
22217
22248
|
try {
|
|
22218
22249
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -22499,10 +22530,11 @@ function modelTierFromDebater(debater) {
|
|
|
22499
22530
|
function isTierLabel(value) {
|
|
22500
22531
|
return value === "fast" || value === "balanced" || value === "powerful";
|
|
22501
22532
|
}
|
|
22502
|
-
async function runComplete(adapter, prompt, options, modelTier) {
|
|
22533
|
+
async function runComplete(adapter, prompt, options, modelTier, timeoutMs) {
|
|
22503
22534
|
return adapter.complete(prompt, {
|
|
22504
22535
|
...options,
|
|
22505
|
-
modelTier
|
|
22536
|
+
modelTier,
|
|
22537
|
+
...timeoutMs !== undefined && { timeoutMs }
|
|
22506
22538
|
});
|
|
22507
22539
|
}
|
|
22508
22540
|
|
|
@@ -22514,6 +22546,9 @@ class DebateSession {
|
|
|
22514
22546
|
workdir;
|
|
22515
22547
|
featureName;
|
|
22516
22548
|
timeoutSeconds;
|
|
22549
|
+
get timeoutMs() {
|
|
22550
|
+
return this.timeoutSeconds * 1000;
|
|
22551
|
+
}
|
|
22517
22552
|
constructor(opts) {
|
|
22518
22553
|
this.storyId = opts.storyId;
|
|
22519
22554
|
this.stage = opts.stage;
|
|
@@ -22521,7 +22556,7 @@ class DebateSession {
|
|
|
22521
22556
|
this.config = opts.config;
|
|
22522
22557
|
this.workdir = opts.workdir ?? process.cwd();
|
|
22523
22558
|
this.featureName = opts.featureName ?? opts.stage;
|
|
22524
|
-
this.timeoutSeconds = opts.timeoutSeconds ?? opts.
|
|
22559
|
+
this.timeoutSeconds = opts.timeoutSeconds ?? opts.stageConfig.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS;
|
|
22525
22560
|
}
|
|
22526
22561
|
pipelineStageForDebate() {
|
|
22527
22562
|
switch (this.stage) {
|
|
@@ -22761,7 +22796,8 @@ class DebateSession {
|
|
|
22761
22796
|
featureName: this.stage,
|
|
22762
22797
|
config: this.config,
|
|
22763
22798
|
storyId: this.storyId,
|
|
22764
|
-
sessionRole: "debate-proposal"
|
|
22799
|
+
sessionRole: "debate-proposal",
|
|
22800
|
+
timeoutMs: this.timeoutMs
|
|
22765
22801
|
}, modelTierFromDebater(debater)).then((result) => ({ debater, adapter, output: result.output, cost: result.costUsd }))));
|
|
22766
22802
|
const successful = proposalSettled.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
22767
22803
|
for (const r of proposalSettled) {
|
|
@@ -22815,7 +22851,8 @@ class DebateSession {
|
|
|
22815
22851
|
featureName: this.stage,
|
|
22816
22852
|
config: this.config,
|
|
22817
22853
|
storyId: this.storyId,
|
|
22818
|
-
sessionRole: "debate-fallback"
|
|
22854
|
+
sessionRole: "debate-fallback",
|
|
22855
|
+
timeoutMs: this.timeoutMs
|
|
22819
22856
|
}, modelTierFromDebater(fallbackDebater));
|
|
22820
22857
|
totalCostUsd += fallbackResult.costUsd;
|
|
22821
22858
|
logger?.info("debate", "debate:result", {
|
|
@@ -22845,7 +22882,8 @@ class DebateSession {
|
|
|
22845
22882
|
featureName: this.stage,
|
|
22846
22883
|
config: this.config,
|
|
22847
22884
|
storyId: this.storyId,
|
|
22848
|
-
sessionRole: "debate-critique"
|
|
22885
|
+
sessionRole: "debate-critique",
|
|
22886
|
+
timeoutMs: this.timeoutMs
|
|
22849
22887
|
}, modelTierFromDebater(debater))));
|
|
22850
22888
|
for (const r of critiqueSettled) {
|
|
22851
22889
|
if (r.status === "fulfilled") {
|
|
@@ -22988,7 +23026,8 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
|
|
|
22988
23026
|
model: resolveDebaterModel({ agent: agentName }, this.config),
|
|
22989
23027
|
config: this.config,
|
|
22990
23028
|
storyId: this.storyId,
|
|
22991
|
-
sessionRole: "synthesis"
|
|
23029
|
+
sessionRole: "synthesis",
|
|
23030
|
+
timeoutMs: this.timeoutMs
|
|
22992
23031
|
}
|
|
22993
23032
|
});
|
|
22994
23033
|
return {
|
|
@@ -23010,7 +23049,8 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
|
|
|
23010
23049
|
model: resolveDebaterModel({ agent: agentName }, this.config),
|
|
23011
23050
|
config: this.config,
|
|
23012
23051
|
storyId: this.storyId,
|
|
23013
|
-
sessionRole: "judge"
|
|
23052
|
+
sessionRole: "judge",
|
|
23053
|
+
timeoutMs: this.timeoutMs
|
|
23014
23054
|
}
|
|
23015
23055
|
});
|
|
23016
23056
|
return {
|
|
@@ -23024,7 +23064,7 @@ Do NOT output the JSON to the conversation. Write the file, then reply with a br
|
|
|
23024
23064
|
};
|
|
23025
23065
|
}
|
|
23026
23066
|
}
|
|
23027
|
-
var RESOLVER_FALLBACK_AGENT = "synthesis", _debateSessionDeps;
|
|
23067
|
+
var RESOLVER_FALLBACK_AGENT = "synthesis", _debateSessionDeps, DEFAULT_TIMEOUT_SECONDS = 600;
|
|
23028
23068
|
var init_session = __esm(() => {
|
|
23029
23069
|
init_registry();
|
|
23030
23070
|
init_config();
|
|
@@ -23228,13 +23268,15 @@ class AutoInteractionPlugin {
|
|
|
23228
23268
|
const modelDef = resolveModelForAgent(naxConfig.models, naxConfig.autoMode.defaultAgent, modelTier, naxConfig.autoMode.defaultAgent);
|
|
23229
23269
|
modelArg = modelDef.model;
|
|
23230
23270
|
}
|
|
23271
|
+
const timeoutMs = this.config.naxConfig ? (this.config.naxConfig.execution?.sessionTimeoutSeconds ?? 600) * 1000 : undefined;
|
|
23231
23272
|
const result = await adapter.complete(prompt, {
|
|
23232
23273
|
...modelArg && { model: modelArg },
|
|
23233
23274
|
jsonMode: true,
|
|
23234
23275
|
...this.config.naxConfig && { config: this.config.naxConfig },
|
|
23235
23276
|
featureName: request.featureName,
|
|
23236
23277
|
storyId: request.storyId,
|
|
23237
|
-
sessionRole: "auto"
|
|
23278
|
+
sessionRole: "auto",
|
|
23279
|
+
...timeoutMs !== undefined && { timeoutMs }
|
|
23238
23280
|
});
|
|
23239
23281
|
const output = typeof result === "string" ? result : result.output;
|
|
23240
23282
|
return this.parseResponse(output);
|
|
@@ -25977,6 +26019,7 @@ ${stderr}` };
|
|
|
25977
26019
|
return { action: "fail", reason: "[acceptance-setup] featureDir is not set" };
|
|
25978
26020
|
}
|
|
25979
26021
|
const language = (ctx.effectiveConfig ?? ctx.config).project?.language;
|
|
26022
|
+
const testPathConfig = (ctx.effectiveConfig ?? ctx.config).acceptance.testPath;
|
|
25980
26023
|
const metaPath = path5.join(ctx.featureDir, "acceptance-meta.json");
|
|
25981
26024
|
const allCriteria = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-")).flatMap((s) => s.acceptanceCriteria);
|
|
25982
26025
|
const nonFixStories = ctx.prd.userStories.filter((s) => !s.id.startsWith("US-FIX-"));
|
|
@@ -25999,7 +26042,7 @@ ${stderr}` };
|
|
|
25999
26042
|
const testPaths = [];
|
|
26000
26043
|
for (const [workdir] of workdirGroups) {
|
|
26001
26044
|
const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
|
|
26002
|
-
const testPath = path5.join(packageDir, ".nax", "features", featureName,
|
|
26045
|
+
const testPath = path5.join(packageDir, ".nax", "features", featureName, resolveAcceptanceTestFile(language, testPathConfig));
|
|
26003
26046
|
testPaths.push({ testPath, packageDir });
|
|
26004
26047
|
}
|
|
26005
26048
|
let totalCriteria = 0;
|
|
@@ -26062,7 +26105,7 @@ ${stderr}` };
|
|
|
26062
26105
|
testableCount = allRefinedCriteria.filter((r) => r.testable).length;
|
|
26063
26106
|
for (const [workdir, group] of workdirGroups) {
|
|
26064
26107
|
const packageDir = workdir ? path5.join(ctx.workdir, workdir) : ctx.workdir;
|
|
26065
|
-
const testPath = path5.join(packageDir,
|
|
26108
|
+
const testPath = path5.join(packageDir, resolveAcceptanceTestFile(language, testPathConfig));
|
|
26066
26109
|
const groupStoryIds = new Set(group.stories.map((s) => s.id));
|
|
26067
26110
|
const groupRefined = allRefinedCriteria.filter((r) => groupStoryIds.has(r.storyId));
|
|
26068
26111
|
const result = await _acceptanceSetupDeps.generate(group.stories, groupRefined, {
|
|
@@ -31531,8 +31574,15 @@ async function _defaultRunDebate(storyId, stageConfig, prompt, config2) {
|
|
|
31531
31574
|
if (resolved.length === 0) {
|
|
31532
31575
|
return { output: null, totalCostUsd: 0 };
|
|
31533
31576
|
}
|
|
31577
|
+
const timeoutMs = (config2?.execution?.sessionTimeoutSeconds ?? 600) * 1000;
|
|
31534
31578
|
const startMs = Date.now();
|
|
31535
|
-
const proposalSettled = await Promise.allSettled(resolved.map(({ debater, adapter }) => adapter.complete(prompt, {
|
|
31579
|
+
const proposalSettled = await Promise.allSettled(resolved.map(({ debater, adapter }) => adapter.complete(prompt, {
|
|
31580
|
+
model: debater.model,
|
|
31581
|
+
config: config2,
|
|
31582
|
+
storyId,
|
|
31583
|
+
sessionRole: "debate-proposal",
|
|
31584
|
+
timeoutMs
|
|
31585
|
+
}).then((out) => typeof out === "string" ? out : out.output)));
|
|
31536
31586
|
const durationMs = Date.now() - startMs;
|
|
31537
31587
|
const successful = proposalSettled.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
31538
31588
|
if (successful.length === 0) {
|
|
@@ -34087,6 +34137,214 @@ var init_crash_recovery = __esm(() => {
|
|
|
34087
34137
|
init_crash_heartbeat();
|
|
34088
34138
|
});
|
|
34089
34139
|
|
|
34140
|
+
// src/acceptance/fix-diagnosis.ts
|
|
34141
|
+
function parseImportStatements(content) {
|
|
34142
|
+
const importRegex = /import\s+(?:{[^}]+}|[^;]+)\s+from\s+["']([^"']+)["']/g;
|
|
34143
|
+
const imports = [];
|
|
34144
|
+
const regexMatch = content.matchAll(importRegex);
|
|
34145
|
+
for (const match of regexMatch) {
|
|
34146
|
+
imports.push(match[1]);
|
|
34147
|
+
}
|
|
34148
|
+
return imports;
|
|
34149
|
+
}
|
|
34150
|
+
function resolveImportPaths(imports, workdir) {
|
|
34151
|
+
const resolved = [];
|
|
34152
|
+
for (const imp of imports) {
|
|
34153
|
+
if (imp.startsWith(".")) {
|
|
34154
|
+
resolved.push(imp);
|
|
34155
|
+
}
|
|
34156
|
+
}
|
|
34157
|
+
return resolved.slice(0, MAX_SOURCE_FILES);
|
|
34158
|
+
}
|
|
34159
|
+
async function readSourceFileContent(filePath, workdir) {
|
|
34160
|
+
try {
|
|
34161
|
+
const fullPath = `${workdir}/${filePath}`;
|
|
34162
|
+
const file3 = await Bun.file(fullPath).text();
|
|
34163
|
+
const lines = file3.split(`
|
|
34164
|
+
`).slice(0, MAX_FILE_LINES);
|
|
34165
|
+
return { path: filePath, content: lines.join(`
|
|
34166
|
+
`) };
|
|
34167
|
+
} catch {
|
|
34168
|
+
return null;
|
|
34169
|
+
}
|
|
34170
|
+
}
|
|
34171
|
+
function buildDiagnosisPrompt(options) {
|
|
34172
|
+
const truncatedOutput = options.testOutput.slice(0, MAX_TEST_OUTPUT_CHARS);
|
|
34173
|
+
const sourceFilesSection = options.sourceFiles.length > 0 ? options.sourceFiles.map((f) => `FILE: ${f.path}
|
|
34174
|
+
\`\`\`
|
|
34175
|
+
${f.content}
|
|
34176
|
+
\`\`\``).join(`
|
|
34177
|
+
|
|
34178
|
+
`) : "(No source files could be resolved from imports)";
|
|
34179
|
+
return `You are a debugging expert. An acceptance test has failed.
|
|
34180
|
+
|
|
34181
|
+
TASK: Diagnose whether the failure is due to a bug in the SOURCE CODE or a bug in the TEST CODE.
|
|
34182
|
+
|
|
34183
|
+
FAILING TEST OUTPUT:
|
|
34184
|
+
${truncatedOutput}
|
|
34185
|
+
|
|
34186
|
+
ACCEPTANCE TEST FILE CONTENT:
|
|
34187
|
+
\`\`\`typescript
|
|
34188
|
+
${options.testFileContent}
|
|
34189
|
+
\`\`\`
|
|
34190
|
+
|
|
34191
|
+
SOURCE FILES (auto-detected from imports, up to ${MAX_FILE_LINES} lines each):
|
|
34192
|
+
${sourceFilesSection}
|
|
34193
|
+
|
|
34194
|
+
Respond with ONLY a JSON object in this exact format (no markdown, no extra text):
|
|
34195
|
+
{
|
|
34196
|
+
"verdict": "source_bug" | "test_bug" | "both",
|
|
34197
|
+
"reasoning": "Your analysis explaining why this is a source_bug, test_bug, or both",
|
|
34198
|
+
"confidence": 0.0-1.0,
|
|
34199
|
+
"testIssues": ["Issue in test code if any"],
|
|
34200
|
+
"sourceIssues": ["Issue in source code if any"]
|
|
34201
|
+
}`;
|
|
34202
|
+
}
|
|
34203
|
+
async function diagnoseAcceptanceFailure(agent, options) {
|
|
34204
|
+
if (!agent) {
|
|
34205
|
+
throw new Error("[diagnosis] Agent adapter is required");
|
|
34206
|
+
}
|
|
34207
|
+
const { testOutput, testFileContent, config: config2, workdir, featureName, storyId } = options;
|
|
34208
|
+
const sessionName = buildSessionName(workdir, featureName, storyId, "diagnose");
|
|
34209
|
+
const diagnoseModelTier = config2.acceptance.fix.diagnoseModel;
|
|
34210
|
+
const modelDef = resolveModelForAgent(config2.models, config2.autoMode.defaultAgent, diagnoseModelTier, config2.autoMode.defaultAgent);
|
|
34211
|
+
const imports = parseImportStatements(testFileContent);
|
|
34212
|
+
const relativeImports = resolveImportPaths(imports, workdir);
|
|
34213
|
+
const sourceFiles = await Promise.all(relativeImports.map((imp) => readSourceFileContent(imp, workdir)));
|
|
34214
|
+
const validSourceFiles = sourceFiles.filter((f) => f !== null);
|
|
34215
|
+
const prompt = buildDiagnosisPrompt({
|
|
34216
|
+
testOutput,
|
|
34217
|
+
testFileContent,
|
|
34218
|
+
sourceFiles: validSourceFiles
|
|
34219
|
+
});
|
|
34220
|
+
try {
|
|
34221
|
+
const result = await agent.run({
|
|
34222
|
+
prompt,
|
|
34223
|
+
workdir,
|
|
34224
|
+
modelTier: undefined,
|
|
34225
|
+
modelDef,
|
|
34226
|
+
timeoutSeconds: 300,
|
|
34227
|
+
sessionRole: "diagnose",
|
|
34228
|
+
acpSessionName: sessionName,
|
|
34229
|
+
featureName,
|
|
34230
|
+
storyId,
|
|
34231
|
+
config: config2
|
|
34232
|
+
});
|
|
34233
|
+
const diagnosis = parseDiagnosisResult(result.output);
|
|
34234
|
+
if (diagnosis) {
|
|
34235
|
+
return diagnosis;
|
|
34236
|
+
}
|
|
34237
|
+
return {
|
|
34238
|
+
verdict: "source_bug",
|
|
34239
|
+
reasoning: "diagnosis failed \u2014 falling back to source fix",
|
|
34240
|
+
confidence: 0
|
|
34241
|
+
};
|
|
34242
|
+
} catch (err) {
|
|
34243
|
+
return {
|
|
34244
|
+
verdict: "source_bug",
|
|
34245
|
+
reasoning: "diagnosis failed \u2014 falling back to source fix",
|
|
34246
|
+
confidence: 0
|
|
34247
|
+
};
|
|
34248
|
+
}
|
|
34249
|
+
}
|
|
34250
|
+
function parseDiagnosisResult(output) {
|
|
34251
|
+
if (!output || output.trim() === "") {
|
|
34252
|
+
return null;
|
|
34253
|
+
}
|
|
34254
|
+
try {
|
|
34255
|
+
const cleaned = output.trim();
|
|
34256
|
+
let jsonStr = cleaned;
|
|
34257
|
+
const firstBrace = cleaned.indexOf("{");
|
|
34258
|
+
const lastBrace = cleaned.lastIndexOf("}");
|
|
34259
|
+
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
|
34260
|
+
jsonStr = cleaned.slice(firstBrace, lastBrace + 1);
|
|
34261
|
+
}
|
|
34262
|
+
const parsed = JSON.parse(jsonStr);
|
|
34263
|
+
if (typeof parsed.verdict === "string" && typeof parsed.reasoning === "string" && typeof parsed.confidence === "number") {
|
|
34264
|
+
return {
|
|
34265
|
+
verdict: parsed.verdict,
|
|
34266
|
+
reasoning: parsed.reasoning,
|
|
34267
|
+
confidence: parsed.confidence,
|
|
34268
|
+
testIssues: parsed.testIssues,
|
|
34269
|
+
sourceIssues: parsed.sourceIssues
|
|
34270
|
+
};
|
|
34271
|
+
}
|
|
34272
|
+
return null;
|
|
34273
|
+
} catch {
|
|
34274
|
+
return null;
|
|
34275
|
+
}
|
|
34276
|
+
}
|
|
34277
|
+
var MAX_SOURCE_FILES = 5, MAX_FILE_LINES = 500, MAX_TEST_OUTPUT_CHARS = 2000;
|
|
34278
|
+
var init_fix_diagnosis = __esm(() => {
|
|
34279
|
+
init_adapter();
|
|
34280
|
+
});
|
|
34281
|
+
|
|
34282
|
+
// src/acceptance/fix-executor.ts
|
|
34283
|
+
function buildSourceFixPrompt(options) {
|
|
34284
|
+
const { testOutput, diagnosis, acceptanceTestPath } = options;
|
|
34285
|
+
let prompt = `ACCEPTANCE TEST FAILURE:
|
|
34286
|
+
${testOutput}
|
|
34287
|
+
|
|
34288
|
+
`;
|
|
34289
|
+
if (diagnosis.reasoning) {
|
|
34290
|
+
prompt += `DIAGNOSIS:
|
|
34291
|
+
${diagnosis.reasoning}
|
|
34292
|
+
|
|
34293
|
+
`;
|
|
34294
|
+
}
|
|
34295
|
+
prompt += `ACCEPTANCE TEST FILE: ${acceptanceTestPath}
|
|
34296
|
+
|
|
34297
|
+
`;
|
|
34298
|
+
prompt += "Fix the source implementation. Do NOT modify the test file.";
|
|
34299
|
+
return prompt;
|
|
34300
|
+
}
|
|
34301
|
+
async function executeSourceFix(agent, options) {
|
|
34302
|
+
if (!agent) {
|
|
34303
|
+
throw new Error("[fix-executor] agent is required");
|
|
34304
|
+
}
|
|
34305
|
+
const { testOutput, testFileContent, diagnosis, config: config2, workdir, featureName, storyId, acceptanceTestPath } = options;
|
|
34306
|
+
const modelDef = resolveModelForAgent(config2.models, config2.autoMode.defaultAgent, config2.acceptance.fix.fixModel, config2.autoMode.defaultAgent);
|
|
34307
|
+
const sessionName = buildSessionName(workdir, featureName, storyId, "source-fix");
|
|
34308
|
+
const prompt = buildSourceFixPrompt(options);
|
|
34309
|
+
const timeoutSeconds = config2.execution?.sessionTimeoutSeconds ?? 3600;
|
|
34310
|
+
const runOptions = {
|
|
34311
|
+
prompt,
|
|
34312
|
+
workdir,
|
|
34313
|
+
modelTier: undefined,
|
|
34314
|
+
modelDef,
|
|
34315
|
+
timeoutSeconds,
|
|
34316
|
+
sessionRole: "source-fix",
|
|
34317
|
+
acpSessionName: sessionName,
|
|
34318
|
+
featureName,
|
|
34319
|
+
storyId,
|
|
34320
|
+
config: config2,
|
|
34321
|
+
pipelineStage: "acceptance"
|
|
34322
|
+
};
|
|
34323
|
+
const result = await agent.run(runOptions);
|
|
34324
|
+
let success2 = result.success;
|
|
34325
|
+
try {
|
|
34326
|
+
const verifyProc = _fixExecutorDeps.spawn(["bun", "test", acceptanceTestPath], {
|
|
34327
|
+
cwd: workdir,
|
|
34328
|
+
stdout: "pipe",
|
|
34329
|
+
stderr: "pipe"
|
|
34330
|
+
});
|
|
34331
|
+
const exitCode = await verifyProc.exited;
|
|
34332
|
+
success2 = exitCode === 0;
|
|
34333
|
+
} catch {
|
|
34334
|
+
success2 = result.success;
|
|
34335
|
+
}
|
|
34336
|
+
return {
|
|
34337
|
+
success: success2,
|
|
34338
|
+
cost: result.estimatedCost
|
|
34339
|
+
};
|
|
34340
|
+
}
|
|
34341
|
+
var _fixExecutorDeps;
|
|
34342
|
+
var init_fix_executor = __esm(() => {
|
|
34343
|
+
init_adapter();
|
|
34344
|
+
init_bun_deps();
|
|
34345
|
+
_fixExecutorDeps = { spawn };
|
|
34346
|
+
});
|
|
34347
|
+
|
|
34090
34348
|
// src/execution/lifecycle/acceptance-loop.ts
|
|
34091
34349
|
var exports_acceptance_loop = {};
|
|
34092
34350
|
__export(exports_acceptance_loop, {
|
|
@@ -34113,6 +34371,14 @@ async function loadSpecContent(featureDir) {
|
|
|
34113
34371
|
const specFile = Bun.file(specPath);
|
|
34114
34372
|
return await specFile.exists() ? await specFile.text() : "";
|
|
34115
34373
|
}
|
|
34374
|
+
async function loadAcceptanceTestContent(featureDir) {
|
|
34375
|
+
if (!featureDir)
|
|
34376
|
+
return { content: "", path: "" };
|
|
34377
|
+
const testPath = path14.join(featureDir, "acceptance.test.ts");
|
|
34378
|
+
const testFile = Bun.file(testPath);
|
|
34379
|
+
const content = await testFile.exists() ? await testFile.text() : "";
|
|
34380
|
+
return { content, path: testPath };
|
|
34381
|
+
}
|
|
34116
34382
|
function buildResult(success2, prd, totalCost, iterations, storiesCompleted, prdDirty) {
|
|
34117
34383
|
return { success: success2, prd, totalCost, iterations, storiesCompleted, prdDirty };
|
|
34118
34384
|
}
|
|
@@ -34198,6 +34464,191 @@ async function regenerateAcceptanceTest(testPath, acceptanceContext) {
|
|
|
34198
34464
|
logger?.info("acceptance", "Acceptance test regenerated successfully");
|
|
34199
34465
|
return true;
|
|
34200
34466
|
}
|
|
34467
|
+
async function runFixRouting(options) {
|
|
34468
|
+
const logger = getSafeLogger();
|
|
34469
|
+
const { ctx, failures, prd, acceptanceContext } = options;
|
|
34470
|
+
const agentName = ctx.config.autoMode.defaultAgent;
|
|
34471
|
+
const agent = (ctx.agentGetFn ?? _acceptanceLoopDeps.getAgent)(agentName);
|
|
34472
|
+
if (!agent) {
|
|
34473
|
+
logger?.error("acceptance", "Agent not found for fix routing");
|
|
34474
|
+
return { fixed: false, cost: 0, prdDirty: false };
|
|
34475
|
+
}
|
|
34476
|
+
const strategy = ctx.config.acceptance.fix?.strategy ?? "diagnose-first";
|
|
34477
|
+
const fixMaxRetries = ctx.config.acceptance.fix?.maxRetries ?? 2;
|
|
34478
|
+
const { content: testFileContent, path: acceptanceTestPath } = await loadAcceptanceTestContent(ctx.featureDir);
|
|
34479
|
+
const firstStory = prd.userStories[0];
|
|
34480
|
+
const storyId = firstStory?.id ?? "unknown";
|
|
34481
|
+
if (strategy === "implement-only") {
|
|
34482
|
+
logger?.info("acceptance", "Strategy is implement-only \u2014 executing source fix directly");
|
|
34483
|
+
let fixAttempts = 0;
|
|
34484
|
+
while (fixAttempts < fixMaxRetries) {
|
|
34485
|
+
fixAttempts++;
|
|
34486
|
+
logger?.info("acceptance", `Source fix attempt ${fixAttempts}/${fixMaxRetries}`);
|
|
34487
|
+
const defaultDiagnosis = {
|
|
34488
|
+
verdict: "source_bug",
|
|
34489
|
+
reasoning: "implement-only strategy \u2014 skipping diagnosis",
|
|
34490
|
+
confidence: 1
|
|
34491
|
+
};
|
|
34492
|
+
const fixResult = await executeSourceFix(agent, {
|
|
34493
|
+
testOutput: failures.testOutput,
|
|
34494
|
+
testFileContent,
|
|
34495
|
+
diagnosis: defaultDiagnosis,
|
|
34496
|
+
config: ctx.config,
|
|
34497
|
+
workdir: ctx.workdir,
|
|
34498
|
+
featureName: ctx.feature,
|
|
34499
|
+
storyId,
|
|
34500
|
+
acceptanceTestPath
|
|
34501
|
+
});
|
|
34502
|
+
logger?.info("acceptance.source-fix", "Source fix completed", {
|
|
34503
|
+
success: fixResult.success,
|
|
34504
|
+
cost: fixResult.cost,
|
|
34505
|
+
attempt: fixAttempts
|
|
34506
|
+
});
|
|
34507
|
+
if (fixResult.success) {
|
|
34508
|
+
return { fixed: true, cost: fixResult.cost, prdDirty: false };
|
|
34509
|
+
}
|
|
34510
|
+
if (fixAttempts >= fixMaxRetries) {
|
|
34511
|
+
logger?.error("acceptance", `Source fix failed after ${fixMaxRetries} attempts`);
|
|
34512
|
+
break;
|
|
34513
|
+
}
|
|
34514
|
+
}
|
|
34515
|
+
return { fixed: false, cost: 0, prdDirty: false };
|
|
34516
|
+
}
|
|
34517
|
+
logger?.info("acceptance", "Strategy is diagnose-first \u2014 running diagnosis");
|
|
34518
|
+
const diagnosis = await diagnoseAcceptanceFailure(agent, {
|
|
34519
|
+
testOutput: failures.testOutput,
|
|
34520
|
+
testFileContent,
|
|
34521
|
+
config: ctx.config,
|
|
34522
|
+
workdir: ctx.workdir,
|
|
34523
|
+
featureName: ctx.feature,
|
|
34524
|
+
storyId
|
|
34525
|
+
});
|
|
34526
|
+
logger?.info("acceptance.diagnosis", "Diagnosis complete", {
|
|
34527
|
+
verdict: diagnosis.verdict,
|
|
34528
|
+
confidence: diagnosis.confidence,
|
|
34529
|
+
reasoning: diagnosis.reasoning
|
|
34530
|
+
});
|
|
34531
|
+
if (diagnosis.verdict === "source_bug") {
|
|
34532
|
+
logger?.info("acceptance", "Diagnosis: source_bug \u2014 executing source fix");
|
|
34533
|
+
let fixAttempts = 0;
|
|
34534
|
+
while (fixAttempts < fixMaxRetries) {
|
|
34535
|
+
fixAttempts++;
|
|
34536
|
+
logger?.info("acceptance", `Source fix attempt ${fixAttempts}/${fixMaxRetries}`);
|
|
34537
|
+
const fixResult = await executeSourceFix(agent, {
|
|
34538
|
+
testOutput: failures.testOutput,
|
|
34539
|
+
testFileContent,
|
|
34540
|
+
diagnosis,
|
|
34541
|
+
config: ctx.config,
|
|
34542
|
+
workdir: ctx.workdir,
|
|
34543
|
+
featureName: ctx.feature,
|
|
34544
|
+
storyId,
|
|
34545
|
+
acceptanceTestPath
|
|
34546
|
+
});
|
|
34547
|
+
logger?.info("acceptance.source-fix", "Source fix completed", {
|
|
34548
|
+
success: fixResult.success,
|
|
34549
|
+
cost: fixResult.cost,
|
|
34550
|
+
attempt: fixAttempts
|
|
34551
|
+
});
|
|
34552
|
+
if (fixResult.success) {
|
|
34553
|
+
return { fixed: true, cost: fixResult.cost, prdDirty: false };
|
|
34554
|
+
}
|
|
34555
|
+
if (fixAttempts >= fixMaxRetries) {
|
|
34556
|
+
logger?.error("acceptance", `Source fix failed after ${fixMaxRetries} attempts`);
|
|
34557
|
+
break;
|
|
34558
|
+
}
|
|
34559
|
+
}
|
|
34560
|
+
return { fixed: false, cost: 0, prdDirty: false };
|
|
34561
|
+
}
|
|
34562
|
+
if (diagnosis.verdict === "test_bug") {
|
|
34563
|
+
logger?.info("acceptance", "Diagnosis: test_bug \u2014 regenerating acceptance test");
|
|
34564
|
+
if (!ctx.featureDir) {
|
|
34565
|
+
logger?.error("acceptance", "Cannot regenerate test without featureDir");
|
|
34566
|
+
return { fixed: false, cost: 0, prdDirty: false };
|
|
34567
|
+
}
|
|
34568
|
+
const testPath = path14.join(ctx.featureDir, "acceptance.test.ts");
|
|
34569
|
+
const testFile = Bun.file(testPath);
|
|
34570
|
+
if (!await testFile.exists()) {
|
|
34571
|
+
logger?.error("acceptance", "Acceptance test file not found for regeneration");
|
|
34572
|
+
return { fixed: false, cost: 0, prdDirty: false };
|
|
34573
|
+
}
|
|
34574
|
+
const regenerated = await regenerateAcceptanceTest(testPath, acceptanceContext);
|
|
34575
|
+
logger?.info("acceptance.test-regen", "Test regeneration completed", {
|
|
34576
|
+
outcome: regenerated ? "success" : "failure"
|
|
34577
|
+
});
|
|
34578
|
+
if (!regenerated) {
|
|
34579
|
+
return { fixed: false, cost: 0, prdDirty: false };
|
|
34580
|
+
}
|
|
34581
|
+
const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance2(), exports_acceptance));
|
|
34582
|
+
const acceptanceResult = await acceptanceStage2.execute(acceptanceContext);
|
|
34583
|
+
if (acceptanceResult.action === "continue") {
|
|
34584
|
+
logger?.info("acceptance", "Acceptance passed after test regeneration");
|
|
34585
|
+
return { fixed: true, cost: 0, prdDirty: true };
|
|
34586
|
+
}
|
|
34587
|
+
logger?.warn("acceptance", "Acceptance still failing after test regeneration");
|
|
34588
|
+
return { fixed: false, cost: 0, prdDirty: true };
|
|
34589
|
+
}
|
|
34590
|
+
if (diagnosis.verdict === "both") {
|
|
34591
|
+
logger?.info("acceptance", "Diagnosis: both \u2014 executing source fix then regenerating test if needed");
|
|
34592
|
+
let sourceFixSuccess = false;
|
|
34593
|
+
let sourceFixCost = 0;
|
|
34594
|
+
let fixAttempts = 0;
|
|
34595
|
+
while (fixAttempts < fixMaxRetries && !sourceFixSuccess) {
|
|
34596
|
+
fixAttempts++;
|
|
34597
|
+
logger?.info("acceptance", `Source fix attempt ${fixAttempts}/${fixMaxRetries}`);
|
|
34598
|
+
const fixResult = await executeSourceFix(agent, {
|
|
34599
|
+
testOutput: failures.testOutput,
|
|
34600
|
+
testFileContent,
|
|
34601
|
+
diagnosis,
|
|
34602
|
+
config: ctx.config,
|
|
34603
|
+
workdir: ctx.workdir,
|
|
34604
|
+
featureName: ctx.feature,
|
|
34605
|
+
storyId,
|
|
34606
|
+
acceptanceTestPath
|
|
34607
|
+
});
|
|
34608
|
+
logger?.info("acceptance.source-fix", "Source fix completed", {
|
|
34609
|
+
success: fixResult.success,
|
|
34610
|
+
cost: fixResult.cost,
|
|
34611
|
+
attempt: fixAttempts
|
|
34612
|
+
});
|
|
34613
|
+
sourceFixSuccess = fixResult.success;
|
|
34614
|
+
sourceFixCost += fixResult.cost;
|
|
34615
|
+
if (fixResult.success) {
|
|
34616
|
+
break;
|
|
34617
|
+
}
|
|
34618
|
+
if (fixAttempts >= fixMaxRetries) {
|
|
34619
|
+
logger?.error("acceptance", `Source fix failed after ${fixMaxRetries} attempts`);
|
|
34620
|
+
break;
|
|
34621
|
+
}
|
|
34622
|
+
}
|
|
34623
|
+
if (!sourceFixSuccess) {
|
|
34624
|
+
return { fixed: false, cost: sourceFixCost, prdDirty: false };
|
|
34625
|
+
}
|
|
34626
|
+
logger?.info("acceptance", "Source fix succeeded \u2014 re-running acceptance to verify");
|
|
34627
|
+
const { acceptanceStage: acceptanceStage2 } = await Promise.resolve().then(() => (init_acceptance2(), exports_acceptance));
|
|
34628
|
+
const acceptanceResult = await acceptanceStage2.execute(acceptanceContext);
|
|
34629
|
+
if (acceptanceResult.action === "continue") {
|
|
34630
|
+
logger?.info("acceptance", "Acceptance passed after source fix");
|
|
34631
|
+
return { fixed: true, cost: sourceFixCost, prdDirty: false };
|
|
34632
|
+
}
|
|
34633
|
+
logger?.info("acceptance", "Acceptance still failing after source fix \u2014 regenerating test");
|
|
34634
|
+
if (!ctx.featureDir) {
|
|
34635
|
+
logger?.error("acceptance", "Cannot regenerate test without featureDir");
|
|
34636
|
+
return { fixed: false, cost: sourceFixCost, prdDirty: false };
|
|
34637
|
+
}
|
|
34638
|
+
const testPath = path14.join(ctx.featureDir, "acceptance.test.ts");
|
|
34639
|
+
const testFile = Bun.file(testPath);
|
|
34640
|
+
if (!await testFile.exists()) {
|
|
34641
|
+
logger?.error("acceptance", "Acceptance test file not found for regeneration");
|
|
34642
|
+
return { fixed: false, cost: sourceFixCost, prdDirty: false };
|
|
34643
|
+
}
|
|
34644
|
+
const regenerated = await regenerateAcceptanceTest(testPath, acceptanceContext);
|
|
34645
|
+
logger?.info("acceptance.test-regen", "Test regeneration completed", {
|
|
34646
|
+
outcome: regenerated ? "success" : "failure"
|
|
34647
|
+
});
|
|
34648
|
+
return { fixed: regenerated, cost: sourceFixCost, prdDirty: regenerated };
|
|
34649
|
+
}
|
|
34650
|
+
return { fixed: false, cost: 0, prdDirty: false };
|
|
34651
|
+
}
|
|
34201
34652
|
async function runAcceptanceLoop(ctx) {
|
|
34202
34653
|
const logger = getSafeLogger();
|
|
34203
34654
|
const maxRetries = ctx.config.acceptance.maxRetries;
|
|
@@ -34292,6 +34743,23 @@ async function runAcceptanceLoop(ctx) {
|
|
|
34292
34743
|
continue;
|
|
34293
34744
|
}
|
|
34294
34745
|
}
|
|
34746
|
+
const strategy = ctx.config.acceptance.fix?.strategy ?? "diagnose-first";
|
|
34747
|
+
if (strategy === "diagnose-first" || strategy === "implement-only") {
|
|
34748
|
+
logger?.info("acceptance", `Running fix routing with strategy: ${strategy}`);
|
|
34749
|
+
const fixResult = await runFixRouting({
|
|
34750
|
+
ctx,
|
|
34751
|
+
failures,
|
|
34752
|
+
prd,
|
|
34753
|
+
acceptanceContext
|
|
34754
|
+
});
|
|
34755
|
+
totalCost += fixResult.cost;
|
|
34756
|
+
if (fixResult.fixed) {
|
|
34757
|
+
logger?.info("acceptance", "Fix succeeded \u2014 re-running acceptance tests...");
|
|
34758
|
+
continue;
|
|
34759
|
+
}
|
|
34760
|
+
logger?.error("acceptance", "Fix routing failed to resolve acceptance failures");
|
|
34761
|
+
return buildResult(false, prd, totalCost, iterations, storiesCompleted, prdDirty);
|
|
34762
|
+
}
|
|
34295
34763
|
logger?.info("acceptance", "Generating fix stories...");
|
|
34296
34764
|
const fixStories = await generateAndAddFixStories(ctx, failures, prd);
|
|
34297
34765
|
if (!fixStories) {
|
|
@@ -34323,6 +34791,8 @@ async function runAcceptanceLoop(ctx) {
|
|
|
34323
34791
|
var _acceptanceLoopDeps;
|
|
34324
34792
|
var init_acceptance_loop = __esm(() => {
|
|
34325
34793
|
init_acceptance();
|
|
34794
|
+
init_fix_diagnosis();
|
|
34795
|
+
init_fix_executor();
|
|
34326
34796
|
init_registry();
|
|
34327
34797
|
init_config();
|
|
34328
34798
|
init_loader();
|
|
@@ -34437,6 +34907,16 @@ async function runDeferredRegression(options) {
|
|
|
34437
34907
|
};
|
|
34438
34908
|
}
|
|
34439
34909
|
const testSummary = _regressionDeps.parseBunTestOutput(fullSuiteResult.output);
|
|
34910
|
+
if (testSummary.failed === 0 && testSummary.passed === 0) {
|
|
34911
|
+
logger?.warn("regression", "No test results parsed from output \u2014 test runner likely crashed or errored (not a regression, accepting as pass)", { output: fullSuiteResult.output.slice(0, 500) });
|
|
34912
|
+
return {
|
|
34913
|
+
success: true,
|
|
34914
|
+
failedTests: 0,
|
|
34915
|
+
passedTests: 0,
|
|
34916
|
+
rectificationAttempts: 0,
|
|
34917
|
+
affectedStories: []
|
|
34918
|
+
};
|
|
34919
|
+
}
|
|
34440
34920
|
const affectedStories = new Set;
|
|
34441
34921
|
const affectedStoriesObjs = new Map;
|
|
34442
34922
|
logger?.warn("regression", "Regression detected", {
|
|
@@ -36618,13 +37098,25 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
36618
37098
|
prd = await loadPRD(ctx.prdPath);
|
|
36619
37099
|
prdDirty = false;
|
|
36620
37100
|
}
|
|
37101
|
+
const storyCounts = countStories(prd);
|
|
37102
|
+
logger?.debug("execution", "Loop iteration", {
|
|
37103
|
+
iteration: iterations,
|
|
37104
|
+
isComplete: isComplete(prd),
|
|
37105
|
+
passed: storyCounts.passed,
|
|
37106
|
+
pending: storyCounts.pending,
|
|
37107
|
+
failed: storyCounts.failed,
|
|
37108
|
+
total: storyCounts.total
|
|
37109
|
+
});
|
|
36621
37110
|
if (isComplete(prd)) {
|
|
37111
|
+
logger?.debug("execution", "All stories complete \u2014 entering completion path");
|
|
36622
37112
|
if (ctx.interactionChain && isTriggerEnabled("pre-merge", ctx.config)) {
|
|
36623
37113
|
const shouldProceed = await checkPreMerge({ featureName: ctx.feature, totalStories: prd.userStories.length, cost: totalCost }, ctx.config, ctx.interactionChain);
|
|
36624
37114
|
if (!shouldProceed)
|
|
36625
37115
|
return buildResult2("pre-merge-aborted");
|
|
36626
37116
|
}
|
|
37117
|
+
logger?.debug("execution", "Running deferred review");
|
|
36627
37118
|
deferredReview = await runDeferredReview(ctx.workdir, ctx.config.review, ctx.pluginRegistry, runStartRef);
|
|
37119
|
+
logger?.debug("execution", "Deferred review done \u2014 returning completed");
|
|
36628
37120
|
return buildResult2("completed");
|
|
36629
37121
|
}
|
|
36630
37122
|
const costLimit = ctx.config.execution.costLimit;
|
|
@@ -36877,9 +37369,7 @@ async function executeUnified(ctx, initialPrd) {
|
|
|
36877
37369
|
}, ctx.eventEmitter);
|
|
36878
37370
|
}
|
|
36879
37371
|
return buildResult2("max-iterations");
|
|
36880
|
-
} finally {
|
|
36881
|
-
stopHeartbeat();
|
|
36882
|
-
}
|
|
37372
|
+
} finally {}
|
|
36883
37373
|
}
|
|
36884
37374
|
var _unifiedExecutorDeps;
|
|
36885
37375
|
var init_unified_executor = __esm(() => {
|
|
@@ -69804,6 +70294,7 @@ function validatePlanOutput(raw, feature, branch) {
|
|
|
69804
70294
|
}
|
|
69805
70295
|
|
|
69806
70296
|
// src/cli/plan.ts
|
|
70297
|
+
var DEFAULT_TIMEOUT_SECONDS2 = 600;
|
|
69807
70298
|
var _planDeps = {
|
|
69808
70299
|
readFile: (path) => Bun.file(path).text(),
|
|
69809
70300
|
writeFile: (path, content) => Bun.write(path, content).then(() => {}),
|
|
@@ -69854,7 +70345,7 @@ async function planCommand(workdir, config2, options) {
|
|
|
69854
70345
|
const outputPath = join12(outputDir, "prd.json");
|
|
69855
70346
|
await _planDeps.mkdirp(outputDir);
|
|
69856
70347
|
const agentName = config2?.autoMode?.defaultAgent ?? "claude";
|
|
69857
|
-
const timeoutSeconds = config2?.
|
|
70348
|
+
const timeoutSeconds = config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2;
|
|
69858
70349
|
let rawResponse;
|
|
69859
70350
|
const debateEnabled = config2?.debate?.enabled && config2?.debate?.stages?.plan?.enabled;
|
|
69860
70351
|
if (debateEnabled) {
|
|
@@ -69938,13 +70429,15 @@ async function planCommand(workdir, config2, options) {
|
|
|
69938
70429
|
}
|
|
69939
70430
|
rawResponse = await _planDeps.readFile(outputPath);
|
|
69940
70431
|
} else {
|
|
70432
|
+
const timeoutMs = (config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2) * 1000;
|
|
69941
70433
|
const completeResult = await adapter.complete(prompt, {
|
|
69942
70434
|
model: autoModel,
|
|
69943
70435
|
jsonMode: true,
|
|
69944
70436
|
workdir,
|
|
69945
70437
|
config: config2,
|
|
69946
70438
|
featureName: options.feature,
|
|
69947
|
-
sessionRole: "plan"
|
|
70439
|
+
sessionRole: "plan",
|
|
70440
|
+
timeoutMs
|
|
69948
70441
|
});
|
|
69949
70442
|
let result = typeof completeResult === "string" ? completeResult : completeResult.output;
|
|
69950
70443
|
try {
|
|
@@ -70317,6 +70810,8 @@ async function planDecomposeCommand(workdir, config2, options) {
|
|
|
70317
70810
|
const stages = config2?.debate?.stages;
|
|
70318
70811
|
const debateEnabled = config2?.debate?.enabled && stages?.decompose?.enabled;
|
|
70319
70812
|
let rawResponse;
|
|
70813
|
+
const timeoutSeconds = config2?.plan?.timeoutSeconds ?? DEFAULT_TIMEOUT_SECONDS2;
|
|
70814
|
+
const timeoutMs = timeoutSeconds * 1000;
|
|
70320
70815
|
if (debateEnabled) {
|
|
70321
70816
|
const stageConfig = stages?.decompose;
|
|
70322
70817
|
const debateSession = _planDeps.createDebateSession({
|
|
@@ -70326,7 +70821,7 @@ async function planDecomposeCommand(workdir, config2, options) {
|
|
|
70326
70821
|
config: config2,
|
|
70327
70822
|
workdir,
|
|
70328
70823
|
featureName: options.feature,
|
|
70329
|
-
timeoutSeconds
|
|
70824
|
+
timeoutSeconds
|
|
70330
70825
|
});
|
|
70331
70826
|
const debateResult = await debateSession.run(prompt);
|
|
70332
70827
|
if (debateResult.outcome !== "failed" && debateResult.output) {
|
|
@@ -70337,7 +70832,8 @@ async function planDecomposeCommand(workdir, config2, options) {
|
|
|
70337
70832
|
workdir,
|
|
70338
70833
|
sessionRole: "decompose",
|
|
70339
70834
|
featureName: options.feature,
|
|
70340
|
-
storyId: options.storyId
|
|
70835
|
+
storyId: options.storyId,
|
|
70836
|
+
timeoutMs
|
|
70341
70837
|
});
|
|
70342
70838
|
rawResponse = typeof completeResult === "string" ? completeResult : completeResult.output;
|
|
70343
70839
|
}
|
|
@@ -70347,7 +70843,8 @@ async function planDecomposeCommand(workdir, config2, options) {
|
|
|
70347
70843
|
workdir,
|
|
70348
70844
|
sessionRole: "decompose",
|
|
70349
70845
|
featureName: options.feature,
|
|
70350
|
-
storyId: options.storyId
|
|
70846
|
+
storyId: options.storyId,
|
|
70847
|
+
timeoutMs
|
|
70351
70848
|
});
|
|
70352
70849
|
rawResponse = typeof completeResult === "string" ? completeResult : completeResult.output;
|
|
70353
70850
|
}
|
|
@@ -72990,6 +73487,10 @@ init_crash_recovery();
|
|
|
72990
73487
|
init_story_context();
|
|
72991
73488
|
async function runCompletionPhase(options) {
|
|
72992
73489
|
const logger = getSafeLogger();
|
|
73490
|
+
logger?.debug("execution", "Completion phase started", {
|
|
73491
|
+
acceptanceEnabled: options.config.acceptance?.enabled,
|
|
73492
|
+
isComplete: isComplete(options.prd)
|
|
73493
|
+
});
|
|
72993
73494
|
if (options.config.acceptance.enabled && isComplete(options.prd)) {
|
|
72994
73495
|
const { runAcceptanceLoop: runAcceptanceLoop2 } = await Promise.resolve().then(() => (init_acceptance_loop(), exports_acceptance_loop));
|
|
72995
73496
|
const acceptanceResult = await runAcceptanceLoop2({
|
|
@@ -73057,9 +73558,12 @@ async function runCompletionPhase(options) {
|
|
|
73057
73558
|
formatterMode: options.formatterMode
|
|
73058
73559
|
});
|
|
73059
73560
|
}
|
|
73561
|
+
logger?.debug("execution", "Completion phase \u2014 stopping heartbeat and writing exit summary");
|
|
73060
73562
|
stopHeartbeat();
|
|
73061
73563
|
await writeExitSummary(options.logFilePath, options.totalCost, options.iterations, options.storiesCompleted, durationMs);
|
|
73564
|
+
logger?.debug("execution", "Completion phase \u2014 auto-committing dirty files");
|
|
73062
73565
|
await autoCommitIfDirty(options.workdir, "run.complete", "run-summary", options.feature);
|
|
73566
|
+
logger?.debug("execution", "Completion phase done \u2014 returning to runner");
|
|
73063
73567
|
return {
|
|
73064
73568
|
durationMs,
|
|
73065
73569
|
runCompletedAt
|
|
@@ -73197,6 +73701,12 @@ async function runExecutionPhase(options, prd, pluginRegistry) {
|
|
|
73197
73701
|
storiesCompleted = unifiedResult.storiesCompleted;
|
|
73198
73702
|
totalCost = unifiedResult.totalCost;
|
|
73199
73703
|
allStoryMetrics.push(...unifiedResult.allStoryMetrics);
|
|
73704
|
+
logger?.debug("execution", "Execution phase complete \u2014 handing off to completion phase", {
|
|
73705
|
+
exitReason: unifiedResult.exitReason,
|
|
73706
|
+
iterations,
|
|
73707
|
+
storiesCompleted,
|
|
73708
|
+
totalCost
|
|
73709
|
+
});
|
|
73200
73710
|
return { prd, iterations, storiesCompleted, totalCost, allStoryMetrics };
|
|
73201
73711
|
}
|
|
73202
73712
|
|
|
@@ -73355,15 +73865,20 @@ async function run(options) {
|
|
|
73355
73865
|
durationMs
|
|
73356
73866
|
};
|
|
73357
73867
|
} finally {
|
|
73868
|
+
const logger2 = getSafeLogger();
|
|
73869
|
+
logger2?.debug("execution", "Runner finally block \u2014 starting cleanup");
|
|
73358
73870
|
stopHeartbeat();
|
|
73359
73871
|
cleanupCrashHandlers();
|
|
73872
|
+
logger2?.debug("execution", "Runner finally \u2014 sweeping ACP sessions");
|
|
73360
73873
|
await sweepFeatureSessions(workdir, feature).catch(() => {});
|
|
73874
|
+
logger2?.debug("execution", "Runner finally \u2014 ACP sweep done");
|
|
73361
73875
|
let branch = "";
|
|
73362
73876
|
try {
|
|
73363
73877
|
const { stdout, exitCode } = await gitWithTimeout(["branch", "--show-current"], workdir);
|
|
73364
73878
|
if (exitCode === 0)
|
|
73365
73879
|
branch = stdout.trim();
|
|
73366
73880
|
} catch {}
|
|
73881
|
+
logger2?.debug("execution", "Runner finally \u2014 running cleanupRun");
|
|
73367
73882
|
const { cleanupRun: cleanupRun2 } = await Promise.resolve().then(() => (init_run_cleanup(), exports_run_cleanup));
|
|
73368
73883
|
await cleanupRun2({
|
|
73369
73884
|
runId,
|
|
@@ -73379,6 +73894,7 @@ async function run(options) {
|
|
|
73379
73894
|
branch,
|
|
73380
73895
|
version: NAX_VERSION
|
|
73381
73896
|
});
|
|
73897
|
+
logger2?.debug("execution", "Runner finally \u2014 cleanupRun done, run() returning");
|
|
73382
73898
|
}
|
|
73383
73899
|
}
|
|
73384
73900
|
|