01

Under the Hood: How AI Coding Tools Actually Work

This course teaches transferable skills for steering AI coding tools well — the mental model and controls stay the same no matter which tool or company you use.

Two failures that look like competence

You typed a prompt, hit enter, and the AI confidently wrote code that imported a package that does not exist. Here is the machinery that produced that — and the exact controls that would have caught it.

📦
The fabricated package

An AI tool wrote import faiss_cpu_optimized — a package that does not exist on PyPI. Attackers pre-register plausible names like this to sneak malware into your dependencies — an attack class called slopsquatting.

🗑️
The wiped database

In a widely-reported incident, an agent with unrestricted write access to a production database fired a destructive command despite a code-freeze instruction — wiping more than 1,200 users’ data.

~20%

Reference phantom packages

of AI-generated code samples import packages that do not exist. (Lasso Security / arXiv, 2024)

~45%

Contain security flaws

of AI-generated code ships with a security vulnerability. (Veracode, 2025)

“Writes code with complete conviction — including bugs or nonsense — and won’t tell you something is wrong unless you catch it.”
— Addy Osmani, Engineering Manager at Google Chrome (2026)

What an LLM actually does (and doesn’t)

Think of the model as a fluent improviser who never breaks character: it always produces confident, plausible output — with no internal fact-checker. Here is the four-step pipeline behind every response.

1

Tokenization
Text becomes subword chunks. The model has no concept of a whole word.

2

Embeddings
Each token becomes a vector. Meaning turns into geometry.

3

Attention
Weighs which tokens matter for predicting the next one.

4

Sampling
Draws one token from a probability distribution — so the same prompt can give different output each run.

⚠️
The model has no correctness check

It produces what is statistically likely — not what is true. That single fact explains hallucinated packages, invented methods, and plausible-but-wrong logic.

CODE
import faiss_cpu_optimized   # looks real, is not on PyPI
PLAIN ENGLISH

The model saw thousands of import <something>_optimized lines during training.

faiss_cpu_optimized is a statistically plausible name, so it generated it.

Zero check that the package actually exists — that is the whole failure.

Every turn resends everything

The context window is everything the model can see this turn — system instructions, your rules files, the conversation so far, files you pulled in. What is not in the window does not exist for that turn.

1
Turn 1 ≈ 1 message

Your first prompt goes in.

2
Turn 2 ≈ 3 messages

Your prompt + the reply + your new prompt — all resent.

3
Turn 3 ≈ 5 messages

The full conversation grows every exchange until it hits the window limit.

Prompt caching saves the computed state of stable content so only new messages are processed fresh — cutting latency and cost. But it silently resets on any of these:

/compactCompaction rewrites the conversation, so the cached prefix no longer matches.
switch modelA different model has a different cache — the whole stable context is reprocessed.
change MCPAdding or removing an MCP server alters the tool list baked into the prefix.
TTL expiryCaches live for a limited time; after it lapses the state is rebuilt.
new sessionA fresh session starts with nothing cached.

Four ways context goes bad

More context is not always better. A lean, relevant window beats a bloated one — here are the four failure modes to watch for (Drew Breunig, 2025).

☠️

Poisoning

A hallucination or error enters the context and gets referenced again and again.

🌫️

Distraction

Context grows so long the model over-focuses on it and neglects what it learned in training.

🎭

Confusion

Superfluous content in the context drags the quality of the response down.

⚔️

Clash

New information or tools conflict with things already in the context.

You didn’t send a prompt; you started a loop

“My preferred definition of an LLM agent is something that runs tools in a loop to achieve a goal.”
— Simon Willison, co-creator of Django (2025)

Reframe it: not “I sent the model a question” but “I started a loop, and I can intervene at each of these four stages.” Step through the agent loop below.

⚙️
Setup
📋
Input Assembly
🤖🔧
Model ↔ Tool
🏁
Loop End
Click “Next Step” to begin

Both incidents from the first screen broke down in Stage 3. The fabricated install and the destructive database command both happened when a tool ran without a gate or a scope to stop it.

The controls — and a 3-question diagnostic

The harness is the director and stage crew around the improviser: it is how you add control. Each loop stage exposes its own levers.

SetupModel choice, rules files (such as AGENTS.md or CLAUDE.md), MCP servers, and ignore filters.
AssemblyExplicit file references (@-mentions) and input hooks.
Model ↔ ToolPermission modes (plan / ask / auto), tool scoping, and pre/post-tool hooks.
EndSession boundaries and handoff artifacts for the next run.
“If you give an agent a way to verify its work, it more often than not fixes its own mistakes and prevents regressions.”
— Mitchell Hashimoto, co-founder of HashiCorp

Here is a post-tool hook that catches the “invented method” class automatically — it re-runs your type-checker after every edit.

CODE
// settings for your harness — run the type-checker after each edit/write
{ "hooks": { "PostToolUse": [
  { "matcher": "edit|write", "command": "run-your-typecheck" }
] } }
PLAIN ENGLISH

A rule that fires after a tool runs.

Whenever the agent edits or writes any file…

…automatically run your type-checker.

If the AI invented a method that does not exist on a real class, the check fails immediately and the agent sees it — no human needed.

💡
The 3-question diagnostic

When AI output disappoints, stop guessing (“try a better prompt?”) and diagnose the failing stage.

1
What failed?

The model fabricated something · context was missing or stale · the agent overstepped.

2
Which control addresses it?

Rules · ignore filters · file refs · model selection · permission mode · tool scope · hooks · handoffs.

3
Where in the loop does it intervene?

Setup · Assembly · Model ↔ Tool · End.

Check your mental model

Three diagnostic scenarios. Pick the fix a working engineer would reach for.

Scenario 1

The agent keeps calling a config method that does not exist on your real class. Which control best prevents this next time?

Scenario 2

Mid-session you switch models to save cost, and suddenly latency spikes on the very next turn. Why?

Scenario 3

Your agent read a wrong fact early in the session and now keeps repeating it across turns. Which context-failure mode is this?

Next: Module 2 — Research → Plan → Implement, the operating pattern that puts these controls into practice.

02

The Operating Pattern: Research → Plan → Implement

Workflow beats prompting. A structured flow turns the model's slot-machine inconsistency into results you can review and repeat.

Same prompt, two very different pulls

The prompt "Add anchor links to the titles in the topics list page" was run twice by the same model. Same words, same code, two unrelated results.

🎰

Run 1

Touched every anchor on the page — not just the titles that were asked for.

🎰

Run 2

Invented an unrequested table of contents nobody asked for — different styling, different regex.

That inconsistency isn't the model being dumb. It's the workflow being absent — and here's the mechanism behind it.

🗨️
Natural language is ambiguous; code is not

Your sentence has many valid readings. The model must pick one, and it can't ask you about every fork.

🧩
The context window can't fit everything

It works from a partial view of the codebase, so it fills the blanks with plausible guesses.

🎰
Ad-hoc prompting is a slot machine

