- Book: Decoupled PHP — Clean and Hexagonal Architecture for Applications That Outlive the Framework
- Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
Twelve turns in, the agent reports success. You run the type checker and the terminal fills with errors, and now you are reading one diff that twelve turns wrote, trying to work out which of them broke the build.
Nobody runs the full CI after every turn. It's too slow, so the usual habit is to run nothing until the end. There is a middle ground: checkers that only redo the work a small change requires, because they cache what they already checked. For most of them the speed is one flag away, and each has one habit that makes it slow or quietly wrong.
Four of them (ESLint, Ruff, tsc and Cargo) keep their caches inside the project folder, so the first run in a fresh git worktree is cold. The second run is the fast one.
TypeScript and JavaScript: tsc and ESLint
1. tsc with --incremental.
npx tsc --noEmit --incremental --tsBuildInfoFile .tsbuildinfo
--incremental tells TypeScript to save information about the project graph to a .tsbuildinfo file, and the next run reuses it. It has worked together with --noEmit since TypeScript 4.0. Add .tsbuildinfo to .gitignore. It catches the error a per-file linter never sees: the agent changed a type or a signature in one file and missed a caller in another.
The mistake is passing it the changed files. The handbook is blunt about it: "When input files are specified on the command line, tsconfig.json files are ignored" (source). You lose your strict flags and your paths. Point tsc at the project and let the build info skip the rest. If you're on TypeScript 7, the native port, Microsoft reports full builds typically 8x to 12x faster (announcement), and a typescript-go collaborator confirmed that incremental mode "exists and works" there (discussion).
2. ESLint with --cache.
npx eslint --cache --cache-strategy content .
--cache makes ESLint store information about processed files "in order to only operate on the changed ones", in .eslintcache by default. The default strategy detects changes by file metadata. content compares file contents instead, which the docs suggest when modification times change without the contents changing, as they do during git operations. It enforces your config on only the files that changed.
Don't trust that cache for cross-file rules. It "does not track dependencies between files", so type-aware rules and rules that resolve imports can report stale results. Per turn, that's acceptable, because tsc covers the cross-file type errors. Before you merge, run npx eslint . once without the cache.
Python: Ruff and the mypy daemon
3. Ruff.
ruff check .
No flag needed. Ruff caches results in a .ruff_cache directory in the project root by default (settings). Its default rule set includes Pyflakes' F821 (undefined name) and F401 (unused import), the kind of leftover you get when code moves between modules.
Don't add --fix to the per-turn check. Ruff applies only safe fixes by default, but a check that edits files still puts changes into the next turn's diff that nobody asked the agent for. Report the error, and let the agent fix it.
4. mypy, through its daemon.
dmypy run --timeout 3600 -- .
The mypy daemon keeps program state from earlier runs in memory, and on a large codebase it can be "10 or more times faster" than plain mypy, especially for repeated runs after small edits. dmypy run starts the daemon if it isn't running, and restarts it when your configuration or mypy version changes. The first run processes all the code. --timeout shuts the daemon down after an hour idle. Without it, the daemon runs until you stop it. Calls that no longer match a signature show up here, and so does a None reaching code that expects a str.
Give every task its own daemon. A daemon "supports one user and one set of source files". If you run agents in parallel worktrees, each worktree gets its own daemon. Run dmypy stop when the task is done.

