· 25 min read · AI agents · Claude Code · Workflow · TypeScript
How I Ship Software With AI Agents: My AI Coding Workflow
My AI coding workflow with Claude Code: grill the idea, write a PRD, slice tracer-bullet tickets, loop agents in a sandbox, then review against tests.

Two hours before a staging demo for eCaller, a smart student dismissal system, I was holding a 3,000-line diff that would not compile.
I'd spent the day in one enormous Claude CLI session, refactoring the NestJS backend and the Socket.io layer that pushes dismissal notifications in real time. Somewhere past the first hour the context bloated. The agent invented a Redis caching implementation that didn't work, wired it in, and told me it was done. The build told me otherwise. The client call was already on the calendar.
That diff is the reason I have an AI coding workflow at all, instead of a chat window and hope. This post is that workflow: how I plan, delegate, review and ship with coding agents, mostly Claude Code, on projects like eCaller, Wedlii, PlanReel, Tammwe and We-Sal. Most of the ideas come from Matt Pocock's workshop Full Walkthrough: Workflow for AI Coding. The adaptation, the numbers and the mistakes are mine.
The thesis fits in one line: the fundamentals that make human teams work (alignment, small slices, fast feedback, deep modules) make agents work even better.
Steal this in 15 minutes
If you read nothing else, do these today. Everything below explains why.
- Install the skills. Pick one route, not both:
claude plugins install mattpocock-skills # managed plugin, updates itself
npx skills@latest add mattpocock/skills # or: editable copies in your repo Then once per repo, inside Claude Code: /setup-matt-pocock-skills.
- Turn on the status line so you can see context use and cost:
/statusline show model name and context percentage with a progress bar. You can't manage a context window you can't see. - Start your next feature with
/grill-me, not a prompt. Answer its questions until you're bored. Boredom means you're aligned. - Let
/to-specwrite the PRD, read only the Out of Scope section, then run/to-tickets. Send back any ticket called "build the service". - Put guardrails on every commit:
pnpm add -D husky lint-staged prettier oxlint vitest
pnpm exec husky initWho this is not for
- A weekend project with no tests and no CI. The guardrails are the workflow. Without them you get the chat-window experience with more steps.
- A codebase you can't run locally. Feedback loops that only exist in production aren't feedback loops.
- Anyone hoping the agent will also decide what to build. Alignment stays human. That's the part I refuse to hand over.
- Anyone who wants to read fewer diffs. You'll read more. Smaller, better ones, but more. Details further down.
How I used to work, and what it cost
One endless chat per feature, in Cursor. I'd paste code in, paste code out, and hope the agent still remembered the shape of a Turborepo and pnpm monorepo it had seen forty messages earlier. When it forgot, I'd paste more.
The bill came in three currencies. Sleep, first. Then API tokens burned on retries with a giant context attached to every one. Then the expensive kind: AWS ECS deployments that broke in subtle ways, because a diff that size can't be reviewed properly and I'd stopped pretending otherwise.
My own count from that period: two or three complex features a week, with roughly 60% of my time spent untangling the code the agent had generated the day before. Not building. Untangling.
Since switching to the workflow below: eight to ten features a week, review down to about fifteen minutes per pull request, and bugs rarely reach QA. Those are my numbers on my projects, not a benchmark. Your codebase will move the needle differently. But the shape of the change, from "most of my time is cleanup" to "most of my time is deciding what's next", is the whole point.
The night the talk clicked
I watched Matt's talk late, after a debugging session that had gone the way they all went back then. Two things landed immediately.
The first was clear, don't compact. I'd been fighting the model's memory. The talk's move is to stop fighting it: reset to the same clean start every time and make that start worth optimising.
The second was that my prompts were not the problem. My codebase architecture was the bottleneck. The agent wasn't bad at my monorepos because I was asking badly. It was bad because the code was made of small tangled files that a human also struggled to hold in their head.
What I kept from the talk: grilling, the PRD, tracer-bullet tickets, TDD, and review in a fresh context. What I adapted: my day-to-day loop is a four-stage Sandcastle loop, plan, implement, review and merge, with Claude Fable 5.1 planning, reviewing and merging and Claude Opus 5.5 implementing. It's in Stage 4. The overnight Tammwe run in the same stage was on a LangGraph setup of my own. What I added: a rule about naming files in Out of Scope, which I learned the expensive way. That story is in Stage 2.
The AI coding workflow at a glance