No matter how detailed the prompt, one shot at a big task is a gamble. The bigger the task, the bigger the risk of a bad pull.

"GitHub Copilot provides better results when assigned clear, well-scoped tasks." — GitHub best practices, 2026

Building a house, not pouring concrete and hoping

You survey the land, draw blueprints a contractor can build from, then construct. You don't pour the foundation and then decide where the kitchen goes. Three phases, three distinct jobs.

🔍

Research

Survey the site. Build a mental model of the problem.

Deliverable: understanding + a findings summary.

📝

Plan

Draw the blueprints. Compress intention into scope.

Deliverable: scope + acceptance criteria.

🔨

Implement

Construct. Execute with discipline — mostly mechanical.

Deliverable: working code + verification.

📏
The spine is fixed; the rigor scales

Each phase feeds the next, and skipping one creates rework. But each column can hold several activities for big work or collapse to almost nothing for a one-line change. You scale the effort to the task.

Research is questioning, not outsourced thinking

"The goal is to build a mental model of the problem space — not to produce output." — AgentPatterns, 2026

Research means asking follow-up questions until you understand, studying what already exists before proposing anything. A research prompt looks like a question, not a request for code.

PROMPT
How does authentication work in this app?
PLAIN ENGLISH

You're not asking it to fix anything yet.

You're asking it to read the code, find the existing patterns, and report back.

Now your plan is grounded in what's actually there, not in guesses.

💡
Why the question matters

In one real run, this exact question revealed the app had no client-side auth at all — a static site fronted by an upstream layer. A naive "fix the login" plan would have chased a problem that didn't exist.

The blueprint is a "Done when" list

A plan's core artifact is a list of checkable outcomes you can review before any code exists. Here's a real one.

PLAN
## Fragment Anchors Plan
Done when:
- Hash links work for page titles in the topics list
- Anchor IDs follow a consistent kebab-case naming pattern
- Deep links survive hash-router navigation
- No scope creep to other pages or unrelated features
PLAIN ENGLISH

A title for the work — small and specific.

Everything below is a checkable outcome, not a vibe.

The feature itself works on the right page.

IDs are kebab-case — predictable, not random each run.

Links still work after hash-router navigation.

A guardrail against scope creep — the fix for that runaway table of contents.

🔧
The executable-plan heuristic

A day-one teammate could finish this in about a day using only the plan plus repo access. That means small scope and enough concrete detail — paths, interfaces, Done-when — that they're never forced back into research.

Grounding changes the answer. The same auth prompt produced materially different plans with vs. without evidence attached.

No evidence

The model assumes how things work and plans against its guess.

Real artifacts attached

Feed it a captured network trace and a working request example — the plan matches reality. Then review it: does it address the original problem? Did I make a wrong assumption?

With good blueprints, construction is mechanical

"Implementation becomes mechanical when the research and plan are solid." — AgentPatterns, 2026

Each implementation prompt cites the plan plus the work item, so the model's job is execution, not re-architecture. Quality gates run automated-first, then human review against "Done when".

1
Automated gates first

Lint, type-check, tests. Let the agent fix its own hygiene.

2
Then human review against "Done when"

You check outcomes, not syntax. The plan already told you what "done" means.

3
When output is wrong, diagnose the miss

A small miss → adjust the prompt or chunk the scope. A systemic miss → revise the plan or return to research. Don't stack hacks.

The cost of being wrong is asymmetric. Where the error lives decides how much it hurts.

🔍

Research error

Re-read a file. Seconds.

📝

Plan error

Rewrite a paragraph. Minutes.

🔨

Implementation error

Revert, re-plan, re-implement — and burn context.

An AI plan is cheap. The wrong plan is expensive. Verify before you execute.

The handoff: passing blueprints between crews

Run each phase in a fresh session and carry a handoff artifact between them. Watch a research-to-implement handoff play out.

A good handoff is Distilled (conclusions only), Validation-carrying (the criteria that prove the next step worked), and Durable (survives compaction — re-loaded next session). If implementation fails, update the plan and retry. No need to re-research.

Scaling up: when the work is too big for one plan

Most product requirements don't arrive pre-broken-down for an agent. Extend the flow with two moves before you build.

🔎
Validate

Have the AI gap-find your own plan before you commit a sprint to it.

✂️
Decompose

Split into vertical-slice stories, not horizontal layers. Aim for 5–8 stories, each ~1–2 days, dependencies explicit and ordered.

A vertical slice is one behavior end-to-end; its tasks are the dependency-ordered build steps (model → schema → API → UI). Write the acceptance criteria as Given/When/Then.

😕

Bad story

"Add comment functionality."

No observable outcome. The model will fill the gaps — slot machine again.

😊

Good story

"Given a logged-in user, When they submit the comment form, Then the comment appears below the post with a timestamp."

VALIDATION PROMPT
Review this task breakdown for gaps and risks. For each: what could go wrong,
wrong assumptions, conflicts with existing architecture, scope creep, and per-task
risk (low/med/high). Flag any story likely over 2 days and suggest how to split it.
Focus on what could go wrong, not what could go right.
PLAIN ENGLISH

You're deliberately asking the model to attack your own plan.

You want the failure modes named — assumptions, conflicts, creep — not reassurance.

Oversized stories get flagged now, while splitting is cheap.

Decomposition errors are cheap to fix now, expensive once implementation starts.

✍️
Write the acceptance criteria before the ticket title

If you can't write the Given/When/Then, the story isn't ready. This one habit — even on a single epic's worth of stories — is the biggest lever on whether an agent builds the right thing.

Check your instinct

No definitions here — just judgment calls. Pick what a disciplined engineer would actually do.

Scenario 1

A one-line copy change in a footer: "© 2025" should read "© 2026". How much Research → Plan → Implement rigor does this deserve?

Scenario 2

On a large feature, the agent's implementation has come out wrong for the third time. Each retry, you tweaked the prompt a little more. What's the best next move?

Scenario 3

You have limited time to catch mistakes. An error in which phase is the most expensive to discover late?

03

Set the Agent Up to Win

Tools, rules, context, and a build that boots on the first try — the workspace is the agent's competence.

Your new colleague's first day

An agent that keeps asking the same question, retries the same command, or edits the wrong folder isn't broken — it's under-briefed. Everything it needs to succeed is something you can set up once.

Picture onboarding a new hire. Leave them the right setup and they ship in an hour; leave them nothing and they spend the day guessing. An agent is that new hire every session — with no memory of yesterday unless you wrote it down.

🪑
A clear desk

Working tools and a harness that can read the repo and run commands.

🗺️
A short map

Rules and docs that name the conventions and the gotchas up front.

🔌
A setup that boots first try

A build anyone — teammate, CI, or agent — can reproduce from a clean checkout.

The permission mode is your oversight dial

Before rules and docs, know the one control you touch every session. The permission mode sets a ceiling on what the agent may do without stopping to ask. The names differ by tool; the four categories are the same.

👁️
Plan / read-only

