Plan mode
# grade this stage (from your course directory)
npx theinference test --stage plan-mode --watchSometimes you want the agent to think out loud and propose before it touches a single file — survey the codebase, sketch the change, hand you a plan — with a hard guarantee that nothing on disk moves until you say go. That's plan mode. Here's the secret you'll prove this step: plan mode isn't a new subsystem. It's the permission engine you already built (Chapter 06) running with one rule flipped — every side-effecting tool is denied, every read-only tool still flows. The agent can Read, Glob, Grep, WebSearch and reason all it likes; the moment it reaches for Edit, Write, or Bash, the gate slams shut.
So the whole job reduces to a single classification: is this tool read-only? Read-only tools observe the world without changing it — they're safe to run before you've approved anything. Everything else has a side effect and must be blocked until you exit plan mode. Get that split right and plan mode falls out for free: feed the answer into the engine as the mode default, and a denied write turns into a proposal instead of an edit.
Read / View / Glob / Grep / WebSearch ─▶ allowed (observe only, never mutates)
Write / Edit / Bash / ... ─▶ DENIED (side effect — proposed, not run)
ExitPlanMode ─▶ you approve ─▶ mode flips off ─▶ writes re-enabled
What to build
Implement allowedInPlan
Implement allowedInPlan(tool): return true for the read-only tools — Read, View, Glob, Grep, WebSearch — and false for every side-effecting tool (Write, Edit, Bash, and anything else). This is the predicate the engine consults when the active mode is plan: an allowed tool runs, a blocked one is denied and surfaced to you as a proposal. Make the read-only set the allowlist — default to false for unknown tools, so a tool you've never seen can't sneak a side effect past the gate.
/** In plan mode, only read-only tools may run; everything else is denied. */
export function allowedInPlan(tool: string): boolean {
// 1. read-only allowlist: Read, View, Glob, Grep, WebSearch -> true
// 2. everything else (Write, Edit, Bash, unknown) -> false
}
Don't write this as "block Write/Edit/Bash, allow the rest." A denylist fails open: the day someone adds a new side-effecting tool, it slips through plan mode and writes to disk before you ever approved. Name the safe tools and deny everything else by default. Plan mode's only promise — nothing mutates until you say so — depends on this being fail-closed.
The grader runs ./your_program.sh plan, which calls allowedInPlan for each tool in a fixed list — the read-only five plus the side-effecting ones — and prints the verdicts as JSON. Every read-only tool must come back true, every side-effecting and unknown tool false, matching the reference exactly. One leaked true on a write tool fails the step: that's the engine letting an edit run before you approved it.
./your_program.sh plan
# tools: ["Read","Glob","WebSearch","Edit","Bash","DeployToProd"]
# -> {"Read":true,"Glob":true,"WebSearch":true,"Edit":false,"Bash":false,"DeployToProd":false}