Project 19: “The Auto-Fixer Loop” — Developer Experience (DX)
| Attribute | Value |
|---|---|
| File | KIRO_CLI_LEARNING_PROJECTS.md |
| Main Programming Language | Bash / JavaScript |
| Coolness Level | Level 3: Genuinely Clever |
| Difficulty | Level 2: Intermediate |
| Knowledge Area | Developer Experience (DX) |
What you’ll build: A PostToolUse hook that runs lint/format and forces retries on failure.
Why it teaches Feedback Loops: The AI cannot finish until the code is clean.
Success criteria:
- Lint errors prevent completion until fixed.
Real World Outcome
You’ll have an auto-fixer loop that enforces code quality by running linters and formatters after every file write, forcing Kiro to fix errors before proceeding. The hook creates a tight feedback loop where code quality is mandatory, not optional:
Successful write with auto-fix:
$ kiro "add a new React component for user profiles"
[Kiro writes UserProfile.tsx]
[PostToolUse Hook] Running linters...
✗ ESLint found 3 issues:
- Line 12: 'useState' is not defined (no-undef)
- Line 18: Missing return statement (consistent-return)
- Line 24: Unexpected console.log (no-console)
[Hook] Auto-fixing with eslint --fix...
✓ Fixed 1 issue automatically (console.log removed)
✗ 2 issues require manual fixes
[Hook] Returning error to Kiro with fix instructions...
Kiro: I see lint errors. Let me fix them.
[Kiro writes corrected UserProfile.tsx]
[PostToolUse Hook] Running linters...
✓ ESLint: All checks passed
✓ Prettier: Code formatted
✓ TypeScript: No type errors
[Hook] All quality checks passed. Proceeding.
Quality enforcement metrics:
$ node analyze-quality.js
Auto-Fixer Loop Report (Last 7 Days)
─────────────────────────────────────
Total File Writes: 342
First-Pass Clean: 89 (26%)
Required Fixes: 253 (74%)
- ESLint errors: 187
- Type errors: 98
- Format issues: 156
Average Fix Iterations: 1.8
Max Iterations (complex): 5
Quality Gate Success Rate: 100%
(Zero files merged with lint errors)
Time Saved:
- Pre-commit rejections prevented: 253
- Manual lint runs avoided: 342
- Broken builds prevented: 98
This hook ensures that Kiro never leaves behind broken or poorly formatted code—it fixes issues immediately or tries again until clean.
The Core Question You’re Answering
“How do I build a feedback loop that forces AI to fix code quality issues immediately instead of accumulating technical debt?”
Before you start coding, consider: Without enforcement, Kiro will happily write code with lint errors, type mismatches, and formatting inconsistencies. Developers then manually run npm run lint and find dozens of issues. An auto-fixer loop shifts quality left—errors are caught and fixed at write-time, not review-time. This project teaches you to build continuous quality gates that prevent bad code from ever existing.
Concepts You Must Understand First
Stop and research these before coding:
- PostToolUse Hook Timing
- When does PostToolUse execute (after tool completes or before result returns)?
- Can a PostToolUse hook modify tool outputs?
- How do you signal failure to trigger a retry?
- What is the maximum retry count before Kiro gives up?
- Book Reference: Kiro CLI documentation - Hook System Architecture
- ESLint and Prettier Integration
- How do you run ESLint programmatically via Node.js API?
- What is the difference between
--fixand manual fixes? - How do you combine ESLint and Prettier without conflicts?
- How do you handle auto-fix failures (unfixable rules)?
- Book Reference: ESLint documentation - Node.js API
- Error Return Codes and Retry Logic
- How does Kiro interpret non-zero exit codes from hooks?
- Should you return exit code 1 (error) or print to stderr?
- How do you pass fix instructions back to Kiro?
- How many times will Kiro retry before failing?
- Book Reference: Unix process exit codes - Advanced Linux Programming
- TypeScript Compiler API
- How do you run
tsc --noEmitto check types without emitting files? - How do you parse TypeScript errors into actionable messages?
- What is the performance cost of running the compiler on every write?
- Should you cache compilation results?
- Book Reference: “Programming TypeScript” by Boris Cherny - Ch. 10 (Compiler API)
- How do you run
Questions to Guide Your Design
Before implementing, think through these:
- Quality Gates
- Which checks should be mandatory (ESLint, types, format)?
- Should you run tests on every write or only on commit?
- How do you handle slow checks (type checking takes 5 seconds)?
- Do you fail fast (stop at first error) or collect all errors?
- Auto-Fix Strategy
- Which errors can be auto-fixed (format, imports) vs require manual fixes?
- Should you apply auto-fixes immediately or ask Kiro to review them?
- How do you avoid infinite loops (auto-fix introduces new errors)?
- What if auto-fix changes behavior (removing unused code)?
- Performance
- How do you avoid blocking Kiro for 10 seconds on every write?
- Should you run checks in parallel (lint + types + format)?
- Do you cache lint results for unchanged files?
- What is the acceptable latency for the feedback loop?
- Error Communication
- How do you format lint errors so Kiro understands how to fix them?
- Should you include line numbers, error codes, suggested fixes?
- How do you distinguish critical errors from warnings?
- Do you provide a “skip quality checks” escape hatch?
Thinking Exercise
Manual Fix Iteration Walkthrough
Before writing code, trace what happens when Kiro writes flawed code:
Iteration 1: Kiro writes component with errors
// UserProfile.tsx (written by Kiro)
export default function UserProfile() {
const [name, setName] = useState(''); // Missing React import
console.log('Rendering profile'); // Disallowed console.log
// Missing return statement
}
Hook Analysis:
- ESLint detects 3 errors
- Auto-fix can remove
console.log - Remaining 2 errors require code changes
Hook returns exit code 1 with message:
ESLint errors found:
- Line 2: 'useState' is not defined. Add: import { useState } from 'react'
- Line 5: Missing return statement. Add JSX return.
Iteration 2: Kiro fixes errors
// UserProfile.tsx (corrected by Kiro)
import { useState } from 'react';
export default function UserProfile() {
const [name, setName] = useState('');
return <div>Profile: {name}</div>;
}
Hook Analysis:
- ESLint: PASS
- TypeScript: PASS (if tsconfig allows implicit any)
- Prettier: Needs formatting
Hook applies Prettier:
// Auto-formatted
import { useState } from 'react';
export default function UserProfile() {
const [name, setName] = useState('');
return <div>Profile: {name}</div>;
}
Hook returns exit code 0 (success)
Questions while tracing:
- How many iterations is acceptable before giving up?
- Should you auto-apply Prettier or ask Kiro to format?
- What if TypeScript errors require type annotations—can you guide Kiro?
- How do you detect an infinite loop (same error every iteration)?
The Interview Questions They’ll Ask
Prepare to answer these:
- “How would you design a retry mechanism that prevents infinite loops while still giving the AI enough attempts to fix complex errors?”
- “Explain the performance tradeoff between running ESLint on every file write vs caching and invalidating results.”
- “How do you integrate multiple linters (ESLint, Prettier, TSC) without conflicts, especially when their rules contradict?”
- “What strategies would you use to communicate lint errors to an AI in a way that maximizes fix success rate?”
- “How would you implement incremental type checking to avoid re-checking the entire project on every file write?”
- “Explain how you would handle a scenario where auto-fix introduces new errors (e.g., removing ‘unused’ code that’s actually needed).”
Hints in Layers
Hint 1: Start with One Linter
Begin with ESLint only. Run eslint --format json path/to/file.ts and parse the output. Return exit code 1 if errors exist, 0 if clean. Get this working before adding TypeScript or Prettier.
Hint 2: Parse Tool Output to Extract File Path The PostToolUse event includes the tool name and arguments. For the Edit/Write tools, extract the file path:
const event = JSON.parse(stdin);
if (event.tool.name === 'Edit' || event.tool.name === 'Write') {
const filePath = event.tool.input.file_path;
runLinters(filePath);
}
Hint 3: Structured Error Messages for Kiro Format errors as actionable instructions:
❌ ESLint Error (no-undef):
File: src/components/UserProfile.tsx
Line: 12
Error: 'useState' is not defined
Fix: Add import at top: import { useState } from 'react';
Hint 4: Performance - Run Checks in Parallel
Use Promise.all() to run ESLint, TypeScript, and Prettier simultaneously:
const [eslintResult, tscResult, prettierResult] = await Promise.all([
runESLint(filePath),
runTypeScript(filePath),
runPrettier(filePath)
]);
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| ESLint Integration | ESLint official docs | Node.js API, Custom Rules |
| Hook System | Kiro CLI docs | Hooks System, Event Schemas |
| Process Exit Codes | “Advanced Linux Programming” by CodeSourcery | Ch. 3 (Processes), Ch. 11 (Error Handling) |
| TypeScript Compiler | “Programming TypeScript” by Boris Cherny | Ch. 10 (Compiler API) |
| Prettier Integration | Prettier official docs | API Reference, Editor Integration |
Common Pitfalls & Debugging
Problem 1: “Hook runs forever, infinite retry loop”
- Why: Auto-fix introduces new errors, or Kiro keeps making the same mistake
- Fix: Track error signatures and exit after 3 identical errors:
const seen = new Set(); const errorSig = JSON.stringify(errors); if (seen.has(errorSig)) { console.error("Same errors after fix. Aborting."); process.exit(0); // Let it through to avoid infinite loop } seen.add(errorSig); - Quick test: Write a file with an unfixable error, verify hook exits gracefully
Problem 2: “Type checking is too slow (10+ seconds per write)”
- Why: Running
tscon entire project for every file write - Fix: Use incremental compilation or project references:
tsc --noEmit --incremental --tsBuildInfoFile .tsbuildinfo - Quick test: Time hook execution—should be <2 seconds for 95% of files
Problem 3: “Prettier and ESLint conflict (format changes fail lint)”
- Why: ESLint has formatting rules that contradict Prettier
- Fix: Disable ESLint formatting rules:
npm install --save-dev eslint-config-prettierAdd to
.eslintrc.json:"extends": ["prettier"] - Quick test: Format with Prettier, then run ESLint—no errors
Problem 4: “Hook doesn’t trigger for certain file writes”
- Why: Hook only listens for Edit tool, but Kiro used Write tool
- Fix: Handle both Edit and Write tools:
if (['Edit', 'Write', 'MultiEdit'].includes(event.tool.name)) { runLinters(event.tool.input.file_path); } - Quick test: Use both Edit and Write tools, verify hook runs for both
Problem 5: “Kiro ignores fix instructions, doesn’t retry”
- Why: Error message is unstructured or exit code is wrong
- Fix: Return exit code 1 and print structured errors to stdout (not stderr):
if (errors.length > 0) { console.log(formatErrorsForKiro(errors)); process.exit(1); // Trigger retry } - Quick test: Manually trigger hook with errors, verify Kiro receives message
Definition of Done
- Hook intercepts all Edit/Write/MultiEdit tool calls
- ESLint runs on every file write and detects errors
- Auto-fix is applied for fixable rules (no-console, quotes, etc.)
- TypeScript compiler checks types (tsc –noEmit) for .ts/.tsx files
- Prettier formats code after all checks pass
- Hook returns exit code 1 with actionable errors if checks fail
- Infinite loop detection prevents >3 retries on identical errors
- Hook completes in <2 seconds for 95% of files
- ESLint and Prettier configs are conflict-free
- Errors are formatted in a way that Kiro can parse and fix
- Documentation explains how to customize linter rules and disable checks