Reasons, never edits. Use it to survey the code or to lock a plan before any code moves.

Ask / default

Pauses for approval before each action. Use it on new or unfamiliar work.

✏️
Accept-edits

Edits flow automatically; still pauses for riskier operations like running shell commands. Use it for known-shape changes.

Auto / full

Full agentic execution, subject to safety checks. Use it for scoped tasks you have supervised before.

Match the mode to what you want to pause for — friction is a dial, not a personality. In most tools a single cycle key (like Shift+Tab) rotates through the modes.

Rules capture the correction you keep repeating

If you're telling the agent the same thing twice — "use our logger, not raw print statements", "tests live in a dedicated folder", "don't touch the legacy billing code" — that repetition is a signal. Capture it as a durable rules file such as AGENTS.md or CLAUDE.md.

The rules lifecycle — step through it

👁️
Notice
✏️
Draft
♻️
Rerun
✂️
Prune
Click "Next Step" to begin
💡
The kill-criterion

"Would removing this cause the agent to make mistakes? If not, cut it." Start with what your next task needs — not a comprehensive repo-wide manifest.

A rules file is one member of a family of durable configuration you can leave behind:

rules file Standing conventions the agent loads every session.
MCP Model Context Protocol: connects outside tools and data sources to the agent.
hook A script that runs automatically at a set point, e.g. formatting on every save.
subagent A helper agent the main agent delegates a focused task to.
skill A packaged, reusable procedure the agent loads on demand for a recurring task.

Write the behavior, not the intent

A rule the agent can act on names the exact behavior. Vague intent forces it to guess — and it guesses differently every time.

Specific

Use 2-space indentation · Run the test suite before committing · API handlers live in src/api/handlers/

Vague

Format code properly · Test your changes · Keep files organized

What earns a place — and what doesn't

📥

Include

Build and test commands it can't guess · style rules that differ from defaults · repo etiquette (branch naming, PR conventions) · non-obvious gotchas · pointers to other files.

🚫

Exclude

Anything it can infer by reading code · standard language conventions it already knows · long tutorials (link instead) · info that changes often · duplicated file contents · "write clean code."

One more thing worth writing down explicitly: what the agent is allowed to read. That is controlled by the tool's ignore or deny config — not by .gitignore, which only affects discovery, not agent access.

CONFIG
# in your agent config
permissions:
  deny:
    - "legacy/billing/**"
    - "**/*.env"
# or, tool-specific: .cursorignore
legacy/billing/
*.env
PLAIN ENGLISH

This is where you fence off files, not .gitignore.

The deny list is the agent's real read-access wall.

Keep it out of the legacy billing folder entirely.

And never let it read secrets like environment files.

Some tools use a dedicated ignore file instead —

same idea: name the folders that are off-limits,

so a curious agent can't wander into sensitive code.

Docs that pass the fresh-agent test

Judge an onboarding doc by one test: could a fresh session build and run one test using only this doc, in under ten minutes? Four moves measurably lift output — the deltas are from a study across 2,500+ repos.

🔢

Numbered workflows

Not "follow our deploy process" but "1. run preflight, 2. wait for green, 3. merge, 4. watch the dashboard 10 min."

+25% correctness · +20% completeness

📊

Decision tables

Where two valid patterns exist, name which is current and which is legacy, in a table.

+25% best-practices adherence

📋

Real code snippets

Copy a real 3–10 line shape from production once, with a pointer to more.

+20% code reuse

🔁

Every "don't" gets a "do"

Not "don't use the old package manager" but "use pnpm 9.x"; not "don't build HTTP clients directly" but "use the shared client from lib/http."

Halves warning-overhead failure

Two anti-patterns that look like "more help" but hurt

📚

Over-reading

Piles of unmapped docs; the agent reads everything "just in case" — one case: 12 docs, ~80,000 tokens for a two-line change.

−25% completeness · Fix: a short root that says which sub-doc to read for which task

🚧

Warning-only docs

30+ "don'ts" with no "do"; the agent walks the whole warning list before every single action.

~2× task time · Fix: pair every "don't" with a "do"

Both backfire for the same reason: frontier models reliably follow only ~150–200 instructions. As the count climbs, adherence degrades across all of them. Every line competes for the same budget.

Short root, progressive disclosure, pointers not copies

Structure the docs so the agent loads only what a task needs. A short root file acts as a table of contents; progressive disclosure keeps each topic in its own file.

AGENTS.md short root: stack, package manager, layout, 3–4 always-on rules (cap ~150 lines; some teams keep it under 60)
agent_docs/ one topic per file — read on demand
building.mdread only when building
running_tests.mdread only when testing
deploy.mdread only when deploying
here_be_dragons.mdthe non-obvious, business-critical gotchas
📄
Short root

Table of contents plus the handful of always-on rules.

🎚️
Progressive disclosure

One topic per file; the agent loads only what it needs right now.

🔗
Pointers, not copies

Link the scripts in package.json; don't paste them. The pointer never rots — the copy always does.

AGENTS.md
# AGENTS.md
- This service uses pnpm, not npm.
- Build commands live in package.json
  scripts — see there, do not duplicate.
- Never modify legacy/billing/ —
  pre-2022, business-critical.
- If you are touching payments,
  read agent_docs/payments.md first.
PLAIN ENGLISH

Four lines do the work of a page.

One fact the agent could never guess: the package manager.

One pointer: find the build commands where they already live, don't recreate them (the copy would rot).

One paired don't/do-by-pointing: the dragon — where not to go, and why it matters.

One map entry: which sub-doc to open for which task.

And keep docs living: any PR that changes a build, convention, or deploy step updates the doc in the same diff. Flag any doc unchanged for 90 days while the repo kept moving.

A deterministic build is a contract

"Local green is not enough." A build is deterministic only when a teammate, CI, or an agent can reproduce it from a clean checkout. Environment drift is why the same clone passes on two laptops and fails in CI. More discipline won't fix it; a process will.

1

Declare the canonical setup → build → test path in the repo

2

Prove it in clean environments — not just one laptop

3

Fix the shared path when local, CI, and agent results disagree

Different artifacts for different consumers

📖
README quickstart

For humans — a narrative quickstart to get oriented.

⚙️
Scripts, containers, CI config

The runnable path itself — often a devcontainer plus a lockfile so installs are idempotent.

🤖
AGENTS.md / CLAUDE.md

Session-persistent facts for the agent.

The most common silent blocker is a private registry: name the feed URL and the auth step in your docs — even if it's just "ask this person for the credential."

💡
Agent churn = a documentation gap

Agents don't ask questions to be polite — they ask because the info isn't there. If an agent asks the same thing twice, that answer belongs in the docs; if it retries a command, the prerequisites aren't clear enough. Verify by prompting a fresh, zero-context session: "Set up the environment and run the tests," and watch for churn. No churn → verified. Churn → you just found the gap.

Check your instinct

Four situations. Decide what a set-up-to-win engineer does before you reveal the answer.