The whole loop. Five human steps, one unattended loop, one human check, and QA feeding new tickets back to the board.
Stage | What goes in | What comes out | Who drives |
|---|---|---|---|
Grill | A vague idea or brief | A shared understanding | Me, with the agent asking |
Research / prototype (optional) | Open questions | A | Me |
PRD | The grilling conversation | A destination document | Agent writes, I skim |
Kanban | The PRD | Tickets with blocking relationships | Agent proposes, I correct |
Implement | Unblocked tickets | Commits, one ticket at a time | Agents, AFK |
Review + QA | Commits | Fixes, or new tickets | Fresh agent, then me |
Everything above implementation is the day shift: human work that queues up a backlog. Implementation is the night shift, run AFK.
The setup: skills and plugins I use
Almost every stage is powered by a skill. A skill is a folder with a SKILL.md whose description tells the agent when to pull it in. Most of these start from Matt Pocock's public mattpocock/skills repo (MIT). The rest you write yourself.


Public skills in teal, external tools in amber, files you write in white. Guardrails run on every commit regardless of stage.
Skill / plugin | What it does | Stage | How to install | Required? |
|---|---|---|---|---|
| Interviews you, one question at a time, each with a recommended answer | Grill | Via the skills plugin below | Required |
| Turns the conversation into a PRD issue | PRD | Via the skills plugin | Required |
| Splits a PRD into tracer-bullet tickets with blockers | Kanban | Via the skills plugin | Required |
| Red-green-refactor, one test at a time | Implement | Via the skills plugin | Recommended |
| Reviews a diff against standards and spec | Review | Via the skills plugin | Recommended |
| Finds shallow modules worth deepening | Codebase | Via the skills plugin | Optional |
Runs agents in sandboxed worktrees, in parallel | Implement | | Optional (for AFK) | |
Runs Claude Code in an isolated microVM ( | Implement | | Optional (for AFK) | |
Claude Code status line | Shows context use and session cost | All | | Recommended |
Install in five minutes
# 1. The skills. Pick ONE route; installing both gives you every skill twice.
claude plugins install mattpocock-skills # managed plugin, updates itself
npx skills@latest add mattpocock/skills # or: editable copies in your repo
# then, once per repo, inside Claude Code:
# /setup-matt-pocock-skills
# 2. The AFK runner (only when you're ready to run unattended)
npm install --save-dev @ai-hero/sandcastle
npx @ai-hero/sandcastle init
# 3. Guardrail tooling
pnpm add -D husky lint-staged prettier oxlint vitest
pnpm exec husky initGuardrail tooling: Husky, lint-staged, Prettier, oxlint and Vitest. They matter more than any skill.
What goes in a skill
The skills worth writing yourself are short. A do-work skill can run the whole "explore, implement with TDD, run the checks, commit" loop. A verify skill can drive the running app in a browser and record what it changed. The one to copy first is the ticket splitter. Here's a trimmed, generic version:
---
name: to-issues
description: Break a PRD into independently grabbable issues, as tracer-bullet
vertical slices. Argument is the PRD issue number.
---
## Draft vertical slices
Each slice is a thin vertical cut through every layer (schema → API → UI → tests),
NOT a horizontal slice of one layer.
- Each slice delivers a narrow but COMPLETE path through every layer
- A completed slice is demoable or verifiable on its own
- Any prefactoring goes first, in its own slice
## Quiz the user
Present the breakdown as a numbered list: title, what it builds, depends on.
Ask: is the granularity right? Is the order right? Merge, split or drop any?The frontmatter description is doing real work: it's what the agent reads to decide whether to load the skill at all.
Two constraints every agent session has
Most of this process follows from two odd properties of LLMs. The eCaller diff was both of them at once.
The smart zone and the dumb zone
A bigger context window doesn't make the agent smarter. It gives it more room to get dumb. A model does its best work at the start of a session. As the context fills, quality drops, and it drops before the window is anywhere near full. Dex Horthy of HumanLayer calls this the dumb zone. The talk puts the line near 100k tokens, whatever the advertised window. A million-token window doesn't move the line. It just lets you keep working past it for longer, which is how you end up with a hallucinated Redis layer and a confident "done".


Tasks #1 and #2 finish while the model is sharp. #3 finishes in the dumb zone. #4 has to be split before anyone starts it.
So I keep a token counter in the status line and size tasks to finish well before the line.
If you can't see your token count, you can't tell when the agent got dumb.
From phased plans to a loop
The obvious fix for a big task is a multi-phase plan: one phase per fresh session, each pointed at the same PRD and plan file.


Four smart-sized sessions instead of one long one. You still type every "do phase N" yourself.
It works, but you are the loop. Any developer looking at "phase 1, phase 2, phase 3" sees a for loop waiting to happen.


Put the executor in a loop and you come back to a stack of commits.
Take the plan away entirely and you get Geoffrey Huntley's Ralph: give the agent only the destination and "make a small change", and loop until it's met.


Ralph works. This workflow puts some structure back, as a board of tickets rather than a numbered plan.
Clear, don't compact
I never compact. I clear. The model forgets everything between sessions, like the lead in Memento. Compacting fights that by squashing the old session into a summary, and a summary is a lossy copy of a session that was already in the dumb zone. Clearing accepts the forgetting and returns to the same starting state every time.


A cleared session starts identically every time. A compacted one carries sediment.
A predictable start is something I can optimise. Sediment isn't. That's also why the always-loaded CLAUDE.md stays small. It pushes a few rules and points to documents the agent can pull when it needs them:
<!-- CLAUDE.md, trimmed and generic -->
## Testing
Run only the test files your change touches while iterating.
CI runs the full suite on every PR. See docs/agents/testing.md.
## Coding standards
Read CODING_STANDARDS.md while writing code, not only while reviewing it.
## Backlog
Issues and PRDs are GitHub issues, managed with the gh CLI. See docs/agents/backlog.md.Keep the always-on context tiny. Everything else should be pulled when needed.
Stage 1: Grill before you plan
Every piece of work starts with /grill-me and the brief. The skill is a few lines long. It tells the agent to interview me relentlessly, walk the decision tree one branch at a time, and give its own recommended answer to each question.
This fixes the most common failure of all, which is misalignment. Plan mode wants to produce a plan as soon as it thinks it has enough. What I actually need first is what Fred Brooks calls the design concept: an idea that everyone involved shares. The recommended answers keep it fast, because most of the time I agree and move on. The talk describes grilling sessions running to dozens of questions. Mine feel long right up until I remember what a wrong assumption costs three tickets later.
This is the opposite of "specs to code", where you edit a document and never look at the output. Matt tried that and dropped it: you have to keep a handle on the code.
Grilling is human-in-the-loop by design. Don't try to automate alignment.
Stage 2: Write the destination, not the journey
Once we agree, /to-spec turns the conversation into a PRD, which here is a GitHub issue. Its shape is simple:
## Problem Statement — from the user's point of view
## Solution — from the user's point of view
## User Stories — a long, numbered list: "As a <actor>, I want…, so that…"
## Implementation Decisions — modules to build or change, and their interfaces
## Testing Decisions — what a good test is here, which modules get tests
## Out of Scope — "we are not building X", said plainlyTwo details matter. Implementation decisions name modules and interfaces but no file paths, because paths go stale. And "Out of Scope" is where decisions not to build something survive, so it doubles as the definition of done.
I don't read the PRD the agent writes. Except Out of Scope. Matt works the same way. After a good grilling session, reviewing the PRD only tests the model's ability to summarise, which is strong. Out of Scope is different: an exclusion you never agreed to is the hardest mistake to spot later, and the cheapest to catch now.
I learned how cheap the hard way. Recently, with the full workflow in place, an agent working in the Wedlii monorepo deleted a Payload CMS schema relation it had no business touching. The staging CMS was down for an hour. The PRD's Out of Scope listed features we weren't building. It said nothing about files the agent must not touch, so as far as the agent knew, that relation was fair game. Out of Scope should name the files that are off limits, not just the features. Sixty seconds of reading would have saved the hour.
Stage 3: Slice the work into tracer bullets
Now the destination needs a journey. I ask for a Kanban board: tickets with blocking relationships. A numbered plan can only be worked by one agent. A board with explicit blockers can be worked in parallel.
How the tickets are cut matters more than how many there are. Systems have layers:


Most features cross every layer. The question is the order you build them in.
Left alone, agents code horizontally, one whole layer per phase. Nothing runs end to end until the last phase lands:


Two phases of work with no way to check it.
The fix is an old one from The Pragmatic Programmer: tracer bullets. Each ticket is a thin vertical slice through every layer, so the first ticket already gives you something you can click.


Slice 1 is a schema change, a service and a minimal UI. Feedback arrives after the first ticket, not the last.
This is the one place I always review, because it's cheap and the mistakes are obvious. Even when told to use vertical slices, the agent will sometimes propose "build the service" as ticket one. That's a horizontal slice in disguise, and I send it back. Each ticket is also tagged AFK (an agent can do it) or human-in-the-loop.
Stage 4: Let the loop implement, AFK
This is where the human leaves. Start with the once-through version and watch it, because every prompt fix you need shows up here:
#!/usr/bin/env bash
# once.sh: illustrative, modelled on the script shown in the talk
set -e
issues=$(cat issues/*.md) # the whole backlog
commits=$(git log -n 5 --format='%h %s') # recent history, so it knows where it is
claude --permission-mode acceptEdits "$issues $commits $(cat prompt.md)"The prompt says: take AFK tickets only, pick the next by priority (bugs, infrastructure, tracer bullets, polish), do one, run the feedback loops, commit, and print a sentinel when nothing is left. Once that behaves, run it in a loop inside a sandbox, then in parallel. The parallel version has four stages, plan, implement, review and merge, and it is the loop I run every day. It's further down this section.
Each implementer gets its own git worktree in its own container, so parallel agents can't trample each other or your machine. Don't skip the stages. Move from the once-through script to a sandboxed loop only when watching it has become boring.
What the night shift looks like when it works: I left my LangGraph setup running overnight on the Tammwe matching engine and woke up to four tickets resolved and merged. About five hours, completely unattended. Four tickets is not a heroic number. It's four tickets I didn't do, on a night I slept.
Queue the work in the day shift so the night shift never has to ask a question.
The loop I actually run: plan, implement, review, merge
My day-to-day loop is Sandcastle's parallel planner, adapted. It runs in rounds, and every round has four stages:
- Plan, with Claude Fable 5.1. It reads the open issues labelled
Sandcastle, writes an implementation plan on any issue that has none, works out what blocks what, and returns the unblocked issues with a branch name each. The plan comes back as JSON and is validated against a Zod schema before anything runs. - Implement, with Claude Opus 5.5. One Docker sandbox per issue, all running at once. Each implementer works its issue on its own branch.
- Review, with Claude Fable 5.1. If the implementer committed, a reviewer runs in the same sandbox. It checks the branch against the issue, the plan and
CODING_STANDARDS.md, and fixes what it finds. - Merge, with Claude Fable 5.1. One agent merges every branch with commits into the checkout I started from, resolves conflicts and runs the repo's checks.
Then the next round starts. Issues the planner held back as blocked get picked up once their blockers have merged.


Three of the four stages belong to the stronger model. The one that doesn't is the one that runs in parallel.
The model split is the part I'd defend. Opus does the long, token-heavy typing, in parallel. Fable makes the calls. Deciding what can run, reading one diff with fresh eyes, reconciling branches: each is a judgement over a small context. It's Matt's cheaper-implementer, stronger-reviewer split moved up a tier, with planning and merging added. Fable 5.1 is Anthropic's most capable generally available model, so I spend it where the context is smallest and judgement counts most.
This is the whole .sandcastle/main.ts, adapted from Sandcastle's parallel-planner template:
// Plan → implement → review → merge, in rounds.
//
// Phase 1 (Plan): A Fable agent reads the open `Sandcastle`
// issues, leaves an implementation plan on each
// one that has none, works out what blocks what,
// and outputs a <plan> JSON of the unblocked
// issues with their branch names.
// Phase 2 (Execute + Review): One sandbox per issue, all running at once.
// An Opus agent implements the issue on its
// branch; if it commits, a Fable agent reviews
// the branch in the same sandbox against the
// issue, the plan and CODING_STANDARDS.md, and
// fixes what it finds.
// Phase 3 (Merge): A Fable agent merges every branch with commits
// into the branch checked out here, resolves
// conflicts and runs the repo's checks.
//
// The outer loop repeats so issues the planner held back as blocked are picked
// up once their blockers are merged.
//
// The merge lands in THIS checkout, so run it from a clean, up-to-date `main`.
// Nothing is pushed: `git push` afterwards deploys the round (Amplify builds
// main) and closes the issues through the commits' `Closes #n` lines.
//
// Usage (from the repo root):
// pnpm sandcastle
// Needs .sandcastle/.env (see .env.example) and the image built once with
// pnpm exec sandcastle docker build-image
import { execFileSync } from "node:child_process";
import * as sandcastle from "@ai-hero/sandcastle";
import { docker } from "@ai-hero/sandcastle/sandboxes/docker";
import { z } from "zod";
// The planner emits its plan as JSON inside <plan> tags; Output.object extracts
// and validates it against this schema. We use Zod here, but any Standard
// Schema validator works just as well — Valibot, ArkType, etc. See
// https://standardschema.dev.
const planSchema = z.object({
issues: z.array(
z.object({ id: z.string(), title: z.string(), branch: z.string() }),
),
});
// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------
// Maximum number of plan→execute→merge cycles before stopping.
// Raise this if your backlog is large; lower it for a quick smoke-test run.
const MAX_ITERATIONS = 10;
// The host's pnpm content-addressable store, mounted into every sandbox so
// `pnpm install` links packages instead of downloading 1.5 GB per sandbox.
// Copying node_modules would not work: pnpm's layout is symlinks into
// node_modules/.pnpm and does not survive a copy.
// Machine-specific (this is the default for the maintainer's snap install);
// override by exporting PNPM_STORE_HOST (find yours with `pnpm store path`).
const PNPM_STORE_HOST =
process.env.PNPM_STORE_HOST ?? "~/snap/code/259/.local/share/pnpm/store/v10";
const PNPM_STORE_SANDBOX = "/home/agent/.pnpm-store";
// Runs inside the sandbox before the agent starts. A frozen install keeps the
// sandbox on the committed lockfile, exactly as Amplify builds it, and
// `prepare` installs the husky hooks so the agents' commits go through the
// same lint-staged / commit-msg gates as ours. Several sandboxes install at
// once and all link from the same store; pnpm locks it, so that is safe.
const hooks = {
sandbox: {
onSandboxReady: [
{
command: `pnpm install --frozen-lockfile --store-dir ${PNPM_STORE_SANDBOX}`,
timeoutMs: 10 * 60_000,
},
],
},
};
// Nothing to copy: node_modules comes from the hook above.
const copyToWorktree: string[] = [];
const pnpmSandbox = () =>
docker({
mounts: [{ hostPath: PNPM_STORE_HOST, sandboxPath: PNPM_STORE_SANDBOX }],
});
// Every branch forks from this checkout and the merge lands back in it, so it
// must be clean and must not be behind GitHub: a round built on a stale main
// would merge in code that conflicts with what is already deployed.
const git = (...args: string[]) =>
execFileSync("git", args, { encoding: "utf8" }).trim();
const dirty = git("status", "--porcelain");
if (dirty) {
console.error(
`Working tree is not clean; commit or stash before running Sandcastle:\n${dirty}`,
);
process.exit(1);
}
git("fetch", "--quiet", "origin");
const upstream = git("rev-parse", "--abbrev-ref", "@{upstream}");
const behind = Number(git("rev-list", "--count", `HEAD..${upstream}`));
if (behind > 0) {
console.error(
`This branch is ${behind} commit(s) behind ${upstream}; pull first so the round builds on what is deployed.`,
);
process.exit(1);
}
// ---------------------------------------------------------------------------
// Main loop
// ---------------------------------------------------------------------------
for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
console.log(`\n=== Iteration ${iteration}/${MAX_ITERATIONS} ===\n`);
// -------------------------------------------------------------------------
// Phase 1: Plan
//
// The planning agent (Fable, for deeper reasoning) reads the open issue list,
// builds a dependency graph, and selects the issues that can be worked in
// parallel right now (i.e., no blocking dependencies on other open issues).
//
// It outputs a <plan> JSON block — Output.object parses and validates it.
// -------------------------------------------------------------------------
const plan = await sandcastle.run({
// No install hook: the planner reads issues and code; it never builds.
sandbox: pnpmSandbox(),
name: "planner",
// One iteration is enough: the planner just needs to read and reason,
// not write code. (Structured output requires maxIterations: 1.)
maxIterations: 1,
agent: sandcastle.claudeCode("claude-fable-5-1"),
promptFile: "./.sandcastle/plan-prompt.md",
// Extract and validate the <plan> JSON into a typed object. Throws
// StructuredOutputError if the tag is missing, the JSON is malformed, or
// validation fails — which aborts the loop.
output: sandcastle.Output.object({ tag: "plan", schema: planSchema }),
});
const issues = plan.output.issues;
if (issues.length === 0) {
// No unblocked work — either everything is done or everything is blocked.
console.log("No unblocked issues to work on. Exiting.");
break;
}
console.log(
`Planning complete. ${issues.length} issue(s) to work in parallel:`,
);
for (const issue of issues) {
console.log(` ${issue.id}: ${issue.title} → ${issue.branch}`);
}
// -------------------------------------------------------------------------
// Phase 2: Execute + Review
//
// For each issue, create a sandbox via createSandbox() so the implementer
// and reviewer share the same sandbox instance per branch. The implementer
// runs first; if it produces commits, the reviewer runs in the same sandbox.
//
// Promise.allSettled means one failing pipeline doesn't cancel the others.
// -------------------------------------------------------------------------
const settled = await Promise.allSettled(
issues.map(async (issue) => {
const sandbox = await sandcastle.createSandbox({
branch: issue.branch,
sandbox: pnpmSandbox(),
hooks,
copyToWorktree,
});
try {
// Run the implementer
const implement = await sandbox.run({
name: "implementer",
maxIterations: 100,
agent: sandcastle.claudeCode("claude-opus-5-5"),
promptFile: "./.sandcastle/implement-prompt.md",
promptArgs: {
TASK_ID: issue.id,
ISSUE_TITLE: issue.title,
BRANCH: issue.branch,
},
});
// Only review if the implementer produced commits
if (implement.commits.length > 0) {
const review = await sandbox.run({
name: "reviewer",
maxIterations: 1,
agent: sandcastle.claudeCode("claude-fable-5-1"),
promptFile: "./.sandcastle/review-prompt.md",
promptArgs: {
TASK_ID: issue.id,
BRANCH: issue.branch,
},
});
// Merge commits from both runs so the merge phase sees all of them.
// Each sandbox.run() only returns commits from its own run.
return {
...review,
commits: [...implement.commits, ...review.commits],
};
}
return implement;
} finally {
await sandbox.close();
}
}),
);
// Log any agents that threw (network error, sandbox crash, etc.).
for (const [i, outcome] of settled.entries()) {
if (outcome.status === "rejected") {
console.error(
` ✗ ${issues[i]!.id} (${issues[i]!.branch}) failed: ${outcome.reason}`,
);
}
}
// Only pass branches that actually produced commits to the merge phase.
// An agent that ran successfully but made no commits has nothing to merge.
const completedIssues = settled
.map((outcome, i) => ({ outcome, issue: issues[i]! }))
.filter(
(entry) =>
entry.outcome.status === "fulfilled" &&
entry.outcome.value.commits.length > 0,
)
.map((entry) => entry.issue);
const completedBranches = completedIssues.map((i) => i.branch);
console.log(
`\nExecution complete. ${completedBranches.length} branch(es) with commits:`,
);
for (const branch of completedBranches) {
console.log(` ${branch}`);
}
if (completedBranches.length === 0) {
// All agents ran but none made commits — nothing to merge this cycle.
console.log("No commits produced. Nothing to merge.");
continue;
}
// -------------------------------------------------------------------------
// Phase 3: Merge
//
// One agent merges all completed branches into the current branch,
// resolving any conflicts and running tests to confirm everything works.
//
// The {{BRANCHES}} and {{ISSUES}} prompt arguments are lists that the agent
// uses to know which branches to merge and which issues to comment on.
// -------------------------------------------------------------------------
await sandcastle.run({
hooks,
sandbox: pnpmSandbox(),
// A worktree on a temporary branch, merged back into this checkout when
// the agent finishes. The default (`head`) would have the agent, and the
// install hook, work directly in this directory's node_modules.
branchStrategy: { type: "merge-to-head" },
name: "merger",
maxIterations: 1,
agent: sandcastle.claudeCode("claude-fable-5-1"),
promptFile: "./.sandcastle/merge-prompt.md",
promptArgs: {
// A markdown list of branch names, one per line.
BRANCHES: completedBranches.map((b) => `- ${b}`).join("\n"),
// A markdown list of issue IDs and titles, one per line.
ISSUES: completedIssues.map((i) => `- ${i.id}: ${i.title}`).join("\n"),
},
});
console.log("\nBranches merged.");
}
console.log("\nAll done.");Four details make it safe to leave running:
- It refuses to start on a dirty or stale checkout. Every branch forks from the checkout and the merge lands back in it. A round built on an out-of-date
mainwould merge code that conflicts with what is already deployed. - Nothing is pushed. A round ends with merged commits in my checkout.
git pushis mine to run: it deploys the round and closes the issues through each commit'sCloses #nline. - One failure doesn't sink the round.
Promise.allSettledkeeps the other sandboxes going, and only branches that actually committed reach the merge. - Agents pass the same gates I do. Each sandbox installs from the frozen lockfile and sets up the husky hooks, so agent commits go through the same lint-staged and commit-message checks as mine. The host's pnpm store is mounted in, so parallel installs link packages instead of downloading them.
The script is only the control flow. What each agent does lives in its prompt file: the planner's, the implementer's, the reviewer's and the merger's.
Fable decides and checks. Opus types in between.
Why feedback loops matter more with agents
Without feedback loops an agent codes blind. The quality of your types, tests and lint sets a ceiling on how good the agent's output can be. If the output is bad, improve the loops before you improve the prompt.
TDD with AI is the strongest loop I know. Agents write bad tests when they write them after the code. They build the implementation, then write tests shaped to pass it. Red-green-refactor, one test at a time, makes that much harder:
<!-- TDD instructions, generic -->
1. Write a SINGLE failing test. Validate through the public interface.
2. Make it pass with the simplest implementation.
3. Repeat. Each test should teach you something new.
4. Refactor under green tests.
Don't test what the type system already proves.Then the mechanical guardrails, which don't care how confident the agent sounds:
# .husky/pre-commit
pnpm exec lint-staged # Prettier on staged files (see .lintstagedrc)
pnpm run typecheck
pnpm run lint:boundaries # dependency-cruiser: no imports across package walls// .claude/settings.json: a PreToolUse hook that blocks a command before it runs
{ "hooks": { "PreToolUse": [{ "matcher": "Bash",
"hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-npx-tsc.sh" }] }] } }The script reads the command from stdin. If it's npx tsc, it exits with code 2 and prints "use pnpm run typecheck instead", and the agent corrects itself (hooks docs). Locally, agents run only the tests their change touches. CI runs everything on every pull request.
The agent's ceiling is your feedback loop's ceiling.
Stage 5: Review in a fresh context, then QA by hand
Every implementation gets an automated review, and never in the same session. An implementer that has used most of its context would be reviewing from the dumb zone. A fresh reviewer starts in the smart zone. Matt uses a cheaper model to implement and a stronger one to review; the Sandcastle snippet above shows that split. My loop moves it up a tier: Opus 5.5 implements, and Fable 5.1 plans, reviews and merges.
The reviewer gets the coding standards pushed into its prompt, while the implementer only pulls them when it has a question:
<!-- review prompt, trimmed -->
Read the diff. For anything that looks fragile, write a test that tries to break it.
If you can break it, fix it. Stress-test edge cases: empty inputs, nulls, off-by-one.
Follow the standards in @CODING_STANDARDS.md. Never change what the code does.
If the code is already clean and well tested, do nothing.More AI means more code review for me, not less. A loop that finishes four tickets overnight produces four diffs by morning. They're small, they arrive with tests, and each takes me about fifteen minutes. But there are more of them than I'd ever have written by hand, and every one still gets read. Anyone selling you "AI so you can stop reviewing" is selling you the eCaller diff.
Automating QA produces slop. I refuse to. After the review, I QA it myself, clicking through the thing like a user would. This is where taste gets into the product. Matt's point, and my experience, is that apps built with every step automated come out generic in a way that's hard to name and easy to feel. Anything I find goes onto the board as a new ticket, so QA and implementation can overlap.
Shape the codebase so agents can work in it
Your codebase, not your prompt, is the main thing limiting your agent. This was the second thing that clicked the night I watched the talk, and it's the one I'd argue hardest for. The talk has one blunt line: "bad code bases make bad agents." My agents weren't struggling with my monorepos because of how I asked. They were struggling because the code was many shallow modules: lots of small files that each expose a little and depend on each other a lot.


To change one box, the agent has to trace every edge.
They're also hard to test, because there's no good place to draw the boundary:


Test each one alone and nothing checks the calls between them. Mock the neighbours and you're testing the mock.
John Ousterhout's answer in A Philosophy of Software Design is deep modules: a small interface in front of a lot of behaviour.


Callers see only the interface. The inside is a gray box.


One boundary, tested through the interface, covers most of what matters.
The trick that lets me keep a sense of the codebase while delegating: I design the interfaces and delegate the implementations. A standards file should say it plainly: prefer deep modules, accept dependencies instead of creating them, and mock only at system boundaries. The improve-codebase-architecture skill looks for places to deepen. It can also run on a schedule in CI and file one proposal at a time as a PRD. If you take one thing from the talk, it's this: run that skill on your own repo and read what it finds.
Running agents unattended, safely
AFK only works if a mistake costs little. The Wedlii incident cost an hour of staging downtime, which is the right order of magnitude: annoying, recoverable, nobody's production data. My rules:
- Sandbox everything unattended. A container or microVM per agent, with a git worktree per branch.
- Scope the credentials. The sandbox's
.envholds an API key and a GitHub token, nothing else. Make the token fine-grained and limited to one repo, with read and write on Contents, Issues and Pull requests. Metadata is read-only and always included. - Use
acceptEditslocally, full autonomy only in a sandbox. On your own machine, let the agent edit files but make it ask before anything riskier. Sandcastle skips permission prompts inside its containers by default, which is exactly why the container matters. - Protect production. A verify skill that touches real data should be read-only by default and log every database write it causes. An empty log is the proof.
- Merge through CI. Agents open pull requests and the full check suite has the final word.
Where this breaks, and what it costs
- You'll do more code review, not less. Loops that finish several tickets per run produce more code than small-PR discipline would like. Matt admits to having no clean answer here, and I don't have one either. Fifteen minutes a PR is the best I've managed.
- Tokens cost money, though less than you'd guess. On my projects a feature runs roughly $0.50 to $1.20 in API costs. Subagents, reviewers and parallel runs all bill, and the status line can show session cost next to context use. Measure it before you scale out. What used to cost me was retrying with a bloated context, not the work itself.
- It still breaks. The Wedlii schema deletion happened with grilling, a PRD, tickets and review all in place. A process reduces the blast radius. It doesn't remove the need to read Out of Scope.
- Docs rot. An old PRD in the repo will mislead the next agent once the code has moved on. The talk's fix is to close PRD issues when they ship rather than keep them as markdown in the tree.
- Frontend is still hard to check. Agents can't see the page well. For UI, the talk's approach is to ask for three throwaway prototypes on a scratch route and pick one yourself.
- Grilling is tiring. A long session is an hour of answering questions. If the agent over-asks, edit the skill. It's your file.
- Frameworks age fast. Own your planning stack. Small skills you can read and edit are easier to fix than a framework you can't see inside.
Lessons worth keeping
- Alignment is the job. Grill until you share the design concept, then let the model summarise it.
- Size tasks to the smart zone, and clear context instead of compacting it.
- Plan as a board of vertical slices, not a numbered list of layers.
- Your feedback loops set the ceiling on agent quality. Invest there first.
- Review in a fresh context, with the standards pushed in, then QA with your own eyes.
- Design the interfaces and delegate the insides. Deep modules keep you in charge of the shape.
None of this is new. It's what good teams already do, applied to a teammate that forgets everything between sessions. An agent can own the loop. Knowing where the work is headed is still your job.
FAQ
What is an AI coding workflow?
A repeatable process for building software with AI coding agents, rather than a single prompt. This one has six stages: grill the idea, write a PRD, split it into tickets, implement in an agent loop, review in a fresh context, and QA by hand. The human owns alignment and taste; the agent owns the loop.
How do you stop AI coding agents from writing bad code?
Give them feedback they can't argue with: type checks, test-first tests, lint, pre-commit hooks, and a separate reviewer with your coding standards in its prompt. Keep each task small enough to finish in the smart zone, and keep the codebase made of deep modules.
How big should a task be for an AI coding agent?
Small enough to finish well before the context window gets crowded. The talk this workflow comes from suggests about 100k tokens, regardless of window size. In practice that means one vertical slice per session: a schema change, a service and a minimal UI, with tests.
Does TDD work with AI coding agents?
Yes, and it may matter more with agents than with people. Tests written after the code tend to confirm whatever was built. Red-green-refactor, one failing test at a time, states the behaviour first and makes cheating much harder.
Is it safe to run AI coding agents unattended?
Only inside a sandbox. Run each agent in a container or microVM with its own git worktree, give it credentials scoped to one repo, and merge through CI. Keep human-in-the-loop work such as planning and QA out of the unattended loop.
What does an AI coding workflow like this cost?
On my projects, roughly $0.50 to $1.20 per feature in API costs, with the implementer, a fresh-context reviewer and the occasional parallel run included. The expensive failure mode is the old one: retrying a giant context over and over. Turn on the Claude Code status line to see cost per session, and measure before you run agents in parallel.
Which Claude models do you use in the loop?
Claude Fable 5.1 plans, reviews and merges, and Claude Opus 5.5 implements, one sandbox per issue, all at once. Implementing is the long, token-heavy half. Planning, review and merging are judgement calls over a small context, so the most capable model takes those.
Do I need Claude Code for this workflow?
No. The skills are plain markdown and install into other agents through npx skills@latest add mattpocock/skills. The workflow is about stages and guardrails, not a particular tool.
Credit and further reading
- Matt Pocock, Full Walkthrough: Workflow for AI Coding (AI Engineer talk page). The source of the workflow, the grilling idea and most of the skills.
- mattpocock/skills and Sandcastle.
- Geoffrey Huntley, Ralph Wiggum as a "software engineer".
- Dex Horthy on context engineering and the dumb zone.
- Claude Code docs: skills, plugins, hooks.
- The books behind it: The Pragmatic Programmer, A Philosophy of Software Design, The Design of Design.
On this site: how I built an AI pipeline with a human approving every step in engineering PlanReel, and the basics of how LLMs are trained and fine-tuned.
Try it on one ticket
Don't adopt all of this on Monday. Pick one ticket this week, the kind you'd normally hand to an agent in a long chat. Grill it first. Let the agent write the PRD and read only Out of Scope. Slice it, run it, review it in a fresh session, then click through the result yourself. Compare that diff to the last one you got the old way.
The agents were never the bottleneck. The process around them was, and the process is yours to fix.
If you'd rather have this set up for your team than build it from a blog post, get in touch. The projects it runs on are on my projects page. I've done it on my own projects, and I'd rather you skip the 3,000-line diff.