Subagents and Builder/Reviewer Patterns
Build a maker-checker pipeline where separate subagents write and review code.
Introduction
30 Second Summary
Every student knows the trick: you proofread your own essay, miss the same typo three times, then a friend spots it in seconds. The same blindness hits AI agents that check their own work.
In this project, you will create two scoped Claude Code subagents: a builder that writes code with tunnel vision, and a reviewer that scans the full codebase with fresh eyes. Together, they form a maker-checker pipeline that catches what no single agent could.
What You'll Build
You will watch the reviewer flag a cross-file inconsistency that the builder missed entirely, then close the loop by feeding the findings back and re-verifying until every file is consistent.
By the end of this project, you'll have:
- A builder subagent you can delegate any feature to, and a reviewer subagent that scans every file in the project with fresh context to catch what the builder missed.
- A demonstrated cross-file gap where the builder reports success but the reviewer catches a file the builder never opened.
- A complete maker-checker cycle you can run end to end: build, review, fix, and re-verify until the reviewer reports PASS.
- Secret Mission: Package the full cycle into a reusable /build-review skill that chains builder, reviewer, fix, and re-verify automatically.
Are there any prerequisites?
This is Project 4 of the Loop Engineering with Claude Code series. Complete Projects 1-3 (covering /goal, evaluator/Stop hooks, and /loop workflows) before starting.
Before We Start
Before you dive in, take a moment to lock in what you are building and why it matters.
You are about to create a multi-agent system where separate subagents build and review code independently. Understanding why that separation exists is the foundation for everything ahead.
Scaffold the Task Tracker
Subagents need a codebase where a change in one file ripples into others. A single-file app would never reveal the cross-file blindness you are about to engineer.
In this step, you will build a three-file Node.js task tracker with shared data structures across files. These cross-file dependencies create the conditions where a narrowly focused builder can miss something.
In this step, get ready to:
- Create a Git-initialized project directory.
- Build three source files with deliberate cross-file data dependencies.
- Commit a clean baseline the reviewer can diff against later.
Create the project directory
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Terminal and press Enter to open it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the project folder, move into it, and initialize a Git repository by running these commands:
mkdir task-tracker
cd task-tracker
git init
You should see a message starting with Initialized empty Git repository confirming the repository is ready.
Command not found?
If git init returns an error, Git may not be installed. Install it from git-scm.com/downloads and try again.
Help me install Git on my machine.
Build the three-file codebase
The task tracker splits into three files. tasks.js handles data storage. format.js handles display formatting. cli.js is the entry point that ties them together.
- Create tasks.js and paste the first section of the data layer by running:
// Import file system module for reading/writing tasks
const fs = require('fs');
// Path to the JSON file that stores all tasks
const DATA_FILE = './tasks.json';
// Load existing tasks from disk, or return empty array if no file exists
function loadTasks() {
if (!fs.existsSync(DATA_FILE)) return [];
return JSON.parse(fs.readFileSync(DATA_FILE, 'utf-8'));
}
// Write the full tasks array back to disk
function saveTasks(tasks) {
fs.writeFileSync(DATA_FILE, JSON.stringify(tasks, null, 2));
}
// Create a new task with an auto-incremented ID
function addTask(title) {
const tasks = loadTasks();
const task = {
id: tasks.length + 1,
title,
status: 'pending',
createdAt: new Date().toISOString()
};
tasks.push(task);
saveTasks(tasks);
return task;
}
What does this code do?
This sets up a simple file-based data store. loadTasks() reads from a JSON file on disk (or returns an empty array if no file exists yet). addTask() creates a task object with an ID, title, status, and timestamp, then saves it.
- Add the remaining functions below the addTask function in tasks.js:
// Mark a task as done by its ID
function completeTask(id) {
const tasks = loadTasks();
const task = tasks.find(t => t.id === id);
if (!task) throw new Error(`Task ${id} not found`);
task.status = 'done';
task.completedAt = new Date().toISOString();
saveTasks(tasks);
return task;
}
// Return tasks, optionally filtered by status
function listTasks(filter) {
const tasks = loadTasks();
if (filter === 'pending') return tasks.filter(t => t.status === 'pending');
if (filter === 'done') return tasks.filter(t => t.status === 'done');
return tasks;
}
module.exports = { addTask, completeTask, listTasks };
What does this code do?
- completeTask() finds a task by ID and flips its status to done with a completion timestamp.
- listTasks() returns all tasks, or only pending/done ones if a filter is provided.
- module.exports exposes all three functions so other files can import them.
- Save tasks.js.
- Verify the data layer works by running this quick test:
node -e "const t = require('./tasks'); console.log(t.addTask('hello'))"
You should see a task object printed with an id of 1, a title of hello, and a status of pending.
- Clean up the test data by running:
rm tasks.json
Seeing a SyntaxError or MODULE_NOT_FOUND?
Double-check that you saved tasks.js and that it is in the task-tracker folder. Make sure both code chunks are present in the file and that the second chunk starts after the closing brace of addTask.
Help me debug my tasks.js file.
✔️ Awesome, I've got everything!
Great. Make sure you have saved tasks.js before moving on.
ⓧ I'd like to double check the full code
Your tasks.js should contain both chunks you pasted above, in order. Scroll up and confirm you have:
- The imports (fs, DATA_FILE), loadTasks, saveTasks, and addTask from chunk 1.
- completeTask, listTasks, and the module.exports line from chunk 2.
- Create format.js and paste the display formatting code:
// Format a single task as a readable line
function formatTask(task) {
const status = task.status === 'done' ? '[x]' : '[ ]';
const done = task.completedAt ? ` (completed ${task.completedAt})` : '';
return `${status} #${task.id}: ${task.title}${done}`;
}
// Format a list of tasks, or show a message if empty
function formatTaskList(tasks) {
if (tasks.length === 0) return 'No tasks found.';
return tasks.map(formatTask).join('\n');
}
module.exports = { formatTask, formatTaskList };
What does this code do?
formatTask() turns a task object into a human-readable line like [ ] #1: my task. formatTaskList() maps over an array of tasks and joins them with newlines.
- Save format.js.
- Verify the formatter works by running:
node -e "const f = require('./format'); console.log(f.formatTask({id:1, title:'test', status:'pending'}))"
You should see [ ] #1: test printed.
Not seeing the formatted output?
Check that you saved format.js in the task-tracker folder. Confirm the template literal backticks in formatTask are correct and not regular quotes.
Help me debug my format.js file.
✔️ Awesome, I've got everything!
Great. Make sure you have saved format.js before moving on.
ⓧ I'd like to double check the full code
// Format a single task as a readable line
function formatTask(task) {
const status = task.status === 'done' ? '[x]' : '[ ]';
const done = task.completedAt ? ` (completed ${task.completedAt})` : '';
return `${status} #${task.id}: ${task.title}${done}`;
}
// Format a list of tasks, or show a message if empty
function formatTaskList(tasks) {
if (tasks.length === 0) return 'No tasks found.';
return tasks.map(formatTask).join('\n');
}
module.exports = { formatTask, formatTaskList };
Now for the CLI entry point. This file imports from both tasks.js and format.js and routes terminal commands to the right function.
- Create cli.js and paste the imports and the add command:
// Import task operations and display formatter
const { addTask, completeTask, listTasks } = require('./tasks');
const { formatTaskList } = require('./format');
// Parse command and arguments from the terminal input
const [,, command, ...args] = process.argv;
switch (command) {
case 'add': {
const title = args.join(' ');
if (!title) {
console.error('Usage: node cli.js add <title>');
process.exit(1);
}
// Print a simple confirmation without using formatTask
const task = addTask(title);
console.log(`Added task #${task.id}: ${task.title}`);
break;
}
Why does add skip the formatter?
The add case builds its own confirmation string with console.log instead of calling formatTask from format.js. The list command (coming next) DOES use the formatter. This separation matters later.
- Add the remaining commands below the add case in cli.js:
case 'done': {
const id = parseInt(args[0], 10);
if (isNaN(id)) {
console.error('Usage: node cli.js done <id>');
process.exit(1);
}
const task = completeTask(id);
console.log(`Completed task #${task.id}`);
break;
}
// List command uses formatTaskList from format.js
case 'list': {
const filter = args[0];
const tasks = listTasks(filter);
console.log(formatTaskList(tasks));
break;
}
default:
console.log('Commands: add <title>, done <id>, list [pending|done]');
}
What does this code do?
- done marks a task as complete by its numeric ID.
- list retrieves tasks and passes them through formatTaskList() from format.js before printing.
- The default case prints a usage guide if the command is unrecognized.
- Save cli.js.
✔️ Awesome, I've got everything!
Great. Make sure you have saved cli.js before testing.
ⓧ I'd like to double check the full code
Your cli.js should contain both chunks you pasted above, in order. Scroll up and confirm you have:
- The imports, process.argv destructuring, the switch opening, and the add case from chunk 1.
- The done case, list case, default case, and the closing } brace from chunk 2.
All three files are in place. Before you run the CLI, what do you think will happen when you add a task and then list all tasks?
- Test the full task tracker by running:
node cli.js add "test task"
You should see Added task #1: test task printed.
- Now list the tasks by running:
node cli.js list
You should see [ ] #1: test task printed. The list command ran the output through formatTaskList() in format.js.
Seeing MODULE_NOT_FOUND or a different error?
- Make sure all three files (tasks.js, format.js, cli.js) are saved inside the task-tracker folder.
- Confirm your terminal is inside task-tracker by running pwd. The path should end with /task-tracker.
- Check that the require paths in cli.js start with ./ (e.g. ./tasks, not just tasks).
Help me fix my task tracker CLI.
- Clean up the test data by running:
rm tasks.json
Commit the clean baseline
The reviewer subagent you will build later needs a clean Git history to diff against. Committing now gives it a known starting point.
- Stage and commit all files by running:
git add -A && git commit -m "initial: three-file task tracker"
You should see a commit summary listing the three files: cli.js, format.js, and tasks.js.
Git asking for your name and email?
If Git asks you to configure your identity before committing, run these two commands with your own name and email, then try the commit again:
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
Help me configure Git for my first commit.
Your three-file task tracker is committed and ready. Next up, you will create a builder subagent that can write code but cannot see the full codebase. That tunnel vision is the whole point.
Create the Builder Subagent
Your task tracker is committed and ready with three files that share data structures across a clean boundary. Now it's time to bring in the first of two AI agents that will work on this codebase.
A subagent is a scoped version of Claude Code that runs with its own tool restrictions and fresh context. The builder subagent you are about to create will have write access to files but deliberately no broad-scan tools like Grep or Glob. This simulates a developer heads-down in the files they already know about, unable to discover ripple effects in files they never opened.
In this step, get ready to:
- Create a builder subagent definition with restricted tools and scoped permissions.
- Delegate a feature request to the builder and watch it implement the change.
- Inspect the builder's results to see what it changed and what it left alone.
Create the agent definition file
Claude Code reads subagent definitions from .claude/agents/ in your project root. Each agent is a Markdown file with YAML frontmatter that controls its name, tools, permissions, and behaviour.
- Create the agents directory by running this command:
mkdir -p .claude/agents
- Create a new file at .claude/agents/builder.md and paste in the following content:
---
name: builder
description: Implements code changes with write access. Use for adding features, fixing bugs, and modifying source files.
tools: Read, Edit, Write, Bash
disallowedTools: Grep, Glob, Agent
model: sonnet
permissionMode: acceptEdits
maxTurns: 15
---
You are a focused code builder. Implement the requested changes efficiently.
Rules:
1. Read the files directly relevant to the change
2. Make the minimum modifications needed
3. Test your changes by running the code
4. Report what you changed and the test results
Focus on the data layer and entry point. Do not refactor unrelated files.
What do these frontmatter fields do?
- tools: Read, Edit, Write, Bash gives the builder write access to files and the ability to run commands.
- disallowedTools: Grep, Glob, Agent removes the builder's ability to search broadly across the codebase or spawn sub-subagents.
- permissionMode: acceptEdits auto-approves file edits so the builder can work without interactive permission prompts.
- maxTurns: 15 caps the builder at 15 API round-trips. If it cannot finish in 15 turns, it stops and reports what it accomplished.
- Confirm the file exists by running:
ls .claude/agents/
You should see builder.md listed in the output.
Don't see builder.md?
- Make sure you ran mkdir -p .claude/agents from inside the task-tracker directory, not from your Desktop or home folder.
- Check that the file is named exactly builder.md (not builder.txt or Builder.md).
- help me find where my builder.md file ended up
✔️ Awesome, I've got everything!
Great. Double check that you have saved builder.md before moving on.
ⓧ I'd like to double check the full code
Here is the complete .claude/agents/builder.md file. Make sure yours matches exactly, including the --- fences around the frontmatter.
---
name: builder
description: Implements code changes with write access. Use for adding features, fixing bugs, and modifying source files.
tools: Read, Edit, Write, Bash
disallowedTools: Grep, Glob, Agent
model: sonnet
permissionMode: acceptEdits
maxTurns: 15
---
You are a focused code builder. Implement the requested changes efficiently.
Rules:
1. Read the files directly relevant to the change
2. Make the minimum modifications needed
3. Test your changes by running the code
4. Report what you changed and the test results
Focus on the data layer and entry point. Do not refactor unrelated files.
Delegate the priority feature to the builder
The builder is defined but has not run yet. To use it, you ask Claude Code to delegate a task to it by name. Claude reads the agent definition, spawns a subagent with those exact tool restrictions, and hands it the task.
- In Claude Code, send this prompt:
Use the builder agent to add a priority field to tasks. The add command should accept an optional priority (high, medium, low) as the last argument, defaulting to medium. Store priority in the task object.
What's happening behind the scenes?
Claude Code spawns the builder as a subagent with a fresh, isolated context window. The builder has never seen your conversation history or the files Claude has already read. It starts from scratch.
Because the builder only has Read, Edit, Write, and Bash, it can only open files it already knows about. It cannot use Grep to search for references or Glob to discover files. Its system prompt also tells it to focus on the data layer and entry point.
- Watch the builder work. You will see it read tasks.js and cli.js, add priority support, and test by running the add command. It reports success when it finishes.
Builder not running?
- Make sure your builder.md file is saved inside .claude/agents/ in the task-tracker directory. Claude Code looks for agent definitions relative to your project root.
- Check that the frontmatter fences (---) are on their own lines. Missing or misformatted fences prevent Claude Code from parsing the agent definition.
- Claude Code won't find my builder agent when I ask it to delegate
Inspect the builder's results
The builder has finished and reported success. It updated tasks.js to accept and store a priority parameter. It updated cli.js to parse priority from the command arguments.
Before you verify, predict: what do you think the task data looks like in tasks.json after the builder's test run? Do you expect to see a priority field?
- Confirm the priority field exists by running:
cat tasks.json
You should see a JSON array containing at least one task object with a "priority" field. The feature works from the builder's perspective.
What did the builder change, and what did it skip?
The builder touched two files. In tasks.js, it added a priority parameter to addTask() and stored it in the task object. In cli.js, it parsed the priority argument from the command line.
It did NOT update format.js. Without Grep and Glob, the builder had no way to discover that format.js handles task display. Its system prompt told it to focus on the data layer and entry point, so it never looked beyond those two files. The builder tested its own work and everything looked fine.
No tasks.json file, or no priority field?
- If tasks.json does not exist, the builder may not have run its test. Try running node cli.js add "test task" high yourself and then check tasks.json again.
- If the file exists but has no priority field, re-run the builder prompt. Subagent behaviour can vary slightly between runs, but the tool restrictions and system prompt reliably steer it toward adding priority to tasks.js and cli.js.
- my builder agent didn't add the priority feature correctly
The builder reported success and its test passed. Everything looks fine from where it stands. In the next step, you will create a reviewer subagent with broad-scan tools and fresh context to check whether the builder's "success" holds up across the entire codebase.
Catch the Cross-File Gap with the Reviewer
The builder added priority support, tested it, and reported success. From its perspective, the job is done.
But should you trust a builder that only tested the code it touched? In this step, you will create a second subagent with completely different tools and zero knowledge of the builder's work. It starts cold, scans every file, and delivers its own verdict.
In this step, get ready to:
- Create a reviewer subagent with broad read-only tools and no write access.
- Run the reviewer to check cross-file consistency of the priority feature.
- Read the reviewer's verdict and see what the builder missed.
Create the reviewer subagent definition
The reviewer needs the opposite toolset from the builder. Where the builder could write files but could not search broadly, the reviewer can search the entire codebase but cannot change a single line.
- Create the file .claude/agents/reviewer.md (in the same .claude/agents/ directory where builder.md lives) and paste the following definition:
---
name: reviewer
description: Reviews code for cross-file consistency, missing updates, and correctness. Read-only access. Use after code changes to catch issues the builder missed.
tools: Read, Grep, Glob, Bash
disallowedTools: Edit, Write, Agent
model: sonnet
maxTurns: 10
---
You are an independent code reviewer with fresh context. You have NOT seen the builder's conversation or reasoning.
Review process:
1. Use Glob to find all source files in the project
2. Use Grep to trace how data structures flow across files
3. If a field was added or changed in one file, verify every other file that touches that data was updated
4. Check that display and formatting code reflects the current data model
5. Run the code to verify behavior matches expectations
Verdict:
- PASS: all files are consistent and the feature works end-to-end
- FAIL: list each issue with the file and what needs to change
Be thorough. The builder may have focused narrowly and missed files outside their immediate task.
How is the reviewer different from the builder?
The tool sets are mirror images. The builder gets Read, Edit, Write, Bash (write access, narrow scan). The reviewer gets Read, Grep, Glob, Bash (broad scan, no write access).
Notice there is no permissionMode set. The reviewer does not need one because Edit and Write are in disallowedTools. Tool restrictions are enforced at the infrastructure level, so the reviewer literally cannot modify files regardless of permission mode.
Agent is in both agents' disallowedTools. This prevents either subagent from spawning sub-subagents, keeping them as leaf agents that cannot delegate further.
- Save reviewer.md.
- Confirm the file exists by running:
ls .claude/agents/
You should see both builder.md and reviewer.md listed.
Only see builder.md?
Make sure you created reviewer.md inside .claude/agents/, not in the project root or inside a different folder.
Check that the file extension is .md, not .txt or .yaml. Help me find where my reviewer.md file ended up.
✔️ Awesome, I've got everything!
Great. Make sure you have saved reviewer.md before moving on.
ⓧ I'd like to double check the full code
Here is the complete reviewer.md file. Compare yours character by character, especially the frontmatter fields:
---
name: reviewer
description: Reviews code for cross-file consistency, missing updates, and correctness. Read-only access. Use after code changes to catch issues the builder missed.
tools: Read, Grep, Glob, Bash
disallowedTools: Edit, Write, Agent
model: sonnet
maxTurns: 10
---
You are an independent code reviewer with fresh context. You have NOT seen the builder's conversation or reasoning.
Review process:
1. Use Glob to find all source files in the project
2. Use Grep to trace how data structures flow across files
3. If a field was added or changed in one file, verify every other file that touches that data was updated
4. Check that display and formatting code reflects the current data model
5. Run the code to verify behavior matches expectations
Verdict:
- PASS: all files are consistent and the feature works end-to-end
- FAIL: list each issue with the file and what needs to change
Be thorough. The builder may have focused narrowly and missed files outside their immediate task.
Run the reviewer against the codebase
The reviewer has never seen the builder's conversation. Each subagent starts with a fresh, isolated context window. It will approach the codebase cold and form its own conclusions.
Why does fresh context matter?
A subagent does not see your conversation history, the files Claude has already read, or the reasoning the builder used. This isolation is the whole point. If the reviewer inherited the builder's context, it would carry the same blind spots.
Starting cold forces the reviewer to systematically scan the codebase instead of relying on assumptions from a prior conversation.
Before you run the reviewer, think about this: the builder added priority to tasks.js and cli.js and reported success. Do you think the reviewer will confirm that success, or find something the builder missed?
- In Claude Code, type the following prompt:
Use the reviewer agent to check whether the priority feature was implemented consistently across all files in the project.
Watch the reviewer work. It starts with zero knowledge of what the builder did.
What should I see?
The reviewer uses Glob to find all .js files in the project. Then it uses Grep to search for "priority" across every file. It reads each file that references task data. Finally, it delivers a verdict.
Reviewer not running?
Make sure reviewer.md is saved in .claude/agents/ and that the frontmatter starts with --- on the very first line of the file. Any blank line before the frontmatter will prevent Claude Code from recognizing the subagent definition.
Help me troubleshoot why my reviewer subagent is not being recognized by Claude Code.
Read the reviewer's verdict
The reviewer reports FAIL.
It identifies a specific gap: format.js formats tasks for display but has zero references to the priority field. When a user runs node cli.js list, tasks appear without their priority level.
- Confirm this yourself by running:
node cli.js add "verify display" high
node cli.js list
The output shows something like [ ] #1: verify display with no mention of high. The priority is stored in the data (check tasks.json to confirm), but format.js never reads it. The builder never noticed because its own test used the add command, which prints inline output instead of routing through format.js.
Why couldn't the builder catch this?
Two factors combined to create the blind spot. First, the builder's tool restrictions denied it Grep and Glob, so it had no way to search the codebase for every file that references task data. It only read the files it already knew about.
Second, its system prompt said "focus on the data layer and entry point." The builder followed those instructions correctly. It changed tasks.js (data) and cli.js (entry point) and tested the add command. It saw priority in the output and reported success.
The reviewer caught the gap because it started cold, scanned every .js file with Glob, and searched for "priority" across all of them with Grep. No assumptions, no inherited blind spots.
- Clean up the test data by deleting tasks.json so it does not interfere with later steps:
rm tasks.json
This is the maker-checker pattern in action. The builder did its job well within its scope. The reviewer did its job well within its scope. Neither agent could do both. Next up, you will close the loop by feeding the reviewer's findings back to the builder for a fix and then re-verifying.
Close the Maker-Checker Loop
The reviewer did its job. It found the gap in format.js that the builder's narrow focus missed. But finding a problem is only half of the maker-checker pattern.
The reviewer cannot fix what it found. Edit and Write are in its disallowedTools. This step completes the cycle: feed the reviewer's findings back to the builder, let it fix the code, then re-run the reviewer with fresh context to confirm the fix.
In this step, get ready to:
- Feed the reviewer's specific findings to the builder subagent so it knows exactly what to fix.
- Re-run the reviewer with fresh context to independently verify the fix.
- Test the complete feature end-to-end and commit the result.
Feed the reviewer's findings to the builder
The reviewer identified the exact issue: format.js formats tasks for display but has zero references to the priority field. By passing this specific finding to the builder, you give it a precise target instead of asking it to search blindly.
- In Claude Code, ask Claude to delegate the fix to the builder by typing this prompt:
Use the builder agent to fix the issue the reviewer found. format.js needs to display the priority field when formatting tasks.
What happens when you run this?
Claude delegates to the builder subagent, which starts with its own fresh context. Because you passed the reviewer's specific finding, the builder knows exactly which file to target.
Watch the builder's progress in Claude Code. It reads format.js, adds priority display to the formatTask function, and tests the change by running the CLI.
- Wait for the builder to finish. You should see it report that it updated format.js and ran a successful test.
- Verify the builder's changes by opening format.js. Look for a new priority line inside the formatTask function that formats the priority into the output string.
Builder didn't update format.js?
If the builder modified a different file or missed format.js entirely, re-run the prompt with more specifics. Mention the formatTask function by name and that it should include task.priority in the return string.
Why isn't the builder subagent updating format.js when I ask it to fix the priority display?
Re-run the reviewer with fresh context
The builder reports success. But that is exactly what happened last time, right before the reviewer found the gap. The whole point of maker-checker is that builders do not grade their own work.
- Ask Claude to delegate the re-verification to the reviewer by typing this prompt:
Use the reviewer agent to re-verify cross-file consistency for the priority feature.
Why does fresh context matter here?
The reviewer starts with a completely isolated context window. It has no memory of its previous FAIL report. It does not know what the builder changed or why.
This is not a limitation. It is the feature. A reviewer that remembered its last report might skip re-checking things it already flagged. Fresh context forces the reviewer to scan everything from scratch, catching any new inconsistencies the fix may have introduced.
Watch the reviewer work. It uses Glob to find all .js files, Grep to search for priority across the codebase, and finds references in all three files: tasks.js, cli.js, and format.js. The verdict: PASS.
Verify the full feature and commit
Both subagents agree the feature is complete. The builder says it fixed the code. The reviewer says all files are consistent. Time to see it for yourself.
Before you run the next commands, think about what you expect to see. The builder added priority support and the reviewer confirmed format.js now includes it. What should the task list output look like?
- Test the priority feature end-to-end by running these commands:
node cli.js add "ship feature" high
node cli.js list
The list output now includes the priority level next to each task. You should see something like [ ] #1: ship feature [high] in the formatted output.
What just happened across this whole cycle?
The builder added priority to tasks.js and cli.js but missed format.js. The reviewer caught the gap. The builder fixed it. The reviewer re-verified with fresh context. No single agent did both building and reviewing.
This is the core maker-checker pattern: build, review, fix, re-verify. Each role has scoped tools that match its job. The builder writes code but cannot scan broadly. The reviewer scans broadly but cannot write code. Separation of concerns enforced at the tool level.
Priority not showing in the list output?
Open format.js and check whether the formatTask function references task.priority. If the builder added the priority logic but used a different format string, the value might be there under a different label.
If format.js still has no priority reference, re-run the builder prompt from the first substep. Make sure the builder is targeting the formatTask function specifically.
Why isn't the priority field showing when I run node cli.js list?
- Commit the completed feature by running this command:
git add -A && git commit -m "feat: priority field with builder/reviewer cycle"
Why commit now?
This commit captures the state after a complete maker-checker cycle. The Git history now shows two commits: the original scaffold and the priority feature. The reviewer can use git diff against this baseline in future reviews.
✔️ Awesome, I've got everything!
Great. Make sure you have saved all files and the commit went through. You can confirm with git log --oneline to see both commits.
ⓧ I'd like to double check the full code
The only file that changed in this step is format.js. Open it and compare against the code below. The key change is the new priority line in formatTask.
function formatTask(task) {
const status = task.status === 'done' ? '[x]' : '[ ]';
const priority = task.priority ? ` [${task.priority}]` : '';
const done = task.completedAt ? ` (completed ${task.completedAt})` : '';
return `${status} #${task.id}: ${task.title}${priority}${done}`;
}
function formatTaskList(tasks) {
if (tasks.length === 0) return 'No tasks found.';
return tasks.map(formatTask).join('\n');
}
module.exports = { formatTask, formatTaskList };
The maker-checker loop is complete. You have a builder that writes code and a reviewer that catches what the builder misses. Next up, you will learn how to put guardrails on this pattern so subagents cannot loop indefinitely or burn through your API credits.
Bound Autonomous Subagent Work
Your builder/reviewer pipeline works. A narrowly scoped builder writes code, an independent reviewer catches what it misses, and the fix-then-re-verify cycle closes the gap.
There is still a problem. Without limits, a subagent can loop indefinitely and burn through API credits. Turn limits, concurrency caps, and budget controls are what separate a useful automation from a runaway process.
In this step, get ready to:
- Test the builder's turn limit by temporarily capping it at 3 turns.
- Learn environment-based controls for concurrency and budget.
- Demonstrate safe parallel fan-out with multiple scoped subagents.
Cap the builder at three turns
Your builder subagent already has a maxTurns field set to 15. This caps how many API round-trips the subagent can make before stopping. The reviewer has the same field set to 10. To see what happens when a subagent hits this ceiling, you will temporarily lower the builder's limit to something tiny.
- In Claude Code, open .claude/agents/builder.md.
- Find this line in the frontmatter:
maxTurns: 15
- Replace 15 with 3, so the line now looks like this:
maxTurns: 3
- Save builder.md.
Before you send the builder a complex task, think about what you expect to happen. With only 3 turns, do you think the builder will finish a multi-step feature, or will it run out of turns partway through?
- In Claude Code, type the following prompt and press Enter:
Use the builder agent to add due date support to tasks. The add command should accept an optional due date as an ISO date string after the priority argument. Store the due date in the task object. Add an overdue detection function that checks if a pending task's due date has passed.
Watch the builder work. After 3 API round-trips, it stops and reports what it accomplished so far. It does not run indefinitely.
What just happened?
The maxTurns field sets a hard ceiling on how many agentic turns a subagent can take before it is forced to stop. With only 3 turns, the builder likely read a file or two, started editing, and then hit the limit before finishing.
This is exactly the behaviour you want from a bounded agent. It stops cleanly and reports partial progress instead of looping forever.
The builder may have partially modified your files. Before restoring the turn limit, undo any partial changes so the codebase stays clean.
- Discard any partial changes the builder made by running:
git checkout -- .
- Open .claude/agents/builder.md and change maxTurns: 3 back to maxTurns: 15.
- Save builder.md.
Builder didn't stop at 3 turns?
Make sure you saved builder.md before running the prompt. Claude Code reads the agent definition file at delegation time, so unsaved changes are ignored.
Also confirm the maxTurns key is inside the YAML frontmatter block (between the --- markers), not in the body text below.
Help me figure out why my builder subagent isn't respecting the maxTurns limit
Learn concurrency and budget controls
The maxTurns field bounds a single subagent. Two environment-level controls bound the broader system when you start running multiple subagents or scripting Claude Code in production.
- CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS caps how many subagents can run at the same time. The default is 20. You can override it by setting the environment variable to a lower number.
- --max-budget-usd caps the total dollar amount spent on API calls before stopping. This flag only works in non-interactive (print) mode with the -p flag. Spend from subagents counts toward the cap.
For production scripts, you would combine both controls. Here is what that looks like:
claude -p --max-budget-usd 2.00 "task"
Which control matters in interactive mode?
The --max-budget-usd flag only applies to non-interactive mode (the -p flag). In interactive sessions like the one you are using right now, maxTurns on each subagent is your primary bound.
Together, these three controls form a safety net: per-agent turn limits, system-wide concurrency caps, and total budget ceilings.
Run two subagents in parallel
So far, you have run subagents one at a time. Claude Code also supports running multiple subagents concurrently in the background. Each subagent gets its own scoped tools, bounded turns, and fresh context.
How does parallel execution work?
In interactive sessions, fork mode is on by default. When Claude delegates to subagents, they run in the background. The CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS variable (default 20) controls how many can run at once.
Before you send this prompt, think about what you expect. Both subagents have Agent in their disallowedTools, so neither can spawn further subagents. Will they interfere with each other?
- Ask Claude to run two checks at the same time by typing the following prompt and pressing Enter:
In parallel, use the reviewer agent to check consistency of the priority feature across all files, and use a subagent to check whether error handling covers invalid priority values passed to the add command.
Watch Claude spawn two subagents that run concurrently in the background. Both return their findings without interfering with each other.
Why is this safe?
Each subagent runs with scoped tools, bounded turns, and fresh isolated context. The reviewer has read-only access, so it cannot conflict with the second subagent. Neither can spawn sub-subagents because Agent is in both agents' disallowedTools.
This is the safe fan-out pattern: scoped tools, bounded turns, and a concurrency limit.
Subagents running one at a time instead of in parallel?
Make sure you asked for both tasks in a single prompt. If you send them as two separate prompts, Claude runs them sequentially.
Fork mode must be on for background execution. It is on by default in interactive sessions. If you previously disabled it, re-enable it in your Claude Code settings.
Help me get my Claude Code subagents to run in parallel
Secret mission
Package the Pattern
You have been running the builder-reviewer cycle manually, typing each delegation prompt by hand. What if you could trigger the entire build, review, fix, and re-verify workflow with a single command? In this secret mission, you will package the full maker-checker pattern into a reusable Claude Code skill.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on your local machine with no cloud resources or running services. The only ongoing cost is Anthropic API credits, which are consumed per-use during Claude Code sessions and stop when you stop prompting.
Decide whether to keep your project files, or delete them entirely.
Resources you used:
- The task-tracker project directory on your Desktop (source files, agent definitions, skill definitions, git history).
- The tasks.json data file generated at runtime inside task-tracker.
Keep everything running
No action needed. Choose this if you plan to continue building on this project or want to reference your subagent definitions later.
- Your task-tracker directory stays on your Desktop with all source files, agent definitions, and git history intact.
- The .claude/agents/ and .claude/skills/ files are scoped to this project directory. They do not affect other Claude Code sessions outside this folder.
- No background processes are running. There are no ongoing costs from keeping these files.
Pause - I'll come back to this later
There are no running processes or cloud resources to pause. Your project is already in a paused state.
- If you want to free up a small amount of disk space, delete the tasks.json data file by running this command from inside the task-tracker directory:
- Your source files, agent definitions, and git history remain intact for when you return.
rm tasks.json
- When you come back, run node cli.js add "test task" to regenerate the data file and pick up where you left off.
Delete - I don't want to use this again
Remove the entire project directory to clean up all files created during this project.
- Navigate to your Desktop and delete the task-tracker folder by running these commands:
macOS
cd ~/Desktop
rm -rf task-tracker
Windows
cd ~\Desktop
rmdir /s /q task-tracker
- Confirm the folder is gone by running ls (macOS) or dir (Windows) on your Desktop. You should no longer see task-tracker in the listing.
This removes all source files, agent definitions, skill definitions, git history, and the runtime data file. Nothing from this project remains on your machine.
Nice Work!
Nice Work!
Nice work! You just built a multi-agent pipeline where no agent grades its own work, and every autonomous run is bounded by turn limits and budget caps.
You've learned how to:
- Enforce maker-checker separation by scoping subagent tools. The builder gets Read, Edit, Write, and Bash for focused implementation, while the reviewer gets Read, Grep, Glob, and Bash for broad read-only scanning with fresh context.
- Experience the designed obstacle that proves why builders should not verify their own output. The builder added priority support and reported success, but the reviewer immediately caught that format.js was never updated.
- Bound autonomous subagent execution with maxTurns per subagent, CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS for concurrency caps, and --max-budget-usd for API spend limits in non-interactive mode.
- Secret Mission: Packaged the full maker-checker cycle into a reusable /build-review skill that chains builder, reviewer, fix, and re-verify automatically.
Ready to quiz yourself?