You should pin a numeric diff budget before you leave any paid coding agent. Paid tools often apply large patches in one turn without exposing the true blast radius. A self-hosted loop will hide that radius even less, because nobody ships a polished review pane. This diary freezes that budget, scores leftover paid diffs, and refuses oversized patches after cutover.
The failure you will hit on day one
You will paste a leftover transcript into the new loop and watch it rewrite half a package. The paid agent trained you to accept that volume because the UI grouped files into a single approve button. After cutover, you own every hunk, every import, and every test file the model touched. Without a numeric budget in the repo, those vibe-sized patches become your default engineering process.
Recent community threads keep splitting casual generation from engineering practice, and that split is useful here. Engineering, during a cutover, means you can reject a change for size before you debate style. You need numbers for files, hunks, and net lines, not a feeling that the patch looks focused. The paid product will not export those numbers in a durable form you can keep after canceling.
What a diff budget actually records
A budget is not a linter, and it is not a model quality score either. It is a hard ceiling on how much tree mutation one agent turn may propose. You record four integers: max files, max hunks, max lines added, and max lines deleted. You also record path prefixes that stay forbidden, because leftover paid runs love to rewrite lockfiles.
Store the budget beside the repository, not inside a vendor dashboard you are about to abandon. Treat formatter-only files as a separate class so a noisy lockfile cannot burn the whole budget. Keep the budget file committed so every teammate scores leftovers against the same numeric ceilings.
{
"version": 1,
"max_files": 6,
"max_hunks": 18,
"max_lines_added": 180,
"max_lines_deleted": 120,
"forbidden_prefixes": [
"vendor/",
"dist/",
"pnpm-lock.yaml",
"package-lock.json"
],
"ignore_globs": ["**/*.snap", "**/*.min.js"]
}
Label the JSON above as a proposed contract, not a benchmark from a production fleet. Tune those integers against leftover paid diffs rather than against a blog post default. Commit the file as .diff-budget.json so later hosts inherit the same ceilings without a dashboard login.
Step 1: Inventory leftover paid patches
Export every leftover apply from the paid tool before you cancel the remaining seat. Prefer unified diffs stored on disk over screenshots copied from a vendor review pane. Name files with the original turn id so you can replay arguments later without the vendor.
mkdir -p leftovers/paid-diffs
# Proposed local layout; replace the export step with whatever your vendor actually provides.
find leftovers/paid-diffs -name '*.diff' | wc -l
git diff --numstat HEAD > leftovers/working-tree.numstat
If the vendor only keeps chat transcripts, reconstruct diffs by copying the last applied tree into a throwaway git worktree. You should not start the free-stack cutover until this directory is non-empty or you have proven there were no leftovers. Empty inventories are usually a smell, because paid agents almost always leave a trail.
Step 2: Parse unified diffs into budget counters
Write a small parser so you never have to eyeball another 400-line leftover patch. The script below is a proposed local harness that understands unified diffs only and ignores binary markers. Put it beside the budget file and treat exit status one as a hard apply refusal.
#!/usr/bin/env python3
"""Proposed scorer: pin this beside .diff-budget.json. Not a vendor integration."""
from __future__ import annotations
import json
import pathlib
import re
import sys
from dataclasses import dataclass, field
HUNK_RE = re.compile(r"^@@ ")
FILE_RE = re.compile(r"^diff --git a/(.+) b/(.+)$")
@dataclass
class DiffStats:
files: set[str]
hunks: int = 0
added: int = 0
deleted: int = 0
forbidden: list[str] = field(default_factory=list)
def parse_unified(text: str) -> DiffStats:
stats = DiffStats(files=set())
current = None
for line in text.splitlines():
file_match = FILE_RE.match(line)
if file_match:
current = file_match.group(2)
stats.files.add(current)
continue
if line.startswith("+++ ") or line.startswith("--- "):
continue
if HUNK_RE.match(line):
stats.hunks += 1
continue
if current is None:
continue
if line.startswith("+") and not line.startswith("+++"):
stats.added += 1
elif line.startswith("-") and not line.startswith("---"):
stats.deleted += 1
return stats
def load_budget(path: pathlib.Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def violates(stats: DiffStats, budget: dict) -> list[str]:
reasons = []
for path in sorted(stats.files):
for prefix in budget["forbidden_prefixes"]:
if path.startswith(prefix) or path.endswith(prefix):
stats.forbidden.append(path)
if len(stats.files) > budget["max_files"]:
reasons.append(f"files {len(stats.files)} > {budget['max_files']}")
if stats.hunks > budget["max_hunks"]:
reasons.append(f"hunks {stats.hunks} > {budget['max_hunks']}")
if stats.added > budget["max_lines_added"]:
reasons.append(f"added {stats.added} > {budget['max_lines_added']}")
if stats.deleted > budget["max_lines_deleted"]:
reasons.append(f"deleted {stats.deleted} > {budget['max_lines_deleted']}")
if stats.forbidden:
reasons.append("forbidden " + ",".join(stats.forbidden))
return reasons
def main(argv: list[str]) -> int:
budget = load_budget(pathlib.Path(argv[1]))
target = pathlib.Path(argv[2])
failed = 0
paths = [target] if target.is_file() else sorted(target.glob("*.diff"))
for diff_path in paths:
stats = parse_unified(diff_path.read_text(encoding="utf-8", errors="replace"))
reasons = violates(stats, budget)
status = "FAIL" if reasons else "PASS"
if reasons:
failed += 1
print(
f"{status} {diff_path} files={len(stats.files)} "
f"hunks={stats.hunks} +{stats.added} -{stats.deleted}"
)
for reason in reasons:
print(f" - {reason}")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
Run it against the leftover directory before you point any new agent at the tree.
python3 score_diff_budget.py .diff-budget.json leftovers/paid-diffs
You now have a leftover report that does not depend on the paid review UI. Keep the FAIL lines because they are the patches that trained you to accept too much. Commit the report beside the diffs so the cutover plan cites evidence instead of memory.
Step 3: Recalibrate ceilings from leftovers, not from hope
Do not copy the paid agent's largest leftover patch and treat it as your new normal. Sort FAIL rows and look for a cluster of PASS sizes that still shipped useful work. Set max_files just above that cluster, then force oversized work to split across turns. If every leftover fails, your paid workflow was already a rewrite machine, and cutover will hurt until you split tasks.
python3 score_diff_budget.py .diff-budget.json leftovers/paid-diffs \
| awk '/^FAIL/ {fail++} /^PASS/ {pass++} END {print "pass=" pass, "fail=" fail}'
Write a one-page note that lists which leftover tasks must be split before cutover. That note belongs in the cutover plan, and it should not sit as optional documentation. Future you will otherwise reopen the paid seat just to finish the big refactor.
Step 4: Gate apply on the new loop
Wire the scorer in front of patch apply, even if apply is just git apply. A budget that only runs against leftovers will rot after the first quiet week. The snippet below is proposed glue you can drop in front of a local runner.
#!/usr/bin/env bash
set -euo pipefail
DIFF_FILE="${1:?usage: gate-apply.sh proposed.diff}"
python3 score_diff_budget.py .diff-budget.json "$DIFF_FILE"
git apply --check "$DIFF_FILE"
git apply "$DIFF_FILE"
If the new loop streams a patch, write it to a temp file and score it before git apply. Cancellation belongs here too: once the scorer fails, you should not keep streaming more hunks into the same turn. Split the original user task and start a fresh turn with a much smaller file list.
Where inexpensive inference actually helps
You will retune this budget several times while leftover paid tasks are replayed locally. That replay stays cheaper when model calls are not metered like another premium seat. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option for this kind of replay. You can rerun leftover tasks against the same scorer without renting another vendor review pane. Keep the budget file in git so the host you choose cannot silently widen the blast radius.
Free model access still does not replace path allowlists, automated tests, or human code owners. It only makes it less painful to iterate the gate while you leave the paid agent. Leave those ceilings in the repository even if you later change hosts again.
Leftovers after the seat is gone
Paid diffs still sitting in chat history are leftovers, even after you exported a few files. Formatter churn that hid inside a giant approve click is another leftover you should score. Lockfile rewrites, snapshot updates, and generated clients are leftovers that will burn budget silently. Any turn that touched more than one service boundary is a leftover that should become two tickets.
Build a short leftover table now and close each row on purpose during cutover. The table is part of the cutover plan, not a retrospective slide. Close rows with diffs on disk, not with a memory of what the paid pane displayed.
| Leftover | Risk if ignored | Close-out action |
|---|---|---|
| Unexported paid patches | You cannot score the old blast radius | Dump unified diffs before seat expiry |
| Mega-refactors in one turn | New loop copies the same habit | Split into path-scoped tasks |
| Lockfile and vendor edits | Reproducible builds drift | Add forbidden prefixes |
| Snapshot and minified files | Budget burned on noise | Add ignore globs |
| Missing apply gate | Free-stack patches land uncounted | Wrap git apply with the scorer |
Limitations and who should skip this
This proposed harness understands unified text diffs and very little else besides that format. Binary assets, submodule pointer moves, and rename detection will undercount or overcount depending on git flags. A formatter that rewrites an entire file will exhaust max_lines_added even when the semantic change is one function. You must decide whether format-on-apply runs before or after the budget, and you must write that down.
Teams that already enforce tiny pull requests through code owners may not need another ceiling. Security-sensitive repositories should not treat a raw line count as a real authorization check. Generated codebases with huge protobuf dumps need a different contract, because the useful unit is a schema file, not a hunk. If your agent only answers questions and never writes trees, you can skip this diary entirely.
Do not use a diff budget as proof that the new model is as good as the paid one. Size control is only a safety rail, and correctness still needs tests you already trust. If you already exported paid-agent diffs, score those files first and keep the same ceilings on the next host you try. Leave the paid seat only after every FAIL row has an owner and a split ticket. The leftover directory is finished when new turns cannot exceed the budget without a conscious edit to the JSON.
Top comments (0)