Scenario 1

Your agent keeps re-asking where the integration tests live, and it retries the setup command twice every session. What is this telling you, and what's the fix?

Scenario 2

You want to be helpful, so you add 35 "don't do X" rules to the root doc. What most likely happens?

Scenario 3

You're reviewing a rule you added six months ago. How do you decide whether it stays?

Scenario 4

Your build commands change often. Which belongs in AGENTS.md — a 30-line copy of the build scripts, or a one-line pointer to where they live?

04

Reviewing & Driving AI-Authored Code

Bad human code looks bad. AI code is a high-quality counterfeit — built to pass inspection. Reviewing it takes a technique, not a vibe.

The counterfeit that passes at the counter

A cashier catches a crude fake in a glance. What gets through is the good counterfeit — right paper, right ink, matching the real bills around it. AI code fails the same way: it compiles, passes lint, reads like its neighbors, and is still wrong.

🪙

Scrolling and vibe-checking catch clumsy mistakes. AI errors are engineered to look competent, so they slip past exactly that. You need a different technique than “does this look okay?”

“AI-generated code is more dangerous to review than bad human-written code, because it fails in ways that look like competence.”

— Faros AI, 2025

“Participants who had access to an AI assistant wrote significantly less secure code — and were also more likely to believe they wrote secure code.”

— Perry et al., Stanford, 2023

The volume problem

+441%

median PR review time, year-over-year

31%

of PRs now merge with zero review

+51%

growth in PR size

1.7×

more issues than human-written code

Faros AI, 22,000-developer dataset, 2025. Volume went up; review capacity didn’t. So your review time has to become effective, not performative.

The five ways a counterfeit gets through

Every AI failure you review falls into one of these five families. Learn the tell for each and review stops being a vibe and becomes a checklist.

👻

Fake / hallucinated APIs

An import or method call that exists in no version of the library. Broken-import is a productivity bug; slopsquatting is a security one — attackers pre-register the invented name with malware.

Tell: check every new import against the lockfile and the library’s current docs.

🎭

Plausible-but-wrong logic

Matches the surrounding style but breaks an invariant: an off-by-one in a paginator, a guard clause with the comparison flipped, a filter that silently drops good data.

Tell: re-derive the invariant from the spec and check the diff against that — do not pattern-match the diff against the code around it.

🕳️

Missing error handling

The happy path works. The null, error, and timeout paths silently swallow the problem or crash the process.

Tell: for each new external call ask “what happens on null / error / timeout?” If the answer isn’t in the diff, the path is missing.

🫥

Vacuous tests

Tests that pass without ever exercising the code they claim to cover — the counterfeit with a fake anti-forgery strip.

Tell: delete the implementation line the test covers. If the test still passes, it’s vacuous.

🧩

Comprehension debt

A big diff accepted without reading; abstractions you can’t explain in plain English.

Tell: “Never commit code you can’t explain.” — Addy Osmani

Vacuous tests, up close

The most seductive counterfeit: a green test suite that proves nothing. Here a payment test looks thorough and asserts nothing real.

CODE
charge = jest.fn().mockResolvedValue({ ok: true });
processOrder(order);
expect(charge).toHaveBeenCalledWith(order.total);
PLAIN ENGLISH

Replace the payment client with a mock that always says “ok.”

Run the order.

Assert only that the mock got called the way we wired it. It never checks that the order was marked paid, saved, or that a receipt was sent — processOrder could be empty and this still passes.

Its siblings — same trick, different disguise

snapshot-only A snapshot test confirms the DOM shape didn’t change — a broken discount still matches the old shape.
doesn’t-throw “It didn’t crash” is not “it did the right thing.”
coverage-padding Chasing a coverage number with toBeDefined(), which happily accepts 0, NaN, or an empty object.
mirror test Asserts a helper was called — not that the result it returned is correct.
✂️
The one test that unmasks them all

Delete the behavior the test claims to cover. Does the test still pass? If yes, it was testing nothing.

Spot the counterfeit

This is plausible-but-wrong logic in the wild. It reads like tidy cleanup and matches the style around it. One line silently destroys valid data. Click the line you’d flag.

A routine that removes “empty” data series:

1 series_to_remove = []
2 for name, data in curves.items():
3 has_events = any(d.get("event") == 1 for d in data)
4 if not has_events:
5 series_to_remove.append(name)
6 for name in series_to_remove:
7 del curves[name]

Don’t inspect every bill equally

Sustained vigilance across a whole diff fails — attention runs out before the lines do. Tier the diff first, then read the high-value lines closely.

🔍

Tier 1 — read every line

Spec interpretation (asked vs. built), interfaces, error paths, security-sensitive logic, and every AI-authored test.

🎲

Tier 2 — sample

Boilerplate, generated code, scaffolding, repetitive transforms. Spot-check 1 in 5 or 1 in 10; stop when you’ve seen enough.

Tier 3 — trust the signal

Type-check output, the linter, import resolution, tests that genuinely exercise the path. Don’t re-read what the harness already proves.

When you choose not to read every line, say so in the review.

“Sampled the migrations, stopped after three.”  ·  “Trusted the type-check for the rename.” Now the author and the next reviewer know exactly what was actually read.

Driving a ticket: rig the checks before the writing

You don’t just review AI code — you drive tickets end-to-end. The daily loop:

1

Ticket

2

Plan

3

Implement

4

Describe

5

Review

6

PR

An “agent-sized” ticket fits one PR (target under ~400 lines changed), has acceptance criteria the harness can verify, and needs no simultaneous migration or cross-service deploy to test locally.

🎯

The single highest-leverage move: “Give the agent a way to verify its work.” Wire it before the agent starts writing — in three layers.

💬
Per-task — state it in the prompt

“Add integration tests for the order flow before declaring done; confirm the modal renders on a mobile viewport.”

🚧
Per-repo — an always-on gate

A rules-file line backed by a verification hook the agent can’t skip.

⚙️
Per-mechanism — the runnable check itself

The test command, a curl probe on a health route, or a browser-automation tool for UI.

CODE
# AGENTS.md
Before saying you are done: run `./scripts/check`.
If it fails, fix and re-run.
PLAIN ENGLISH

A rules file the agent reads on every task.

This turns “please verify” — a polite request the model may skip — into a standing contract.

Paired with a stop-hook, the agent literally cannot declare done on a red check.

You sign for every claim in the PR

⚠️
Agents hallucinate PR descriptions — confidently

Real cases include fabricated screenshots (a UI result the agent had no way to actually run) and fabricated commit hashes narrated in a summary. For every claim — test names, file references, “I checked X” — open the file and verify. If the agent drafted the description, edit at least one section before you sign your name.

A PR template tuned for AI diffs — one that lives on the source branch of your code host — turns the vague “looks good” into five targeted defenses:

