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:

  1. 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
  2. ESLint and Prettier Integration
    • How do you run ESLint programmatically via Node.js API?
    • What is the difference between --fix and 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
  3. 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
  4. TypeScript Compiler API
    • How do you run tsc --noEmit to 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)

Questions to Guide Your Design

Before implementing, think through these:

  1. 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?
  2. 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)?
  3. 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?
  4. 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:

  1. “How would you design a retry mechanism that prevents infinite loops while still giving the AI enough attempts to fix complex errors?”
  2. “Explain the performance tradeoff between running ESLint on every file write vs caching and invalidating results.”
  3. “How do you integrate multiple linters (ESLint, Prettier, TSC) without conflicts, especially when their rules contradict?”
  4. “What strategies would you use to communicate lint errors to an AI in a way that maximizes fix success rate?”
  5. “How would you implement incremental type checking to avoid re-checking the entire project on every file write?”
  6. “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 tsc on 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-prettier
    

    Add 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