Check each turn as it lands, and the bad one stops at the gate on its own.
Go and Rust: go vet and cargo check
5. go vet, on the packages the turn touched.
go vet ./internal/billing
go vet reports suspicious constructs the compiler accepts: Printf arguments that don't match the format string, locks passed by value, a context cancel function that is never called, unreachable code. It works by heuristics, so not every report is a real problem. It exits non-zero when it reports one. The script further down works out the changed packages for you.
Vet won't look at the importers. It checks the packages you name, and a package that imports one of them isn't on that list. If the turn changed an exported signature, widen it to go vet ./....
6. cargo check.
cargo check --all-targets
cargo check compiles your package without the final code-generation step, so it finishes faster than cargo build. It saves metadata files and reuses them when the source hasn't changed. --all-targets includes tests, examples and benches, which agents edit too. Borrow-checker and type errors land here.
cargo check is not a full build. Some diagnostics are only emitted during code generation, so it never reports them. Leave cargo build and cargo test for the end of the task.
PHP and shell: PHPStan and ShellCheck
7. PHPStan, with its result cache.
vendor/bin/phpstan analyse
PHPStan keeps a result cache at %tmpDir%/resultCache.php. When a file changes, PHPStan analyses that file again, plus every file that calls or references its symbols. It catches calls to methods that don't exist and arguments of the wrong type, up to the rule level you set.
Same trap as tsc: don't pass it the changed files. "The list of paths passed to the analyse command should be the same to take advantage of the result cache." Put paths in phpstan.neon and run it with no arguments. Set tmpDir there too, to a folder inside the project such as var/cache/phpstan: by default PHPStan keeps its cache in the system temp directory (config reference), shared by every worktree on the machine. A change to PHP, PHPStan or the config triggers a full analysis, and PHPStan runs one at least every seven days anyway. Debug mode disables the cache.
8. ShellCheck, on the scripts the turn touched.
shellcheck scripts/deploy.sh
ShellCheck is a static analyser for shell scripts. The one you'll see most is SC2086: "Double quote to prevent globbing and word splitting", the unquoted $file that works until a filename has a space in it. It exits non-zero on warnings, so it can fail a check script.
It has no cache. Scope it by passing only the scripts the turn changed.
One script, run after every turn
Put the eight in one file and delete the lines for languages your repo doesn't use. It assumes each tool's config sits at the repo root. Cheap and narrow checks go first, and set -e stops at the first failure, so a lint error ends the run before the type checker starts.
#!/usr/bin/env bash
# scripts/check-turn.sh: fast checks after an agent turn.
# Delete the lines for languages this repo doesn't use.
set -eo pipefail
changed() { # files changed or added since HEAD
{ git diff --name-only --diff-filter=d HEAD
git ls-files --others --exclude-standard
} | sort -u | grep -E "$1" || true
}
go_dirs() { # ./dir for each changed .go file
changed '\.go$' | while IFS= read -r f; do
d=$(dirname "$f")
if [ "$d" = . ]; then echo .; else echo "./$d"; fi
done | sort -u
}
run_on() { # run CMD on stdin lines, if there are any
local args=()
while IFS= read -r x; do args+=("$x"); done
if [ "${#args[@]}" -gt 0 ]; then "$@" "${args[@]}"; fi
}
changed '\.(sh|bash)$' | run_on shellcheck
ruff check .
npx eslint --cache --cache-strategy content .
go_dirs | run_on go vet
npx tsc --noEmit --incremental --tsBuildInfoFile .tsbuildinfo
cargo check --all-targets
dmypy run --timeout 3600 -- .
vendor/bin/phpstan analyse
"Changed" here means since the last commit. On an agent branch with no commits yet, that's every turn so far, which is still far less than the whole repo. Commit after each green turn and it shrinks to one turn. I haven't timed this script on a real repo, so measure it on yours. If it takes more than a few seconds, check first for a line running without its cache.
When a check fails, you know which turn broke it
Running the checks per turn changes the question you ask when one goes red. The script passed after turn 6 and fails after turn 7, so the problem is in turn 7. You read that turn's diff, not twelve.
With plain git, that's the commit-after-green habit: git diff HEAD shows only what came after the last green commit, and git restore --staged --worktree . puts tracked files back the way that commit had them. Untracked files need git clean, so look before you run that.
I build Hermes IDE, so weigh this part with that in mind. Hermes saves every agent turn, and you can diff or restore any single turn, with undo. The "Agent replies" strip under a session has one chip per reply with the lines it changed, and clicking one shows its changes or restores your files to that point (changelog, 2.3.1). ⌘G (Ctrl+Shift+G on Windows and Linux) opens the Review Desk, which shows the changes by turn or by file.

The Review Desk (⌘G) in By turn view for a Claude Code task: two turns listed (T1: 3 files +69 −1, T2: 1 file +1 −0), turn 1's diff of src/db.ts adding a refunded status, the changed files and a commit message drafted from the turns.
In Hermes, your own checks can decide when a task is done. The default checks go in done_when, in a .hermes/worktree.toml in your repo (recipe docs):
# .hermes/worktree.toml
done_when = ["./scripts/check-turn.sh"]
Make the script executable and commit both files, so every branch has them.
Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It isn't affiliated with Nous Research, Anthropic, OpenAI or any agent vendor, and it doesn't make any checker faster. It adds the per-turn history, so a red check points at one turn to read or restore.
If this was useful
If you want to try the per-turn loop, download Hermes IDE, and if it saves you a bisect, star it on GitHub.
If your agents work in a PHP codebase, PHPStan catches the type errors a turn leaves behind. What keeps each turn small enough to read and restore is structure. Decoupled PHP is my book on keeping Laravel or Symfony at the edge, behind ports and adapters, so a change to a use case touches the use case and its test, not a dozen controllers.
Available on Kindle, Paperback, and Hardcover. English, German, and Japanese editions out now — Portuguese and Spanish coming soon.

Top comments (0)