CODE
## What & why — one paragraph + linked ticket
## Test plan
- Happy / Error / Edge: one concrete case each
## AI authorship
- [ ] Tools used: <which>
- [ ] I read every line of this diff myself
## Reviewer focus
- Read closely / Sample / Trust-signal: <files>
## Pre-flight
- [ ] New imports verified (no hallucinated packages)
- [ ] Searched repo for existing utilities
PLAIN ENGLISH

What & why — anchors the diff to the actual ask.

Test plan — defends against vacuous tests: name a real case per path.

AI authorship — defends against reviewer mis-calibration. The “I read every line” box is the thing your signature attests to.

Reviewer focus — defends against rubber-stamping by pre-tiering the diff for the next reader.

Pre-flight — catches hallucinated imports and duplicated utilities before review even starts.

📏
Keep PRs small

Review quality decays with size. A 200-line PR reviewed twice in 20 min beats a 1,200-line PR reviewed once in 90 min.

🧠
Use a fresh-context review

“A fresh context improves code review since the model won’t be biased toward code it just wrote.”

⚖️
Decide deliberately

Approve-with-comments by default; request-changes only for real blockers (it hard-blocks merge); reject rarely and always with a rationale — never a vibe rejection.

Check yourself

Four situations from the daily loop. Pick the move a calibrated reviewer makes.

Scenario 1

A test mocks the payment client to return success and asserts the client was called. Your teammate says it’s vacuous. What single check proves it?

Scenario 2

You’re handed a 900-line AI PR to review. What’s the best first move?

Scenario 3

The AI’s PR description says “verified the search filters correctly, see screenshot.” You know the agent had no running app. What do you do?

Scenario 4

You’re about to hand an agent a ticket. What’s the single highest-leverage thing you can do for quality?

05

The Deterministic Safety Net

Quality, testing, and post-deploy nets that catch mistakes automatically — before, during, and after you ship.

Cheap levers beat heroic review

Picture a trapeze artist working above a safety net. The performer is non-deterministic — even a great one slips. The net is deterministic: same spot every time, it never has to recognize the fall, it just catches.

🤸
The performer = the AI

Fast, creative, and fallible. You will never make it perfect.

🕸️
The net = formatters, tests, rollback gates

Mechanical and boring on purpose. It never moves, so it always catches.

📊
The only signal that correlated with productivity was code quality

"None of the things around volume of code or number of coding agents correlated with productivity. The only signal was code quality. Great code in the pattern means great code out." — Eno Reyes, CTO of Factory. Your codebase is the agent's training signal at inference time: it reads your existing files to learn your style, and the linter is its feedback loop when it gets it wrong.

💡
Never send an LLM to do a linter's job

LLMs are slow, expensive, and non-deterministic. Linters are fast and deterministic. Use each for what it is good at — and the net matters more, not less, when an AI is writing the code.

Formatter vs linter: determinism is the product

Two tools, two jobs. Running one through the other is the classic anti-pattern — keep the layers separate.

📐

Formatter (mechanical)

Whitespace, line breaks, quote style. Reprints the AST deterministically in milliseconds, near-zero config. Prettier, Black, gofmt, ruff format.

🔎

Linter (judgment)

Logic, naming, dead code, bug patterns. Emits diagnostics, takes seconds to minutes, hundreds of toggles. ESLint, Pylint, golangci-lint, ruff check.

🤝
The point of an opinionated formatter is not the style — it is giving up the argument

Adopt a vendor default and move on. Determinism buys you identical output for identical input (whoever or whatever wrote it), smaller diffs, invisible style so review focuses on logic, and faster onboarding.

The right gate at the right stage

1
IDE on-save

Formatter + fast lints. Zero-cost feedback while you type.

2
Pre-commit

Staged files only. Keep it under ~3 seconds or people bypass it.

3
Pre-push

Slower lints and the type-check, before code leaves your machine.

4
CI, required — the only contract

Everything, whole repo, check-only. This CI gate is the one that actually blocks a merge.

CODE
# CI: verify only — never rewrite files
prettier --check .
eslint .

# Agent post-edit hook: format every file it writes
on_file_write: prettier --write $FILE
PLAIN ENGLISH

In CI, run the formatter in --check mode: it reports if anything is misformatted but changes nothing.

Run the linter across the whole repo. Either check fails the build if it finds a problem.

 

Separately, wire a hook so the agent formats every file the instant it writes it — turning a polite request into a hard guarantee.

Paying down lint debt: the ratchet

You want a strict new rule. The codebase has 3,300 existing violations. Three ways forward — only one ships fast without blocking everyone.

💥

Big-bang

Fix everything in one PR. Only viable when the count is small — otherwise a merge nightmare.

🧭

Boy-scout

Fix violations on files you happen to touch. Slow and uneven — old code you never open never improves.

🔧

Ratchet ✓

Record today's counts, block any increase, and lower the cap as people fix old ones. New errors can never merge.

🔩
Start by stopping the bleeding

"When we ratchet errors, we ensure they trend downward in a steady, irreversible process… a new error cannot merge." One team went 3,300 → 0 in ten months this way. A mass-reformat need not wreck git blame: record the reformat commit hash in a blame-ignore file and blame skips over it.

Testing: draw the line, keep the harness deterministic

The test pyramid tells you how to spend your testing budget — most tests fast and low, a few broad and slow.

E2E
~10%
Integration ~20%
Unit ~70%
Unit ~70%

Fast and isolated. One function, no outside world. Milliseconds each.

🔗
Integration ~20%

An integration test checks the contract between 2+ components across one boundary; a failure pinpoints the module.

🌐
E2E ~10%

An E2E test drives a full business outcome, UI to database. Broad, slow, fragile — use sparingly.

AI-assisted test generation is a four-phase loop

1
Research

Read the source, its collaborators, and existing example tests.

2
Plan

Draft the assertions, fixtures, and edge cases before writing code.

3
Implement

Compile, run, score coverage, feed the errors back and iterate.

4
Human review

Assert on behavior, not implementation. Curate the golden set.

determinism Seeded fixtures, dynamic ports, random container names, fixed model versions, frozen clocks, named goldens. Randomness belongs inside the system under test — never in the setup.
real coverage Coverage % is not quality: one study found 31% of generated tests had no assertions. Require the focal method actually called, ≥1 behavior-checking assertion, and ≥1 negative path.

Flaky tests: own them, don't re-run them

A flaky test passes and fails on the same code. At scale the noise is enormous.

84%

of CI pass-to-fail transitions are flakes, not real regressions.

16%

of tests are flaky at scale.

150k+

dev-hours per year lost to flakiness.

Properties of a high-signal test

isolatedOwns its own state; no dependence on other tests or run order.
stable selectorsTarget data-testids or ARIA roles — never brittle CSS paths.
condition waitsWait for a state to be true, never a fixed sleep timer.
one scenarioDeterministic input, one behavior per test.

Triage decision rules

