@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 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.1",
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("405c88a"))
22255
- return "405c88a";
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 (!lintFixCmd && !formatFixCmd) {
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
- fixed = true;
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
- if (fixed) {
24567
- logger.info("autofix", "Autofix succeeded \u2014 retrying review", { storyId: ctx.story.id });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nathapp/nax",
3
- "version": "0.46.1",
3
+ "version": "0.46.2",
4
4
  "description": "AI Coding Agent Orchestrator — loops until done",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 (lint, format) before escalating.
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 no fix commands
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
- if (!lintFixCmd && !formatFixCmd) {
49
- logger.debug("autofix", "No fix commands configured — skipping autofix", { storyId: ctx.story.id });
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
- // Update ctx.reviewResult so downstream stages see the corrected state
91
- if (ctx.reviewResult) {
92
- ctx.reviewResult = { ...ctx.reviewResult, success: true };
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
- if (fixed) {
100
- logger.info("autofix", "Autofix succeeded retrying review", { storyId: ctx.story.id });
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 };