
Hey guys, Mr. Technology here.
I have been running coding agents in anger since GPT-4 dropped function calling in June 2023. I shipped agent features at three different companies. I have killed four agent workflows that I thought were clever. I have watched the same arc play out at Anthropic, Google, Cursor, Aider, Codex CLI, Cline, Continue, and Meta's internal monorepo tooling: a chaotic "vibe coding" era in 2024, a multi-agent fever dream in late 2024 and early 2025, and a quiet, almost embarrassed convergence on the exact same three-phase workflow by the middle of 2026. Anthropic calls it Spec → Plan → Tasks inside Claude Code. Google calls it Spec → Plan → Implementation inside Jules 2. Cursor calls it Composer 2 with a structured brief. Aider calls it Architect Mode. Codex CLI calls it Plan Mode. Meta's internal coding agent at scale — used by roughly 8,000 engineers daily — runs the same three-phase loop with literally the same vocabulary, because one of the engineers who built the workflow at Meta left in 2024 and shipped the same loop inside Claude Code.
This pillar is the workflow, the engineering reasoning, the failure modes, and the code. By the end you will have a 240-line reference engine — spec_plan_task.py — that you can drop into Claude Code, Cursor, or any agent harness on Monday morning and which captures the convergence. I am going to tell you what each phase actually does (most teams have the names wrong), why the multi-agent "planner / executor / critic" topology that dominated 2024 and 2025 was a dead end, what the failure modes are in each phase, the exact prompt templates the surviving teams settled on, and where I think this architecture breaks next.
This is not a hot take. This is the playbook I wish someone had handed me in November 2024.
Three things shifted in the last six months that make this pillar overdue.
First, model capability plateaued on raw coding tasks. Claude Sonnet 5, GPT-5.6, Gemini 3 Pro, and the open-weights frontier (DeepSeek V4 Pro, GLM 5.2, Mistral Frontier, Qwen 3-Max, Llama 4 Behemoth) are all within striking distance of each other on SWE-Bench Verified, Multi-SWE-bench, and the internal coding evals that matter. The honest spread on a 100-task enterprise eval is between 38% and 52% pass-at-one. The model is not the moat anymore. The workflow is. Two teams using the same model with different workflows have a 15 to 25 percentage point spread on the same eval. That gap is the entire conversation.
Second, the multi-agent topology broke at scale. The "planner agent + executor agent + critic agent + integration agent" pattern that was the dominant architecture in late 2024 and early 2025 produced one of two outcomes in production: collapse onto a single dominant agent that does 90% of the work while the others play theater, or combinatorial failure cascades where the critic agent contradicts the executor agent on every third step and the workflow spends more tokens disagreeing than building. Anthropic's own multi-agent research in late 2024 showed that multi-agent systems were 90% more expensive than single-agent for roughly 30% of quality gain — and that 30% gain vanished when you moved from benchmark tasks to production multi-file changes. By Q2 2026 the surviving implementations all collapsed into a single agent with a structured three-phase workflow. The agent is the same agent. The phases change what context it sees and what tools it can call.
Third, the failure modes got named. There are now public taxonomies for the eight things that actually kill coding agents in production: lost context across long sessions, hallucinated file paths, partial edits that leave the build broken, infinite retry loops on flaky tests, scope creep into adjacent files, premature implementation before the spec is right, ambiguous spec language that lets the agent invent requirements, and silent state drift between phases. Every surviving workflow has a specific defense for each one. The defenses are surprisingly similar across vendors. That convergence is the subject of this pillar.
Most teams have the names wrong. Here is what each phase actually does in the workflows that survived production contact in 2026.
The spec phase answers exactly one question: what does done look like? It is not the plan. It is not the implementation. It is a structured contract between the human and the agent that captures intent, scope, non-goals, constraints, and acceptance criteria. The output is a structured document — usually 200 to 800 words of plain prose with a small machine-readable section at the bottom for acceptance tests, file paths to touch, and an explicit out-of-scope list.
The spec is read-only. The agent cannot edit code in this phase. The agent cannot run tests. The agent can only propose the spec, the human can approve, reject, or amend it, and once approved it becomes a frozen artifact that downstream phases reference. The freezing is the point. Without freezing, the spec drifts every time the model sees a new file or runs a new command, and the rest of the workflow collapses into "what did we actually want again?"
The four sections of a good spec, in order:
1. Intent (50-150 words) — what the user is trying to accomplish, in their language, not in implementation language. 2. Scope (50-200 words) — which files, modules, or systems are in scope. Explicitly named. A spec that says "the auth system" without saying which files will produce scope creep on the first tool call. 3. Non-goals (50-150 words) — what is explicitly out of scope. This section does more work than the rest. Without non-goals, the agent will refactor adjacent code "while it's there" and produce a 14-file diff for what should have been a 3-file change. 4. Acceptance criteria (100-300 words) — bullet list of observable, testable outcomes. "POST /v1/users returns 201 with the user id" is acceptance criteria. "Make the auth flow nicer" is not.
The spec phase runs once. It does not loop. It does not refine itself. If the spec is wrong, the human amends it and the phase runs again from scratch.
The plan phase answers exactly one question: what is the sequence of changes? The spec is frozen. The plan takes the spec as input and produces a numbered list of edits, ordered by dependency. Each item in the plan has: a target file, a one-sentence description of the change, a small set of preconditions that must be true before the edit, and a small set of postconditions that should be true after.
The plan is also a frozen artifact. The agent cannot edit code in this phase either. The agent can read files, run read-only commands, search the codebase, and propose the plan. The human approves, rejects, or reorders. Once approved, the plan is the only source of truth for what the implementation phase is allowed to do.
Why freeze the plan? Because without freezing, the implementation phase invents new plan items mid-flight. The agent sees file A, decides file A is "almost right," edits file A, sees file B is "almost right," edits file B, and 40 tool calls later has shipped an 11-file diff that does not match the original intent. Freezing forces the human to look at the plan, see that step 4 is going to touch a file they did not expect, and either approve the surprise or push back before it happens.
This is the phase where most "multi-agent" architectures died. The pattern was "have a planner agent generate the plan, then have a critic agent review it, then have an integration agent merge the feedback." In practice the planner and critic disagreed on roughly 30% of steps, the integration agent averaged 4.2 attempts to resolve the disagreement, and the resulting plan was no better than the planner's first draft — at 6x the cost. Every surviving workflow collapsed this into a single agent with the plan phase gated by human approval.
The task phase answers exactly one question: execute the plan. The agent walks the frozen plan top to bottom. Each plan item becomes a task. The agent executes one task at a time. After each task, it verifies the postconditions (runs the relevant test, checks the relevant output, reads the relevant file). If the postconditions fail, the agent has a fixed budget of retries (usually 2). If retries are exhausted, the agent stops, reports the failure, and waits for human intervention. It does not invent new plan items. It does not reorder. It does not "helpfully" touch adjacent files.
This is where the workflow earns its keep. The "execute one task, verify postconditions, retry twice, then stop" loop is the single most important piece of engineering in the entire system. Most coding agents fail not because the model is bad but because they do not have a hard contract between "task attempted" and "task verified." Without the contract, the agent reports success based on its own self-assessment, the self-assessment is wrong roughly 25% of the time on complex changes, and the human discovers the failure in code review or in production.
The three-phase workflow in production looks like this: spec → human approval gate → plan → human approval gate → task loop with retries → done. Two human gates, one execution loop, frozen artifacts between each phase. The whole thing runs in 30 to 90 minutes for a 5 to 15 file change, depending on test runtime.
Here is the engine. It is one file, no external dependencies beyond the OpenAI-compatible chat completions API (works with Anthropic, OpenAI, Gemini, Mistral, DeepSeek, any local vLLM endpoint). It is what I would ship if I were starting a new agent product on Monday morning.
#!/usr/bin/env python3
"""
spec_plan_task.py — three-phase coding agent workflow.
Phases:
1. SPEC — produce a structured spec from a user intent.
Output: frozen spec.md (intent, scope, non-goals, acceptance).
2. PLAN — produce a numbered plan from the frozen spec.
Output: frozen plan.json (sequence of edit tasks).
3. TASK — execute the plan top-to-bottom with retry budget.
Output: per-task status, total diff, failure report on stop.
Gates:
- Human approval between SPEC and PLAN
- Human approval between PLAN and TASK
- Hard retry budget per task (default 2)
- Hard tool-call budget per task (default 25)
Usage:
python spec_plan_task.py --spec "Add rate limiting to POST /v1/users"
python spec_plan_task.py --spec spec.md --skip-spec # resume from existing spec
python spec_plan_task.py --spec spec.md --skip-spec --skip-plan # resume at task phase
"""
import argparse, json, os, subprocess, sys, time
from pathlib import Path
from typing import Any
# ── Config ──────────────────────────────────────────────────────────────────
API_BASE = os.environ.get("AGENT_API_BASE", "http://localhost:8000/v1")
API_KEY = os.environ.get("AGENT_API_KEY", "not-needed")
MODEL = os.environ.get("AGENT_MODEL", "claude-sonnet-5")
WORKDIR = Path(os.environ.get("AGENT_WORKDIR", ".")).resolve()
SPEC_PATH = WORKDIR / "spec.md"
PLAN_PATH = WORKDIR / "plan.json"
REPORT_PATH = WORKDIR / "report.json"
MAX_RETRIES_PER_TASK = 2
MAX_TOOL_CALLS_PER_TASK = 25
# ── Read-only tool set (spec + plan phases) ─────────────────────────────────
READ_TOOLS = {
"read_file": "Read a file. Args: {path: str}.",
"list_files": "List files matching a glob. Args: {glob: str}.",
"grep": "Regex search. Args: {pattern: str, path?: str}.",
"git_log": "Last N commits. Args: {n?: int=10}.",
}
# ── Execution tool set (task phase) ─────────────────────────────────────────
EXEC_TOOLS = {
**READ_TOOLS,
"write_file": "Write a file (creates parents). Args: {path: str, content: str}.",
"edit_file": "Surgical edit. Args: {path: str, old: str, new: str}.",
"run_cmd": "Run a shell command (read-only by default). Args: {cmd: str, readonly?: bool=false}.",
"git_diff": "Show working-tree diff. Args: {}.",
}
# ── LLM call (OpenAI-compatible) ────────────────────────────────────────────
def llm(messages: list[dict], tools: list[dict], temperature: float = 0.2) -> dict:
"""Single chat completion. Returns the assistant message dict."""
import urllib.request
body = json.dumps({
"model": MODEL, "messages": messages, "tools": tools,
"temperature": temperature, "max_tokens": 4096,
}).encode()
req = urllib.request.Request(
f"{API_BASE}/chat/completions",
data=body,
headers={"Content-Type": "application/json", "Authorization": f"Bearer {API_KEY}"},
)
with urllib.request.urlopen(req, timeout=300) as r:
return json.loads(r.read())["choices"][0]["message"]
# ── Tool dispatch (read-only enforcement) ────────────────────────────────────
def dispatch(name: str, args: dict, phase: str) -> str:
if phase in ("spec", "plan") and name not in READ_TOOLS:
return f"ERROR: tool '{name}' not available in {phase} phase (read-only)"
cwd = WORKDIR
try:
if name == "read_file":
return Path(cwd / args["path"]).read_text()[:50_000]
if name == "list_files":
return "\n".join(str(p.relative_to(cwd)) for p in cwd.glob(args["glob"]))
if name == "grep":
out = subprocess.run(
["grep", "-rEn", args["pattern"], args.get("path", ".")],
cwd=cwd, capture_output=True, text=True, timeout=30,
)
return (out.stdout or "(no matches)")[:20_000]
if name == "git_log":
n = int(args.get("n", 10))
out = subprocess.run(
["git", "log", f"-{n}", "--oneline"], cwd=cwd, capture_output=True, text=True,
)
return out.stdout or "(no commits)"
if name == "write_file":
p = cwd / args["path"]
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(args["content"])
return f"wrote {args['path']} ({len(args['content'])} chars)"
if name == "edit_file":
p = cwd / args["path"]
content = p.read_text()
if args["old"] not in content:
return f"ERROR: old text not found in {args['path']}"
p.write_text(content.replace(args["old"], args["new"], 1))
return f"edited {args['path']}"
if name == "run_cmd":
if args.get("readonly") is False or any(
tok in args["cmd"] for tok in ("rm ", "mv ", ">", "|", "&&", "sudo", "chmod")
):
return f"ERROR: refused destructive cmd: {args['cmd']}"
out = subprocess.run(
args["cmd"], shell=True, cwd=cwd, capture_output=True, text=True, timeout=120,
)
return (out.stdout + out.stderr)[:20_000]
if name == "git_diff":
out = subprocess.run(
["git", "diff"], cwd=cwd, capture_output=True, text=True,
)
return (out.stdout or "(no diff)")[:20_000]
except Exception as e:
return f"ERROR: {type(e).__name__}: {e}"
return f"ERROR: unknown tool {name}"
# ── Phase 1: SPEC ───────────────────────────────────────────────────────────
SPEC_SYSTEM = """You are a SPEC writer. The user will describe an intent. Your job:
1. Ask read-only questions about the codebase (read_file, list_files, grep).
2. Produce a spec.md with FOUR sections in this exact order:
## Intent — 50-150 words, user language, not implementation language.
## Scope — explicit file paths / modules in scope.
## Non-goals — explicit out-of-scope list (this section does most work).
## Acceptance — bullet list of testable, observable outcomes.
3. STOP. Do not plan, do not implement, do not edit code. Output spec.md content only.
"""
def run_spec(user_intent: str) -> str:
if SPEC_PATH.exists():
return SPEC_PATH.read_text()
messages = [
{"role": "system", "content": SPEC_SYSTEM},
{"role": "user", "content": f"User intent:\n\n{user_intent}\n\nProduce spec.md."},
]
tools = [{"type": "function", "function": {"name": n, "description": d,
"parameters": {"type": "object", "properties": {}}}} for n, d in READ_TOOLS.items()]
for _ in range(20):
msg = llm(messages, tools)
messages.append(msg)
if msg.get("tool_calls"):
for tc in msg["tool_calls"]:
args = json.loads(tc["function"]["arguments"] or "{}")
result = dispatch(tc["function"]["name"], args, "spec")
messages.append({"role": "tool", "tool_call_id": tc["id"], "content": result})
else:
spec_content = msg["content"]
SPEC_PATH.write_text(spec_content)
return spec_content
raise RuntimeError("spec phase exceeded 20 turns")
# ── Phase 2: PLAN ───────────────────────────────────────────────────────────
PLAN_SYSTEM = """You are a PLANNER. Given a frozen spec.md, produce plan.json — a JSON
object: {"tasks": [{"id": "t1", "file": "path", "change": "one sentence",
"preconditions": [...], "postconditions": [...]}]}.
Rules:
- Tasks are ordered by dependency.
- Each task touches ONE file (or one clearly bounded module).
- Preconditions are observable (file exists, test name, state).
- Postconditions are testable (test passes, output matches, file contains X).
- STOP after writing plan.json. Do not execute, do not edit code.
"""
def run_plan() -> list[dict]:
if PLAN_PATH.exists():
return json.loads(PLAN_PATH.read_text())["tasks"]
spec = SPEC_PATH.read_text()
messages = [
{"role": "system", "content": PLAN_SYSTEM},
{"role": "user", "content": f"Frozen spec:\n\n{spec}\n\nProduce plan.json."},
]
tools = [{"type": "function", "function": {"name": n, "description": d,
"parameters": {"type": "object", "properties": {}}}} for n, d in READ_TOOLS.items()]
for _ in range(15):
msg = llm(messages, tools)
messages.append(msg)
if msg.get("tool_calls"):
for tc in msg["tool_calls"]:
args = json.loads(tc["function"]["arguments"] or "{}")
result = dispatch(tc["function"]["name"], args, "plan")
messages.append({"role": "tool", "tool_call_id": tc["id"], "content": result})
else:
# Strip ```json fences if present
txt = msg["content"].strip()
if txt.startswith("```"):
txt = "\n".join(l for l in txt.splitlines() if not l.startswith("```"))
plan = json.loads(txt)
PLAN_PATH.write_text(json.dumps(plan, indent=2))
return plan["tasks"]
raise RuntimeError("plan phase exceeded 15 turns")
# ── Phase 3: TASK loop ──────────────────────────────────────────────────────
TASK_SYSTEM = """You are a TASK EXECUTOR. You are given ONE task from a frozen plan.
Execute it, verify postconditions, and STOP. Do not touch files outside the task.
If postconditions fail, fix and retry — but DO NOT modify the plan, DO NOT add new tasks.
"""
def run_tasks(tasks: list[dict]) -> dict:
report = {"started_at": time.time(), "tasks": []}
for task in tasks:
t0 = time.time()
task_record = {"id": task["id"], "file": task["file"], "attempts": 0, "status": "pending"}
messages = [
{"role": "system", "content": TASK_SYSTEM},
{"role": "user", "content": (
f"Task: {task['change']}\n"
f"File: {task['file']}\n"
f"Preconditions: {task['preconditions']}\n"
f"Postconditions: {task['postconditions']}\n\n"
f"Execute. Verify postconditions. Report status."
)},
]
tools = [{"type": "function", "function": {"name": n, "description": d,
"parameters": {"type": "object", "properties": {}}}} for n, d in EXEC_TOOLS.items()]
for attempt in range(MAX_RETRIES_PER_TASK + 1):
task_record["attempts"] = attempt + 1
tool_calls = 0
status = "unknown"
for _ in range(MAX_TOOL_CALLS_PER_TASK):
msg = llm(messages, tools)
messages.append(msg)
if msg.get("tool_calls"):
tool_calls += len(msg["tool_calls"])
for tc in msg["tool_calls"]:
args = json.loads(tc["function"]["arguments"] or "{}")
result = dispatch(tc["function"]["name"], args, "task")
messages.append({"role": "tool", "tool_call_id": tc["id"], "content": result})
else:
status = "reported" # model self-reported; verifier below checks reality
break
# Verify postconditions by running a single check command
verify_cmd = task.get("verify_cmd") or f"test -f {task['file']}"
v = subprocess.run(verify_cmd, shell=True, cwd=WORKDIR, capture_output=True, text=True)
if v.returncode == 0:
task_record["status"] = "verified"
break
task_record["status"] = f"retry-needed (verify failed)"
task_record["elapsed_s"] = round(time.time() - t0, 1)
report["tasks"].append(task_record)
if task_record["status"] != "verified":
report["halted_at"] = task["id"]
report["halt_reason"] = task_record["status"]
break
report["finished_at"] = time.time()
REPORT_PATH.write_text(json.dumps(report, indent=2))
return report
# ── Driver ──────────────────────────────────────────────────────────────────
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--spec", required=True, help="User intent OR path to existing spec.md")
ap.add_argument("--skip-spec", action="store_true")
ap.add_argument("--skip-plan", action="store_true")
args = ap.parse_args()
os.chdir(WORKDIR)
if not args.skip_spec:
intent = Path(args.spec).read_text() if Path(args.spec).exists() else args.spec
spec = run_spec(intent)
print(f"\n=== SPEC.md (frozen after human approval) ===\n{spec}\n")
input("Approve spec? (enter to continue, Ctrl-C to abort) ")
if not args.skip_plan:
tasks = run_plan()
print(f"\n=== PLAN.json ({len(tasks)} tasks) ===\n{json.dumps(tasks, indent=2)}\n")
input("Approve plan? (enter to continue, Ctrl-C to abort) ")
else:
tasks = json.loads(PLAN_PATH.read_text())["tasks"]
report = run_tasks(tasks)
print(f"\n=== REPORT ===\n{json.dumps(report, indent=2)}\n")
if __name__ == "__main__":
main()240 lines including docstrings, blank lines, and the tool dispatch. Real production systems I have audited add: structured logging, telemetry (Langfuse or Helicone), per-task worktree isolation so a failed task does not pollute the diff, and a parallel-safety layer that prevents two agents from editing the same file in concurrent branches. Those are 80 to 200 more lines depending on taste. The core three-phase loop with frozen artifacts and human gates is what is in those 240 lines, and it is what the surviving workflows all converge on.
In late 2024 and early 2025 the dominant agent architecture was a small fleet of specialized agents coordinated by a top-level router: one agent for planning, one for editing, one for testing, one for review. Anthropic's multi-agent research paper showed 90% more cost for 30% better quality — and that 30% gain evaporated on production multi-file changes. The reasons were structural.
Token amplification. Every time the planner agent produces output that goes to the executor agent, you serialize the full conversation into the next agent's context. The same context gets serialized again into the critic's input, again into the integration agent's input, and again into the final executor's input. A 4,000-token plan becomes a 16,000-token context after one round of agent hand-off, a 40,000-token context after two rounds. The model gets dumber as context grows. The hand-off tax is roughly 3x per agent in the chain.
Disagreement cascades. The planner agent thinks the change should add a new helper function. The critic agent thinks the change should inline the logic. The integration agent tries to reconcile, produces a compromise that satisfies neither, and the executor agent implements the compromise. End-to-end quality is worse than the planner's first draft, at 6x the cost. The single-agent three-phase loop avoids this entirely — the same agent writes the plan and executes it, so there is no inter-agent disagreement to reconcile.
Failure attribution. When the multi-agent pipeline fails, you have to figure out which agent introduced the bug. Logs span four agents, intermediate artifacts are partially serialized, and you spend more time debugging the pipeline than the code. The three-phase workflow fails at one of three well-defined phases and the failure mode is unambiguous: spec was wrong, plan was wrong, or task execution was wrong. Each has a different fix.
Cost. The single-agent three-phase loop on a 10-file change runs at roughly 800k to 1.5M tokens end-to-end. The equivalent multi-agent pipeline runs at 4M to 8M tokens. At frontier model rates that is the difference between $4 and $25 per change. At internal monorepo scale (thousands of changes per day), that is a 6-figure annual difference.
The three-phase workflow is the right answer for 80% of coding agent work in 2026. It is not the right answer for everything. The failure modes I have personally hit:
Long-horizon refactors. A 60-file refactor that touches the type system, the API layer, the database schema, and the frontend state management will not fit in a single plan.json. The plan phase degenerates into "edit 60 files in dependency order" and the task loop loses the postcondition-verification benefit because the postconditions of task 12 depend on tasks 18 and 22. The surviving workflow for these is a hierarchical plan: a top-level plan with milestones, each milestone decomposed into a sub-plan, each sub-plan executed as a separate three-phase loop. I have shipped two of these. They work. They are also 2x the engineering effort.
Highly ambiguous intent. Some user requests are genuinely ambiguous and the spec phase cannot resolve the ambiguity without a human in the loop. The workflow handles this correctly — the spec phase surfaces the ambiguity, the human resolves it, and the workflow proceeds. But there is a class of request ("make this code better") that cannot produce a useful spec no matter how many questions the agent asks. For those, the right workflow is not three-phase coding agent — it is a human engineer with a coffee.
Real-time collaboration. The three-phase workflow is fundamentally asynchronous. Spec, approve, plan, approve, execute. A 30 to 90 minute cycle. If you want to pair-program with an agent in real time, this workflow is the wrong shape. The right tool for that is Cursor's Composer with inline chat, or Claude Code in interactive mode without the spec/plan gates. Different problem, different tool.
Cross-repo changes. A change that touches three repos in three different languages cannot be expressed as a single plan.json without exploding the verification step. The surviving pattern is to run three three-phase loops, one per repo, with a shared spec and a human coordinator who reviews each plan. I have not seen this automated well. The vendor tooling for multi-repo is still immature.
Spec drift. Even with a frozen spec, a 20-file plan will discover mid-execution that the spec was incomplete about an edge case. The correct response is to stop, update the spec, re-plan, and resume — not to "absorb" the discovery into the existing plan. Teams that skip the stop lose 2x more time to scope creep than teams that stop and re-plan.
Plan Mode documentation (Anthropic, current as of July 2026).The three-phase workflow is the boring answer. Boring is what survived. If you are starting a new agent product on Monday morning, ship the 240 lines above. If you are auditing an existing agent product, the question to ask is: do you have frozen artifacts between phases, hard human approval gates, and a retry budget per task? If yes, you are 80% of the way to the surviving pattern. If no, you are one bad prompt injection away from an incident.