If… Then…
Fails once, passes on retryLog as a flake; do not block the merge.
Fail rate > 2%Open a ticket with a named owner; fix this sprint.
Fail rate > 5%Auto-quarantine; remove from the gate; 30-day SLA to fix or delete.
Catches a real intermittent bugDo not quarantine — it is a correctness issue.
🎯
Quarantine plus ownership beats heroic stability

No team eliminates flakiness. The ones that ship reliably have named owners, SLAs, and the discipline to fix-or-delete — instead of a "just re-run it" culture that only compounds the flake rate over time.

A smoke test is a behavioral assertion — not /health

Improve one piece of the net each iteration (n+1 > n). Since the AI ships more code than review can catch, the post-deploy net has to absorb the difference.

CODE
# What most pipelines call a "smoke test"
curl https://api.example.com/health
# -> 200 OK  -> "deploy succeeded"

# An actual behavioral smoke test
POST /checkout {cart} -> 200 {order_id: "…"}
PLAIN ENGLISH

Hitting /health only proves the process booted and routes answer — that is liveness.

Nothing has been asserted about whether the service does its job.

 

A real smoke test exercises one production-critical path and checks the result: checkout returns a well-formed order ID; login returns a session with the expected claims; search returns hits for a known query.

Seven shapes exist — API contract, user journey, integration, data-shape, background job, agent/LLM path, and a negative permission check that asserts an unauthorized request is still rejected.

The hero move: canary deploy → auto-rollback

🐤
Deploy canary
💨
Smoke canary
🚀
Promote to prod
💨
Smoke prod
↩️
Auto-rollback
Click "Next Step" to trace a safe deploy

A canary limits the blast radius: a bad version only touches a sliver of traffic before the gate stops it.

Rollback, reversibility, and safe schema change

Five recovery moves exist — but only two are true rollbacks. Know which lever you are actually pulling.

⏮️

Deploy prior artifact

A true rollback. Seconds. The move you want.

🔀

Switch traffic

Blue/green or canary. Also a true rollback.

🔌

Flip a kill switch

Not a rollback, but a kill switch takes effect in ~200ms.

⏭️

Revert + redeploy

Roll-forward, not rollback. Slower — a full pipeline run.

🩹

Compensating action

A new forward action (refund, follow-up email) when nothing can be undone.

None of these undo migrations that already ran, rows the bad code wrote, or emails sent — those need a compensating action.

🚪
Reversibility sorts your gates

Two-way doors (reversible) — automate the check; the success condition is the approval. One-way doors (irreversible) — keep a human and pre-design the do-over: soft-delete with a restore window instead of hard delete, a first-class refund flow instead of raw charges. Best of all, decouple deploy from release with a feature flag: ship the code OFF, flip it ON when ready; if it breaks, flip OFF in ~200ms with no redeploy.

Safe schema change: expand → migrate → contract

Two services on their own deploy schedules still share one schema — a shared database still couples them. Never drop or rename a column in the same deploy that removes the code using it.

1

Expand
Add the new column or table. Nothing reads it yet.

2

Migrate
Write both old and new; backfill; cut reads over once both sides confirm.

3

Contract
Remove the old structure — a separate deploy, only after both sides are confirmed.

This is why a schema migration is a one-way door: the migration has already run, so you sequence it in stages instead of relying on rollback.

Check your instincts

Four situations. Pick what a senior engineer would actually do.

Scenario 1

You want to add a strict new lint rule, but the codebase already has 3,300 existing violations. What ships fastest without blocking everyone?

Scenario 2

A UI test fails about 1 time in 20 and passes on retry. What should team policy be?

Scenario 3

Your deploy pipeline curls /health, gets 200 OK, and reports success — but users cannot check out. What is missing?

Scenario 4

You need to rename a database column that both an old monolith and a new service still read. What is the safe sequence?

06

Guardrails & Safe Refactoring

Build like it will go rogue — limit what the agent can do, then shrink what it must reason about.

Part A — Guardrails

When is an agent dangerous?

Even with nobody attacking it, an agent that can act — deploy, delete, change data — can do real damage by an honest mistake. It gets far worse when three things line up.

🗄️

Private data it can read

Your code, database, customer records — anything sensitive in the agent's reach.

📥

Untrusted input in the mix

A web page, a ticket, an email, another tool's output — content the agent didn't write.

📤

A way to act or send out

Run a command, call an API, post data somewhere the outside world can see.

🗄️+ 📥+ 📤= the lethal trifecta

With all three, a booby-trapped page can quietly turn the agent against you — because the model can't reliably tell instructions from content. This is prompt injection, and it echoes the OWASP Top 10 for Agentic Applications (2026). Mindset: build like it will go rogue — not because it usually does, but because the cost when it does is too high to leave to luck.

Watch the guardrail fire

A support ticket carries hidden instructions. The agent can query the database and run commands — the trifecta is live. Play it through and watch the approval hook stop a disaster.

The agent believed the ticket. The hook didn't — it stopped an action, not an intention. That is a guardrail. A politely-worded "please don't" would not have saved you.

Two ways to set a limit — plus a layer underneath

Onboarding metaphor: would you give a new hire production database access on day one? You wouldn't — not from distrust, but because one honest mistake costs too much. You scope access to the task. An agent is that new hire.

📏

Standing limit

A rule obeyed on every action — an allow list or deny list. Best for clear-cut rules: never read .env, never rm -rf. If a command is on both lists, block wins.

Approval hook

A checkpoint that stops and asks you before one risky step: installs, deploys, big edits. It can ask, block, or allow.

📦

Sandbox

The layer underneath: it blocks the actual OS call regardless of wording. Narrows the blast radius of writes and exfiltration — but it doesn't stop reads, so it complements your lists, not replaces them.

Why the sandbox matters: lists and hooks depend on recognizing the action — a rephrased command can slip past either. The sandbox recognizes nothing; it just blocks the syscall. Here is what a standing limit looks like in a settings file:

CODE
{ "permissions": {
    "allow": ["Bash(npm run *)", "Bash(git diff *)"],
    "deny":  ["Bash(rm *)", "Bash(git push *)",
             "Bash(curl *)", "Read(.env*)"]
} }
Plain English

Open the rulebook for what the agent may do.

Allow: pre-approve the safe, routine commands so you're not clicking "yes" all day.

Deny: hard-block the destructive ones — no delete, no push.

Note we also block curl and hide .env: blocking read of a secret is useless if a script can read it and upload it anyway.

Close the rulebook.

Least privilege is the whole game

🔒
A polite instruction is not a guardrail

"Please don't delete the database" won't stop it — the permissions will. Guardrails are about what the agent can do, not what you asked it to do.

🎯
Scope to the task, repo, and environment

One agent should not hold every permission. Least privilege is the default posture.

🔧
Prefer a narrow tool over a broad one

A close_ticket tool beats a run-any-SQL tool — a capability you never granted can't be misused.

👤
Never more access than the operator has

The common trap: a service account or API token scoped far broader than the person running it.

🏛️
The org ceiling sits above per-repo config

Managed rules individuals can't loosen: which models are allowed, which MCP servers are approved, spend caps, audit logging. Personal config is the floor; the org layer is the ceiling.

