@nathapp/nax 0.46.1 → 0.46.2
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/CHANGELOG.md +8 -0
- package/dist/nax.js +185 -111
- package/package.json +1 -1
- package/src/pipeline/stages/autofix.ts +112 -25
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.46.2] - 2026-03-17
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- **Review rectification:** When lint or typecheck fails in the review stage and mechanical autofix (`lintFix`, `formatFix`) cannot resolve it, nax now spawns an agent rectification session with the exact error output as context. The agent fixes the issues, commits, and re-runs review to verify. Reuses `quality.autofix.maxAttempts` (default: 2) for agent attempts.
|
|
12
|
+
|
|
13
|
+
### Tests
|
|
14
|
+
- 12 new tests in `test/unit/pipeline/stages/autofix.test.ts` covering agent rectification paths.
|
|
15
|
+
|
|
8
16
|
## [0.46.1] - 2026-03-17
|
|
9
17
|
|
|
10
18
|
### Fixed
|
package/dist/nax.js
CHANGED
|
@@ -22178,7 +22178,7 @@ var package_default;
|
|
|
22178
22178
|
var init_package = __esm(() => {
|
|
22179
22179
|
package_default = {
|
|
22180
22180
|
name: "@nathapp/nax",
|
|
22181
|
-
version: "0.46.
|
|
22181
|
+
version: "0.46.2",
|
|
22182
22182
|
description: "AI Coding Agent Orchestrator \u2014 loops until done",
|
|
22183
22183
|
type: "module",
|
|
22184
22184
|
bin: {
|
|
@@ -22251,8 +22251,8 @@ var init_version = __esm(() => {
|
|
|
22251
22251
|
NAX_VERSION = package_default.version;
|
|
22252
22252
|
NAX_COMMIT = (() => {
|
|
22253
22253
|
try {
|
|
22254
|
-
if (/^[0-9a-f]{6,10}$/.test("
|
|
22255
|
-
return "
|
|
22254
|
+
if (/^[0-9a-f]{6,10}$/.test("506ad27"))
|
|
22255
|
+
return "506ad27";
|
|
22256
22256
|
} catch {}
|
|
22257
22257
|
try {
|
|
22258
22258
|
const result = Bun.spawnSync(["git", "rev-parse", "--short", "HEAD"], {
|
|
@@ -24086,6 +24086,99 @@ ${stderr}` };
|
|
|
24086
24086
|
};
|
|
24087
24087
|
});
|
|
24088
24088
|
|
|
24089
|
+
// src/agents/shared/validation.ts
|
|
24090
|
+
function validateAgentForTier(agent, tier) {
|
|
24091
|
+
return agent.capabilities.supportedTiers.includes(tier);
|
|
24092
|
+
}
|
|
24093
|
+
function validateAgentFeature(agent, feature) {
|
|
24094
|
+
return agent.capabilities.features.has(feature);
|
|
24095
|
+
}
|
|
24096
|
+
function describeAgentCapabilities(agent) {
|
|
24097
|
+
const tiers = agent.capabilities.supportedTiers.join(",");
|
|
24098
|
+
const features = Array.from(agent.capabilities.features).join(",");
|
|
24099
|
+
const maxTokens = agent.capabilities.maxContextTokens;
|
|
24100
|
+
return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
|
|
24101
|
+
}
|
|
24102
|
+
|
|
24103
|
+
// src/agents/shared/version-detection.ts
|
|
24104
|
+
async function getAgentVersion(binaryName) {
|
|
24105
|
+
try {
|
|
24106
|
+
const proc = _versionDetectionDeps.spawn([binaryName, "--version"], {
|
|
24107
|
+
stdout: "pipe",
|
|
24108
|
+
stderr: "pipe"
|
|
24109
|
+
});
|
|
24110
|
+
const exitCode = await proc.exited;
|
|
24111
|
+
if (exitCode !== 0) {
|
|
24112
|
+
return null;
|
|
24113
|
+
}
|
|
24114
|
+
const stdout = await new Response(proc.stdout).text();
|
|
24115
|
+
const versionLine = stdout.trim().split(`
|
|
24116
|
+
`)[0];
|
|
24117
|
+
const versionMatch = versionLine.match(/v?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
|
|
24118
|
+
if (versionMatch) {
|
|
24119
|
+
return versionMatch[0];
|
|
24120
|
+
}
|
|
24121
|
+
return versionLine || null;
|
|
24122
|
+
} catch {
|
|
24123
|
+
return null;
|
|
24124
|
+
}
|
|
24125
|
+
}
|
|
24126
|
+
async function getAgentVersions() {
|
|
24127
|
+
const agents = await getInstalledAgents();
|
|
24128
|
+
const agentsByName = new Map(agents.map((a) => [a.name, a]));
|
|
24129
|
+
const { ALL_AGENTS: ALL_AGENTS2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
|
|
24130
|
+
const versions2 = await Promise.all(ALL_AGENTS2.map(async (agent) => {
|
|
24131
|
+
const version2 = agentsByName.has(agent.name) ? await getAgentVersion(agent.binary) : null;
|
|
24132
|
+
return {
|
|
24133
|
+
name: agent.name,
|
|
24134
|
+
displayName: agent.displayName,
|
|
24135
|
+
version: version2,
|
|
24136
|
+
installed: agentsByName.has(agent.name)
|
|
24137
|
+
};
|
|
24138
|
+
}));
|
|
24139
|
+
return versions2;
|
|
24140
|
+
}
|
|
24141
|
+
var _versionDetectionDeps;
|
|
24142
|
+
var init_version_detection = __esm(() => {
|
|
24143
|
+
init_registry();
|
|
24144
|
+
_versionDetectionDeps = {
|
|
24145
|
+
spawn(cmd, opts) {
|
|
24146
|
+
return Bun.spawn(cmd, opts);
|
|
24147
|
+
}
|
|
24148
|
+
};
|
|
24149
|
+
});
|
|
24150
|
+
|
|
24151
|
+
// src/agents/index.ts
|
|
24152
|
+
var exports_agents = {};
|
|
24153
|
+
__export(exports_agents, {
|
|
24154
|
+
validateAgentForTier: () => validateAgentForTier,
|
|
24155
|
+
validateAgentFeature: () => validateAgentFeature,
|
|
24156
|
+
parseTokenUsage: () => parseTokenUsage,
|
|
24157
|
+
getInstalledAgents: () => getInstalledAgents,
|
|
24158
|
+
getAllAgentNames: () => getAllAgentNames,
|
|
24159
|
+
getAgentVersions: () => getAgentVersions,
|
|
24160
|
+
getAgentVersion: () => getAgentVersion,
|
|
24161
|
+
getAgent: () => getAgent,
|
|
24162
|
+
formatCostWithConfidence: () => formatCostWithConfidence,
|
|
24163
|
+
estimateCostFromTokenUsage: () => estimateCostFromTokenUsage,
|
|
24164
|
+
estimateCostFromOutput: () => estimateCostFromOutput,
|
|
24165
|
+
estimateCostByDuration: () => estimateCostByDuration,
|
|
24166
|
+
estimateCost: () => estimateCost,
|
|
24167
|
+
describeAgentCapabilities: () => describeAgentCapabilities,
|
|
24168
|
+
checkAgentHealth: () => checkAgentHealth,
|
|
24169
|
+
MODEL_PRICING: () => MODEL_PRICING,
|
|
24170
|
+
CompleteError: () => CompleteError,
|
|
24171
|
+
ClaudeCodeAdapter: () => ClaudeCodeAdapter,
|
|
24172
|
+
COST_RATES: () => COST_RATES
|
|
24173
|
+
});
|
|
24174
|
+
var init_agents = __esm(() => {
|
|
24175
|
+
init_types2();
|
|
24176
|
+
init_claude();
|
|
24177
|
+
init_registry();
|
|
24178
|
+
init_cost();
|
|
24179
|
+
init_version_detection();
|
|
24180
|
+
});
|
|
24181
|
+
|
|
24089
24182
|
// src/pipeline/event-bus.ts
|
|
24090
24183
|
class PipelineEventBus {
|
|
24091
24184
|
subscribers = new Map;
|
|
@@ -24496,8 +24589,87 @@ async function recheckReview(ctx) {
|
|
|
24496
24589
|
const result = await reviewStage2.execute(ctx);
|
|
24497
24590
|
return result.action === "continue";
|
|
24498
24591
|
}
|
|
24592
|
+
function collectFailedChecks(ctx) {
|
|
24593
|
+
return (ctx.reviewResult?.checks ?? []).filter((c) => !c.success);
|
|
24594
|
+
}
|
|
24595
|
+
function buildReviewRectificationPrompt(failedChecks, story) {
|
|
24596
|
+
const errors3 = failedChecks.map((c) => `## ${c.check} errors (exit code ${c.exitCode})
|
|
24597
|
+
\`\`\`
|
|
24598
|
+
${c.output}
|
|
24599
|
+
\`\`\``).join(`
|
|
24600
|
+
|
|
24601
|
+
`);
|
|
24602
|
+
return `You are fixing lint/typecheck errors from a code review.
|
|
24603
|
+
|
|
24604
|
+
Story: ${story.title} (${story.id})
|
|
24605
|
+
|
|
24606
|
+
The following quality checks failed after implementation:
|
|
24607
|
+
|
|
24608
|
+
${errors3}
|
|
24609
|
+
|
|
24610
|
+
Fix ALL errors listed above. Do NOT change test files or test behavior.
|
|
24611
|
+
Do NOT add new features \u2014 only fix the quality check errors.
|
|
24612
|
+
Commit your fixes when done.`;
|
|
24613
|
+
}
|
|
24614
|
+
async function runAgentRectification(ctx) {
|
|
24615
|
+
const logger = getLogger();
|
|
24616
|
+
const maxAttempts = ctx.config.quality.autofix?.maxAttempts ?? 2;
|
|
24617
|
+
const failedChecks = collectFailedChecks(ctx);
|
|
24618
|
+
if (failedChecks.length === 0) {
|
|
24619
|
+
logger.debug("autofix", "No failed checks found \u2014 skipping agent rectification", { storyId: ctx.story.id });
|
|
24620
|
+
return false;
|
|
24621
|
+
}
|
|
24622
|
+
logger.info("autofix", "Starting agent rectification for review failures", {
|
|
24623
|
+
storyId: ctx.story.id,
|
|
24624
|
+
failedChecks: failedChecks.map((c) => c.check),
|
|
24625
|
+
maxAttempts
|
|
24626
|
+
});
|
|
24627
|
+
const agentGetFn = ctx.agentGetFn ?? getAgent;
|
|
24628
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
24629
|
+
logger.info("autofix", `Agent rectification attempt ${attempt}/${maxAttempts}`, { storyId: ctx.story.id });
|
|
24630
|
+
const agent = agentGetFn(ctx.config.autoMode.defaultAgent);
|
|
24631
|
+
if (!agent) {
|
|
24632
|
+
logger.error("autofix", "Agent not found \u2014 cannot run agent rectification", { storyId: ctx.story.id });
|
|
24633
|
+
return false;
|
|
24634
|
+
}
|
|
24635
|
+
const prompt = buildReviewRectificationPrompt(failedChecks, ctx.story);
|
|
24636
|
+
const modelTier = ctx.story.routing?.modelTier ?? ctx.config.autoMode.escalation.tierOrder[0]?.tier ?? "balanced";
|
|
24637
|
+
const modelDef = resolveModel(ctx.config.models[modelTier]);
|
|
24638
|
+
await agent.run({
|
|
24639
|
+
prompt,
|
|
24640
|
+
workdir: ctx.workdir,
|
|
24641
|
+
modelTier,
|
|
24642
|
+
modelDef,
|
|
24643
|
+
timeoutSeconds: ctx.config.execution.sessionTimeoutSeconds,
|
|
24644
|
+
dangerouslySkipPermissions: resolvePermissions(ctx.config, "rectification").skipPermissions,
|
|
24645
|
+
pipelineStage: "rectification",
|
|
24646
|
+
config: ctx.config,
|
|
24647
|
+
maxInteractionTurns: ctx.config.agent?.maxInteractionTurns,
|
|
24648
|
+
storyId: ctx.story.id,
|
|
24649
|
+
sessionRole: "implementer"
|
|
24650
|
+
});
|
|
24651
|
+
const passed = await _autofixDeps.recheckReview(ctx);
|
|
24652
|
+
if (passed) {
|
|
24653
|
+
logger.info("autofix", `[OK] Agent rectification succeeded on attempt ${attempt}`, {
|
|
24654
|
+
storyId: ctx.story.id
|
|
24655
|
+
});
|
|
24656
|
+
return true;
|
|
24657
|
+
}
|
|
24658
|
+
const updatedFailed = collectFailedChecks(ctx);
|
|
24659
|
+
if (updatedFailed.length > 0) {
|
|
24660
|
+
failedChecks.splice(0, failedChecks.length, ...updatedFailed);
|
|
24661
|
+
}
|
|
24662
|
+
logger.warn("autofix", `Agent rectification still failing after attempt ${attempt}`, {
|
|
24663
|
+
storyId: ctx.story.id
|
|
24664
|
+
});
|
|
24665
|
+
}
|
|
24666
|
+
logger.warn("autofix", "Agent rectification exhausted", { storyId: ctx.story.id });
|
|
24667
|
+
return false;
|
|
24668
|
+
}
|
|
24499
24669
|
var autofixStage, _autofixDeps;
|
|
24500
24670
|
var init_autofix = __esm(() => {
|
|
24671
|
+
init_agents();
|
|
24672
|
+
init_config();
|
|
24501
24673
|
init_logger2();
|
|
24502
24674
|
init_event_bus();
|
|
24503
24675
|
autofixStage = {
|
|
@@ -24523,14 +24695,7 @@ var init_autofix = __esm(() => {
|
|
|
24523
24695
|
}
|
|
24524
24696
|
const lintFixCmd = ctx.config.quality.commands.lintFix;
|
|
24525
24697
|
const formatFixCmd = ctx.config.quality.commands.formatFix;
|
|
24526
|
-
if (
|
|
24527
|
-
logger.debug("autofix", "No fix commands configured \u2014 skipping autofix", { storyId: ctx.story.id });
|
|
24528
|
-
return { action: "escalate", reason: "Review failed and no autofix commands configured" };
|
|
24529
|
-
}
|
|
24530
|
-
const maxAttempts = ctx.config.quality.autofix?.maxAttempts ?? 2;
|
|
24531
|
-
let fixed = false;
|
|
24532
|
-
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
24533
|
-
logger.info("autofix", `Autofix attempt ${attempt}/${maxAttempts}`, { storyId: ctx.story.id });
|
|
24698
|
+
if (lintFixCmd || formatFixCmd) {
|
|
24534
24699
|
if (lintFixCmd) {
|
|
24535
24700
|
pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: lintFixCmd });
|
|
24536
24701
|
const lintResult = await _autofixDeps.runCommand(lintFixCmd, ctx.workdir);
|
|
@@ -24556,22 +24721,24 @@ var init_autofix = __esm(() => {
|
|
|
24556
24721
|
const recheckPassed = await _autofixDeps.recheckReview(ctx);
|
|
24557
24722
|
pipelineEventBus.emit({ type: "autofix:completed", storyId: ctx.story.id, fixed: recheckPassed });
|
|
24558
24723
|
if (recheckPassed) {
|
|
24559
|
-
if (ctx.reviewResult)
|
|
24724
|
+
if (ctx.reviewResult)
|
|
24560
24725
|
ctx.reviewResult = { ...ctx.reviewResult, success: true };
|
|
24561
|
-
}
|
|
24562
|
-
|
|
24563
|
-
break;
|
|
24726
|
+
logger.info("autofix", "Mechanical autofix succeeded \u2014 retrying review", { storyId: ctx.story.id });
|
|
24727
|
+
return { action: "retry", fromStage: "review" };
|
|
24564
24728
|
}
|
|
24565
24729
|
}
|
|
24566
|
-
|
|
24567
|
-
|
|
24730
|
+
const agentFixed = await _autofixDeps.runAgentRectification(ctx);
|
|
24731
|
+
if (agentFixed) {
|
|
24732
|
+
if (ctx.reviewResult)
|
|
24733
|
+
ctx.reviewResult = { ...ctx.reviewResult, success: true };
|
|
24734
|
+
logger.info("autofix", "Agent rectification succeeded \u2014 retrying review", { storyId: ctx.story.id });
|
|
24568
24735
|
return { action: "retry", fromStage: "review" };
|
|
24569
24736
|
}
|
|
24570
24737
|
logger.warn("autofix", "Autofix exhausted \u2014 escalating", { storyId: ctx.story.id });
|
|
24571
24738
|
return { action: "escalate", reason: "Autofix exhausted: review still failing after fix attempts" };
|
|
24572
24739
|
}
|
|
24573
24740
|
};
|
|
24574
|
-
_autofixDeps = { runCommand, recheckReview };
|
|
24741
|
+
_autofixDeps = { runCommand, recheckReview, runAgentRectification };
|
|
24575
24742
|
});
|
|
24576
24743
|
|
|
24577
24744
|
// src/execution/progress.ts
|
|
@@ -25719,99 +25886,6 @@ ${pluginMarkdown}` : pluginMarkdown;
|
|
|
25719
25886
|
};
|
|
25720
25887
|
});
|
|
25721
25888
|
|
|
25722
|
-
// src/agents/shared/validation.ts
|
|
25723
|
-
function validateAgentForTier(agent, tier) {
|
|
25724
|
-
return agent.capabilities.supportedTiers.includes(tier);
|
|
25725
|
-
}
|
|
25726
|
-
function validateAgentFeature(agent, feature) {
|
|
25727
|
-
return agent.capabilities.features.has(feature);
|
|
25728
|
-
}
|
|
25729
|
-
function describeAgentCapabilities(agent) {
|
|
25730
|
-
const tiers = agent.capabilities.supportedTiers.join(",");
|
|
25731
|
-
const features = Array.from(agent.capabilities.features).join(",");
|
|
25732
|
-
const maxTokens = agent.capabilities.maxContextTokens;
|
|
25733
|
-
return `${agent.name}: tiers=[${tiers}], maxTokens=${maxTokens}, features=[${features}]`;
|
|
25734
|
-
}
|
|
25735
|
-
|
|
25736
|
-
// src/agents/shared/version-detection.ts
|
|
25737
|
-
async function getAgentVersion(binaryName) {
|
|
25738
|
-
try {
|
|
25739
|
-
const proc = _versionDetectionDeps.spawn([binaryName, "--version"], {
|
|
25740
|
-
stdout: "pipe",
|
|
25741
|
-
stderr: "pipe"
|
|
25742
|
-
});
|
|
25743
|
-
const exitCode = await proc.exited;
|
|
25744
|
-
if (exitCode !== 0) {
|
|
25745
|
-
return null;
|
|
25746
|
-
}
|
|
25747
|
-
const stdout = await new Response(proc.stdout).text();
|
|
25748
|
-
const versionLine = stdout.trim().split(`
|
|
25749
|
-
`)[0];
|
|
25750
|
-
const versionMatch = versionLine.match(/v?(\d+\.\d+(?:\.\d+)?(?:[-+][\w.]+)?)/);
|
|
25751
|
-
if (versionMatch) {
|
|
25752
|
-
return versionMatch[0];
|
|
25753
|
-
}
|
|
25754
|
-
return versionLine || null;
|
|
25755
|
-
} catch {
|
|
25756
|
-
return null;
|
|
25757
|
-
}
|
|
25758
|
-
}
|
|
25759
|
-
async function getAgentVersions() {
|
|
25760
|
-
const agents = await getInstalledAgents();
|
|
25761
|
-
const agentsByName = new Map(agents.map((a) => [a.name, a]));
|
|
25762
|
-
const { ALL_AGENTS: ALL_AGENTS2 } = await Promise.resolve().then(() => (init_registry(), exports_registry));
|
|
25763
|
-
const versions2 = await Promise.all(ALL_AGENTS2.map(async (agent) => {
|
|
25764
|
-
const version2 = agentsByName.has(agent.name) ? await getAgentVersion(agent.binary) : null;
|
|
25765
|
-
return {
|
|
25766
|
-
name: agent.name,
|
|
25767
|
-
displayName: agent.displayName,
|
|
25768
|
-
version: version2,
|
|
25769
|
-
installed: agentsByName.has(agent.name)
|
|
25770
|
-
};
|
|
25771
|
-
}));
|
|
25772
|
-
return versions2;
|
|
25773
|
-
}
|
|
25774
|
-
var _versionDetectionDeps;
|
|
25775
|
-
var init_version_detection = __esm(() => {
|
|
25776
|
-
init_registry();
|
|
25777
|
-
_versionDetectionDeps = {
|
|
25778
|
-
spawn(cmd, opts) {
|
|
25779
|
-
return Bun.spawn(cmd, opts);
|
|
25780
|
-
}
|
|
25781
|
-
};
|
|
25782
|
-
});
|
|
25783
|
-
|
|
25784
|
-
// src/agents/index.ts
|
|
25785
|
-
var exports_agents = {};
|
|
25786
|
-
__export(exports_agents, {
|
|
25787
|
-
validateAgentForTier: () => validateAgentForTier,
|
|
25788
|
-
validateAgentFeature: () => validateAgentFeature,
|
|
25789
|
-
parseTokenUsage: () => parseTokenUsage,
|
|
25790
|
-
getInstalledAgents: () => getInstalledAgents,
|
|
25791
|
-
getAllAgentNames: () => getAllAgentNames,
|
|
25792
|
-
getAgentVersions: () => getAgentVersions,
|
|
25793
|
-
getAgentVersion: () => getAgentVersion,
|
|
25794
|
-
getAgent: () => getAgent,
|
|
25795
|
-
formatCostWithConfidence: () => formatCostWithConfidence,
|
|
25796
|
-
estimateCostFromTokenUsage: () => estimateCostFromTokenUsage,
|
|
25797
|
-
estimateCostFromOutput: () => estimateCostFromOutput,
|
|
25798
|
-
estimateCostByDuration: () => estimateCostByDuration,
|
|
25799
|
-
estimateCost: () => estimateCost,
|
|
25800
|
-
describeAgentCapabilities: () => describeAgentCapabilities,
|
|
25801
|
-
checkAgentHealth: () => checkAgentHealth,
|
|
25802
|
-
MODEL_PRICING: () => MODEL_PRICING,
|
|
25803
|
-
CompleteError: () => CompleteError,
|
|
25804
|
-
ClaudeCodeAdapter: () => ClaudeCodeAdapter,
|
|
25805
|
-
COST_RATES: () => COST_RATES
|
|
25806
|
-
});
|
|
25807
|
-
var init_agents = __esm(() => {
|
|
25808
|
-
init_types2();
|
|
25809
|
-
init_claude();
|
|
25810
|
-
init_registry();
|
|
25811
|
-
init_cost();
|
|
25812
|
-
init_version_detection();
|
|
25813
|
-
});
|
|
25814
|
-
|
|
25815
25889
|
// src/tdd/isolation.ts
|
|
25816
25890
|
function isTestFile(filePath) {
|
|
25817
25891
|
return TEST_PATTERNS.some((pattern) => pattern.test(filePath));
|
package/package.json
CHANGED
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
* Autofix Stage (ADR-005, Phase 2)
|
|
4
4
|
*
|
|
5
5
|
* Runs after a failed review stage. Attempts to fix quality issues
|
|
6
|
-
* automatically
|
|
6
|
+
* automatically before escalating:
|
|
7
|
+
*
|
|
8
|
+
* Phase 1 — Mechanical fix: runs lintFix / formatFix commands (if configured)
|
|
9
|
+
* Phase 2 — Agent rectification: spawns an agent session with the review error
|
|
10
|
+
* output as context (reuses the pattern from rectification-loop.ts)
|
|
7
11
|
*
|
|
8
12
|
* Language-agnostic: uses quality.commands.lintFix / formatFix from config.
|
|
9
13
|
* No hardcoded tool names.
|
|
@@ -12,10 +16,15 @@
|
|
|
12
16
|
*
|
|
13
17
|
* Returns:
|
|
14
18
|
* - `retry` fromStage:"review" — autofix resolved the failures
|
|
15
|
-
* - `escalate` — max attempts exhausted or
|
|
19
|
+
* - `escalate` — max attempts exhausted or agent unavailable
|
|
16
20
|
*/
|
|
17
21
|
|
|
22
|
+
import { getAgent } from "../../agents";
|
|
23
|
+
import { resolveModel } from "../../config";
|
|
24
|
+
import { resolvePermissions } from "../../config/permissions";
|
|
18
25
|
import { getLogger } from "../../logger";
|
|
26
|
+
import type { UserStory } from "../../prd";
|
|
27
|
+
import type { ReviewCheckResult } from "../../review/types";
|
|
19
28
|
import { pipelineEventBus } from "../event-bus";
|
|
20
29
|
import type { PipelineContext, PipelineStage, StageResult } from "../types";
|
|
21
30
|
|
|
@@ -45,18 +54,8 @@ export const autofixStage: PipelineStage = {
|
|
|
45
54
|
const lintFixCmd = ctx.config.quality.commands.lintFix;
|
|
46
55
|
const formatFixCmd = ctx.config.quality.commands.formatFix;
|
|
47
56
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return { action: "escalate", reason: "Review failed and no autofix commands configured" };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const maxAttempts = ctx.config.quality.autofix?.maxAttempts ?? 2;
|
|
54
|
-
let fixed = false;
|
|
55
|
-
|
|
56
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
57
|
-
logger.info("autofix", `Autofix attempt ${attempt}/${maxAttempts}`, { storyId: ctx.story.id });
|
|
58
|
-
|
|
59
|
-
// Step 1: lint fix
|
|
57
|
+
// Phase 1: Mechanical fix (if commands are configured)
|
|
58
|
+
if (lintFixCmd || formatFixCmd) {
|
|
60
59
|
if (lintFixCmd) {
|
|
61
60
|
pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: lintFixCmd });
|
|
62
61
|
const lintResult = await _autofixDeps.runCommand(lintFixCmd, ctx.workdir);
|
|
@@ -69,7 +68,6 @@ export const autofixStage: PipelineStage = {
|
|
|
69
68
|
}
|
|
70
69
|
}
|
|
71
70
|
|
|
72
|
-
// Step 2: format fix
|
|
73
71
|
if (formatFixCmd) {
|
|
74
72
|
pipelineEventBus.emit({ type: "autofix:started", storyId: ctx.story.id, command: formatFixCmd });
|
|
75
73
|
const fmtResult = await _autofixDeps.runCommand(formatFixCmd, ctx.workdir);
|
|
@@ -82,22 +80,21 @@ export const autofixStage: PipelineStage = {
|
|
|
82
80
|
}
|
|
83
81
|
}
|
|
84
82
|
|
|
85
|
-
// Re-run review to check if fixed
|
|
86
83
|
const recheckPassed = await _autofixDeps.recheckReview(ctx);
|
|
87
84
|
pipelineEventBus.emit({ type: "autofix:completed", storyId: ctx.story.id, fixed: recheckPassed });
|
|
88
85
|
|
|
89
86
|
if (recheckPassed) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}
|
|
94
|
-
fixed = true;
|
|
95
|
-
break;
|
|
87
|
+
if (ctx.reviewResult) ctx.reviewResult = { ...ctx.reviewResult, success: true };
|
|
88
|
+
logger.info("autofix", "Mechanical autofix succeeded — retrying review", { storyId: ctx.story.id });
|
|
89
|
+
return { action: "retry", fromStage: "review" };
|
|
96
90
|
}
|
|
97
91
|
}
|
|
98
92
|
|
|
99
|
-
|
|
100
|
-
|
|
93
|
+
// Phase 2: Agent rectification — spawn agent with review error context
|
|
94
|
+
const agentFixed = await _autofixDeps.runAgentRectification(ctx);
|
|
95
|
+
if (agentFixed) {
|
|
96
|
+
if (ctx.reviewResult) ctx.reviewResult = { ...ctx.reviewResult, success: true };
|
|
97
|
+
logger.info("autofix", "Agent rectification succeeded — retrying review", { storyId: ctx.story.id });
|
|
101
98
|
return { action: "retry", fromStage: "review" };
|
|
102
99
|
}
|
|
103
100
|
|
|
@@ -134,7 +131,97 @@ async function recheckReview(ctx: PipelineContext): Promise<boolean> {
|
|
|
134
131
|
return result.action === "continue";
|
|
135
132
|
}
|
|
136
133
|
|
|
134
|
+
function collectFailedChecks(ctx: PipelineContext): ReviewCheckResult[] {
|
|
135
|
+
return (ctx.reviewResult?.checks ?? []).filter((c) => !c.success);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function buildReviewRectificationPrompt(failedChecks: ReviewCheckResult[], story: UserStory): string {
|
|
139
|
+
const errors = failedChecks
|
|
140
|
+
.map((c) => `## ${c.check} errors (exit code ${c.exitCode})\n\`\`\`\n${c.output}\n\`\`\``)
|
|
141
|
+
.join("\n\n");
|
|
142
|
+
|
|
143
|
+
return `You are fixing lint/typecheck errors from a code review.
|
|
144
|
+
|
|
145
|
+
Story: ${story.title} (${story.id})
|
|
146
|
+
|
|
147
|
+
The following quality checks failed after implementation:
|
|
148
|
+
|
|
149
|
+
${errors}
|
|
150
|
+
|
|
151
|
+
Fix ALL errors listed above. Do NOT change test files or test behavior.
|
|
152
|
+
Do NOT add new features — only fix the quality check errors.
|
|
153
|
+
Commit your fixes when done.`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function runAgentRectification(ctx: PipelineContext): Promise<boolean> {
|
|
157
|
+
const logger = getLogger();
|
|
158
|
+
const maxAttempts = ctx.config.quality.autofix?.maxAttempts ?? 2;
|
|
159
|
+
const failedChecks = collectFailedChecks(ctx);
|
|
160
|
+
|
|
161
|
+
if (failedChecks.length === 0) {
|
|
162
|
+
logger.debug("autofix", "No failed checks found — skipping agent rectification", { storyId: ctx.story.id });
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
logger.info("autofix", "Starting agent rectification for review failures", {
|
|
167
|
+
storyId: ctx.story.id,
|
|
168
|
+
failedChecks: failedChecks.map((c) => c.check),
|
|
169
|
+
maxAttempts,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const agentGetFn = ctx.agentGetFn ?? getAgent;
|
|
173
|
+
|
|
174
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
175
|
+
logger.info("autofix", `Agent rectification attempt ${attempt}/${maxAttempts}`, { storyId: ctx.story.id });
|
|
176
|
+
|
|
177
|
+
const agent = agentGetFn(ctx.config.autoMode.defaultAgent);
|
|
178
|
+
if (!agent) {
|
|
179
|
+
logger.error("autofix", "Agent not found — cannot run agent rectification", { storyId: ctx.story.id });
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const prompt = buildReviewRectificationPrompt(failedChecks, ctx.story);
|
|
184
|
+
const modelTier = ctx.story.routing?.modelTier ?? ctx.config.autoMode.escalation.tierOrder[0]?.tier ?? "balanced";
|
|
185
|
+
const modelDef = resolveModel(ctx.config.models[modelTier]);
|
|
186
|
+
|
|
187
|
+
await agent.run({
|
|
188
|
+
prompt,
|
|
189
|
+
workdir: ctx.workdir,
|
|
190
|
+
modelTier,
|
|
191
|
+
modelDef,
|
|
192
|
+
timeoutSeconds: ctx.config.execution.sessionTimeoutSeconds,
|
|
193
|
+
dangerouslySkipPermissions: resolvePermissions(ctx.config, "rectification").skipPermissions,
|
|
194
|
+
pipelineStage: "rectification",
|
|
195
|
+
config: ctx.config,
|
|
196
|
+
maxInteractionTurns: ctx.config.agent?.maxInteractionTurns,
|
|
197
|
+
storyId: ctx.story.id,
|
|
198
|
+
sessionRole: "implementer",
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const passed = await _autofixDeps.recheckReview(ctx);
|
|
202
|
+
if (passed) {
|
|
203
|
+
logger.info("autofix", `[OK] Agent rectification succeeded on attempt ${attempt}`, {
|
|
204
|
+
storyId: ctx.story.id,
|
|
205
|
+
});
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Refresh failed checks for next attempt
|
|
210
|
+
const updatedFailed = collectFailedChecks(ctx);
|
|
211
|
+
if (updatedFailed.length > 0) {
|
|
212
|
+
failedChecks.splice(0, failedChecks.length, ...updatedFailed);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
logger.warn("autofix", `Agent rectification still failing after attempt ${attempt}`, {
|
|
216
|
+
storyId: ctx.story.id,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
logger.warn("autofix", "Agent rectification exhausted", { storyId: ctx.story.id });
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
137
224
|
/**
|
|
138
225
|
* Injectable deps for testing.
|
|
139
226
|
*/
|
|
140
|
-
export const _autofixDeps = { runCommand, recheckReview };
|
|
227
|
+
export const _autofixDeps = { runCommand, recheckReview, runAgentRectification };
|