Build a Plan You Can Reject Before the Patch
A plan is useful if it names files, symbols, risks, commands, and expected evidence. A list of "write code, add tests" doesn't help check the scope and doesn't surface hidden actions. In the companion project a plan step is a typed structure, not free text.
export interface PlanStep {
readonly id: string;
readonly title: string;
readonly reads: readonly string[];
readonly writes: readonly string[];
readonly commands: readonly CommandSpec[];
readonly evidence: string;
}
export interface ChangePlan {
readonly contract: TaskContract;
readonly steps: readonly PlanStep[];
}And because the plan is data, it can be checked by code before execution: that every write falls inside the allowed scope and every command is in the allowlist.
function validateStep(root: string, contract: TaskContract, step: PlanStep, policy: CommandPolicy): void {
if (step.id.trim() === "" || step.title.trim() === "" || step.evidence.trim() === "") {
throw new Error("Every plan step needs an id, title, and evidence target");
}
for (const path of [...step.reads, ...step.writes]) {
resolveContained(root, path);
}
for (const path of step.writes) {
const normalized = relative(root, resolveContained(root, path)).split("\\").join("/");
if (!pathAllowed(normalized, contract.allowedWritePaths)) {
throw new Error(`Plan writes outside allowed scope: ${path}`);
}
}
for (const command of step.commands) {
if (!policy.allowedCommandKeys.has(commandKey(command))) {
throw new Error(`Plan contains a command outside the allowlist: ${command.label}`);
}
}
}
export function validatePlan(root: string, plan: ChangePlan, policy: CommandPolicy): ChangePlan {
if (plan.steps.length === 0) {
throw new Error("Plan must have at least one step");
}
const ids = new Set<string>();
for (const step of plan.steps) {
if (ids.has(step.id)) {
throw new Error(`Duplicate plan step id: ${step.id}`);
}
ids.add(step.id);
validateStep(root, plan.contract, step, policy);
}
return plan;The validator's meaning shows through the review questions to each step field.
Step field · Review question
- reads - Are there enough facts to change this layer?
- writes - Do all paths fall inside the allowed scope?
- commands - Is the command exact, safe, and available in the environment?
- evidence - What observable result will prove the step?
- risk - What could break beyond the happy path?
- stop - What discovery requires a new owner decision?
To run a plan through a completeness check and see where it's silent about risks or evidence, use the lab.
Check the plan's completeness
0 of 5 filled. Don't allow edits while open items change the scope or the check.
The main thing is to forbid silent expansion. If during implementation it turns out you need to change a schema, a dependency, or the public API, the old plan no longer holds.
A plan is a rejection point, not a formality. The agent should come back with a new fact and a proposal, not silently append a patch beyond what was agreed. Rejecting a plan is cheap; rolling back an unplanned change in production is expensive.
The plan is agreed. But before writing code you must decide where the rules live - and here it's important not to mix shared instructions with product-specific ones.