What goes wrong → the guardrail that catches it

What goes wrong The guardrail that catches it
Runaway loop / surprise overnight bill Token + time budgets; a "keep going?" approval past a threshold
Confidently wrong work slips through A reviewer agent before it lands, plus a human on anything critical
A poisoned tool's text redirects the agent Locked permissions; read MCP tool descriptions before installing (that text becomes instructions)
One agent with every permission ("it's only internal") Least privilege; scope per task + environment
A rephrased command slips past a rule or a tired reviewer A sandbox — it blocks the OS-level call regardless of wording
Part B — Safe Refactoring

Why big files hurt agents specifically

A god file is a wall you can't tell is load-bearing. Knock it down blindly and the ceiling comes with it.

🧑
Humans

Above size and complexity thresholds, defect probability rises and the code gets harder to test and understand.

🤖
Agents

Usable context is often ~64k–200k tokens in practice, not the headline million. A giant file crowds out reasoning — too much context degrades output.

🏗️

Decomposition is infrastructure for safe AI edits. Smaller files help agents, not only humans. Set a soft cap (~300 lines) and enforce it in CI — "warnings nobody reads are not a policy." But understand why a big file exists before you carve it.

Find the seams, lock behavior, then move

A seam is a place you can alter behavior without editing at that exact spot. Each seam has an adaptor: constructor injection, a factory, a function parameter, a link-time stub. Classic case: code calls Clock; a seam lets you slot in a FakeClock for tests without touching the call site — that's dependency inversion.

🧪
Tests before moves

Write characterization tests that pin the current behavior — happy path, null/empty, boundaries, errors — before any move. Don't assume correctness; lock actual outputs and mark suspect cases // TODO: verify. Skipping this is the #1 driver of bad AI refactors.

Keep the model on judgment, not bulk edits, with a read-only research pass — the same Research → Plan discipline from Module 2, aimed at a scary file:

Prompt
Read this file. List every top-level function or
class and assign each a single responsibility:
{data access, validation, business rule,
formatting, I/O, framework glue, dead code}.
Propose a target module tree, one responsibility
per module. Flag any symbol that belongs in two
places — those are your decomposition targets.
Do not move anything yet.
Plain English

"Study the file — don't change it."

"Tell me what each piece is actually for, sorted into a fixed set of jobs."

"Draw me the shape it should be — one job per file."

"Point to anything doing two jobs at once — that's where the seams are."

"And keep your hands off the code until we agree on the plan."

Ship it in small, reversible slices

Pick the strategy by risk. Every slice is a small PR — ≤ ~300 lines, revert-safe (no mid-sequence data migrations), all tests green (including characterization) before merge.

🔩

Helper extraction

Large but cohesive, single owner. Mechanical, low risk — just lift the piece out.

🌱

Strangler split

Old and new run in parallel behind a façade. Contested ownership; cutover stays reversible.

🧨

Module-boundary rewrite

Tangled, no tests. Highest risk — characterization tests first, always.

Six pitfalls to watch

circular depsAdd a shared-abstraction module; verify with a dependency-graph tool.
broken importsFaçade re-export plus a @deprecated marker on the old path.
merge conflictsShort-lived branches; a single owner during the migration.
nano-modulesEach new file earns ≥2 exports used by ≥2 callers — or a justified single responsibility.
skipped testsA branch-coverage gate must pass before a refactor lands.
re-accumulationCI hard-fails on file size — not a yellow warning everyone ignores.
📌

The split isn't done until it's canonical. In the same PR, update the agent docs to name the new module, its owner, and what may not cross the seam — so the next agent respects the boundary you just drew.

Check your judgment

Four scenarios. Pick the move a careful engineer would make.

Scenario 1

Your agent reads customer support tickets, can query the database, and can call external APIs. A ticket contains hidden instructions. What's the risk, and the cleanest structural fix?

Scenario 2

You're about to let an agent refactor a 2,000-line file with no tests. What's your first move?

Scenario 3

A deny rule blocks Read(.env), but you didn't block curl. Is the secret safe?

Scenario 4

Which is safer to expose to the agent, and why: a narrow close_ticket tool, or a broad run-any-SQL tool?

07

Extending the Harness

Teach the harness your team's know-how with custom skills, then shape your data so the agent reads it like a pro.

Part A · Custom Skills

Tool vs. skill

You keep re-pasting the same five-step procedure into the agent. It works, but it lives in your chat history where a teammate can never find it, and stuffing it into an always-on rules file makes every session pay for it.

A tool is a raw ingredient in the pantry. A skill is the written recipe that turns ingredients into a dish. Fifty recipes in a drawer cost nothing until you open one.

TOOL — the ingredient

What it is: a capability — one action.

Example: read file, git diff, create issue.

Granularity: atomic — one call.

Where it's from: the harness or an MCP server.

How it runs: the model calls it mid-task.

SKILL — the recipe

What it is: know-how — a recipe.

Example: “summarize this diff and flag the risk.”

Granularity: a workflow — many calls plus judgment.

Where it's from: you — markdown plus YAML.

How it runs: loaded by its description when relevant.

A skill uses tools — it's the layer above them, not an alternative. Skills don't give the agent new powers; they give it expertise, loaded on demand so 50 skills cost nothing until one fires.

Anatomy of a skill

Here's a real, tiny SKILL.md. Every part earns its place — the top block is metadata, the bottom is the recipe itself.

CODE — SKILL.md
---
name: summarize-changes
description: Summarizes uncommitted changes and flags
  anything risky. Use when the user asks what
  changed, wants a commit message, or reviews a diff.
allowed-tools: Bash(git diff:*)
---
## Current changes
!`git diff HEAD`
## Instructions
Summarize the changes above in two or three
bullets, then list any risks. If the diff is
empty, say so — do not guess.
PLAIN ENGLISH

The frontmatter (between the --- lines) is metadata about the skill.

The name and description are the only signal the model sees when deciding whether to use this skill.

So the description says both what it does and when to use it — that's what makes it get picked.

allowed-tools pre-approves one specific command so you're not prompted mid-run. It pre-approves — it does not sandbox; your permission settings still govern the rest.

The !-prefixed line runs live at invocation, so the skill reads the current repo — not a stale pasted snapshot.

The body is the instructions the model follows — and it handles the edge case (empty diff) so the model doesn't invent recovery each run.

The description is the whole ballgame

Among 100+ installed skills, the description is the only thing the model reads to decide. Get it wrong and a perfectly good skill never fires. The formula: what it does + when to use it + key capabilities.

Too vague to fire

Helps with documents

Processes data

Implements the Project entity model

Specific, with triggers

Summarizes PR diffs and flags risky changes. Use when the user asks what changed, wants a commit message, or asks to review their diff before pushing.

Be pushy with triggers. Models under-trigger far more than they over-trigger, so an explicit “Use when…” beats a subtle implication every time.

Two authoring habits that pay off

🎯
Goal + guardrails beat a brittle recipe

“Always output exactly three bullets” breaks the moment the input isn't what you pictured. “Output 1–3 bullets; mention only files that changed” leaves room for judgment — and prefer the positive form, since negation is a weak spot.

Inline live data with the ! prefix

Let the skill pull the current state at run time instead of pasting a snapshot that's stale by the next invocation.

Keep it small, then author in a loop

Progressive disclosure: the SKILL.md stays tiny; detail lives in bundled files that load only when the task needs them.

skills/summarize-changes/
SKILL.md always loaded: metadata + short body (~tens of tokens)
references/
risk-checklist.md read only on a risky change
commit-style.md read only when writing a commit message
scripts/
changed-importers.sh run only when listing affected importers

Three levels: metadata (always in context) → body (loads when triggered) → bundled files (read or run only when pointed at). That's why a skill can be thorough without being heavy.

Author in a loop

A skill is never right on the first draft. Build it, run it on a real task, then read what the agent actually did — and go again until the runs stop surprising you.

🔨
Build
▶️
Run
🔍
Introspect
Click “Next Step” to walk the authoring loop
⚠️
The “punt” anti-pattern

An instruction that just says “summarize the changes above” with no edge handling forces the model to invent recovery when the input isn't the happy case — and inventions diverge run to run. At team scale, formalize the eyeball check into an eval harness: committed fixtures (a rename, an empty diff, a conflict marker) scored by a rubric on every change.

When not to build a skill

📜

Fixed steps? Write a script.

If the steps never change and need no judgment, a plain script is cheaper and deterministic — a release script, a pre-commit that runs lint + typecheck + test.

🧠

Needs judgment? Write a skill.

Reach for a skill when it must read context and adapt. They compose: put the deterministic part in scripts/ and let the instructions call it.

“A skill that is only fixed steps is a script wearing a costume.”

Part B · Agent-Readable Observability

A saved query beats a raw log dump

When something breaks, the agent dumps 200 lines of logs into its context and loses the thread. Your logs are a patient's chart: raw notes are a shoebox the agent reads cover-to-cover and still misattributes; a clean, indexed chart lets it find the cause in seconds.

CODE — before / after
# BEFORE — raw log dump (~200 lines, ~12,000 tokens)
2026-05-19T14:02:11Z svc=checkout ...
svc=payments ... ERROR svc=auth
trace=7f3a48 Read timed out ...
 
# AFTER — one saved query (~600 tokens, same answer)
SELECT service, COUNT(*) AS error_count
FROM logs WHERE status = 'error'
GROUP BY service ORDER BY error_count DESC LIMIT 10
 auth-service 184 | payments-api 42 | checkout-api 19
PLAIN ENGLISH

Handed raw logs, the agent fills its context window and loses track of what it was doing.

A saved query returns the same answer, pre-shaped — about 20× fewer tokens.

The answer stays sharp because the agent reads a short summary, not a haystack.

Same investigation, one line back: auth-service is the outlier, not payments-api.

💡
Pre-shape the data, don't dump it

“Agents would fill their context windows with log data and lose track of what they were doing.” — Datadog Engineering, 2026. A saved query is roughly 20× cheaper on tokens and more accurate, because you did the reasoning once and named it.

Four improvements that make data agent-readable

You don't need a new platform — four small changes turn raw output into evidence the agent can reason over.

🧭

Add a trace ID

One trace ID per request at the entry point, propagated through every hop and attached to every log line — so the agent can follow a request across services.

📑

Structured logs

Emit structured logs with the smallest field set you commit to everywhere: timestamp, level, service, trace_id, span_id, tenant_id, event, message.

🩺

Extend /health

Have your health endpoint return version, commit, dependency state, and last-deploy time — not just "OK" — so the agent can confirm a deploy and tell what's degraded.

🔍

Add a saved query

Wrap the questions you always ask into named views the agent calls by name — no token-burning raw dumps.

The keystone is structured logging — here's the whole idea in two lines:

CODE — before / after
// Before
console.log('user 123 paid');
 
// After
logger.info('payment_received', {
  tenant_id: '123', trace_id: traceId,
  amount: 4250 });
PLAIN ENGLISH

The “before” is a sentence — only a human can parse it, and only by reading it.

The “after” is a structured event the agent can filter (event = payment_received).

It can join across services on trace_id and scope to one customer with tenant_id.

Same information — now machine-readable.

Lock it down before you let it loose

Once an agent can read your systems, two things matter: what it can touch, and what it can see. Read-access lockdown has three tiers — and only one is a real boundary.

🛡️

Server-side tool disable

Strongest. The server simply doesn't expose write tools — the agent can't call what isn't there.

🔑

OAuth scope / IAM

Strong, but only as strong as the OAuth scope grammar your vendor exposes. IAM pins the same way.

⚠️

Client-side allowlist

Weakest — best-effort, not a security guarantee. Put a scrubber in front regardless.

Classify before the agent reads

Tier 0 · public Service names, status codes — fine for the agent to see.
Tier 1 · internal Internal IDs, hostnames, timings — low risk, keep inside.
Tier 2 · sensitive Customer PII, payment fields — redact before the agent, via a structured-logging interceptor, not after.
Tier 3 · secrets Tokens, keys — never logged. If it's in a log today, that's the bug.

One skill fires three ways

⌨️
You type it

A person pulls the trigger, in the loop for every run.

💬
A chat command invokes it headlessly

A team member fires it without opening the harness.

📡
An alert webhook fires it autonomously

Same skill underneath — climbing this list is a policy decision about who pulls the trigger, not new construction.

⚠️
Autonomous webhooks need hard caps

A real four-agent pipeline once ping-ponged internally for 11 days and ran up a ~$47,000 bill. Guard both failure modes — one run looping (cap max turns, max budget, allowed tools) and the same alert firing over and over (dedup on the alert fingerprint; concurrency limits) — with an org-level spend cap as the last-resort backstop. And an alert needs an owner and a one-page runbook before it needs an agent.

Check your instinct

Four situations. Pick what you'd actually do, then check.

Scenario

You keep re-pasting the same test-scaffolding procedure to the agent, and a teammate can't find it. Skill or script — and why?

Scenario

Two skills do similar things; the model keeps picking the wrong one — or neither. Where do you look first?

Scenario

The agent triages an incident but blames payments-api when the real cause is auth-service two hops away. Root cause and fix?

Scenario

You wire an alert webhook to fire an agent autonomously. What must you add first?

🎉

That's the whole loop

You built a mental model of what's under the hood, then an operating pattern — research, plan, implement. You learned to set up to win, to review and drive instead of rubber-stamp, to string a safety net under your work, to put up guardrails as you refactor, and finally to extend the harness itself with your own skills and readable data.

None of it was about one editor or one codebase. It's a way of working with a capable, tireless, occasionally overconfident collaborator — and it travels with you to whatever tool and whatever code you touch next. Go build something, and keep the loop tight.