You sit down Saturday with one small feature.
The agent returns a plan that spans the repo.
You asked for a CLI flag, not a new framework.
Weekend side projects often rot inside oversized diffs.
Cheap generation never means a cheap review later.
You still pay for every extra file during review.
You need a gate that fails in the open.
A polite prompt will not hold the line.
A file budget can, because git still enforces it.
The actual weekend failure
Agents optimize for a complete-looking patch.
They add helpers, wrappers, and spare modules.
You wanted a demo you can run by Sunday.
This is not a model-quality problem first.
It is a scope problem you can measure.
Diff size is the measurement that does not lie.
Current agent write-ups keep hitting the same wall.
The model assumes missing architecture and extra packages.
Your repo then inherits that assumption as files.
You freeze three numbers before any generation starts.
Max files touched. Max lines added. Allowed path prefixes.
The agent may argue in chat. Git still rejects the branch.
Scope cut for this working demo
You will ship a local checker in one sitting.
You will not ship a bot platform this weekend.
The demo is a card, a script, and a hook.
Skip these on purpose before you open the chat:
- GitHub Actions and required status checks
- Language-specific AST parsing or formatters
- Auto-rewriting the agent patch in place
- Auth, billing, users, and hosted dashboards
- A second service just to store scores
If a step needs a new package, it is out.
If a step needs a new config file, it is out.
The working demo must run on a dirty git tree.
Artifact: freeze card plus a budget file
Create two files at the repo root before coding.
Commit both while the tree is still clean.
Treat them as product constraints, not comments.
1. Write the human card
DIFFBUDGET.md is for you and the agent.
Keep the goal to one testable sentence.
If you cannot run the done line, cut the goal.
# DIFFBUDGET.md
Goal: add `--json` to the existing `report` command.
Deadline: Sunday 18:00 local.
Max files touched: 5
Max lines added: 120
Max lines deleted: 40
Allowed prefixes:
- src/cli/
- src/report/
- tests/cli/
Forbidden:
- new packages
- new config files
- containers
- HTTP clients
Done means:
- `python -m src.cli report --json` prints valid JSON
- existing text output still works
- `python3 scripts/check_diff_budget.py` exits 0
2. Write the machine card
Markdown is easy to skim and easy to ignore.
Put the numbers in DIFFBUDGET.json as well.
The checker reads JSON only. Do not parse prose.
{
"base": "main",
"max_files": 5,
"max_lines_added": 120,
"max_lines_deleted": 40,
"allowed_prefixes": [
"src/cli/",
"src/report/",
"tests/cli/"
]
}
Change the prefixes to match your tree.
Keep max_files at five for a weekend CLI change.
Raise it only after a failed run, never before.
Artifact: the checker script
Save this as scripts/check_diff_budget.py.
It needs Python 3 and git. No extra packages.
Label this as a local gate you can copy and run.
#!/usr/bin/env python3
"""Fail if the current git diff exceeds DIFFBUDGET.json."""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
BUDGET_PATH = ROOT / "DIFFBUDGET.json"
def git(*args: str) -> str:
proc = subprocess.run(
["git", *args],
cwd=ROOT,
check=False,
text=True,
capture_output=True,
)
if proc.returncode != 0:
raise SystemExit(f"git {' '.join(args)} failed:\n{proc.stderr}")
return proc.stdout
def load_budget() -> dict:
if not BUDGET_PATH.exists():
raise SystemExit(f"missing {BUDGET_PATH}")
return json.loads(BUDGET_PATH.read_text(encoding="utf-8"))
def unique(seq: list[str]) -> list[str]:
seen: list[str] = []
for item in seq:
if item not in seen:
seen.append(item)
return seen
def changed_files(base: str) -> list[str]:
named = git("diff", "--name-only", "--diff-filter=ACMR", base)
tracked = [n for n in named.splitlines() if n]
extra = git("ls-files", "--others", "--exclude-standard")
untracked = [n for n in extra.splitlines() if n]
return unique(tracked + untracked)
def line_stats(base: str, untracked: list[str]) -> tuple[int, int]:
added = 0
deleted = 0
out = git("diff", "--numstat", base)
for line in out.splitlines():
parts = line.split("\t")
if len(parts) < 3:
continue
a, d = parts[0], parts[1]
if a == "-" or d == "-":
continue
added += int(a)
deleted += int(d)
for path in untracked:
file_path = ROOT / path
if not file_path.is_file():
continue
try:
text = file_path.read_text(encoding="utf-8")
except UnicodeDecodeError:
continue
added += len(text.splitlines())
return added, deleted
def main() -> int:
budget = load_budget()
base = budget.get("base", "main")
allowed = tuple(budget["allowed_prefixes"])
files = changed_files(base)
untracked = [
n for n in git("ls-files", "--others", "--exclude-standard").splitlines() if n
]
added, deleted = line_stats(base, untracked)
errors: list[str] = []
if len(files) > int(budget["max_files"]):
errors.append(f"files {len(files)} > {budget['max_files']}")
if added > int(budget["max_lines_added"]):
errors.append(f"added {added} > {budget['max_lines_added']}")
if deleted > int(budget["max_lines_deleted"]):
errors.append(f"deleted {deleted} > {budget['max_lines_deleted']}")
outside = [f for f in files if not f.startswith(allowed)]
if outside:
listing = "\n ".join(outside)
errors.append(f"paths outside allowed prefixes:\n {listing}")
print(f"base={base}")
print(f"files={len(files)}/{budget['max_files']}")
print(f"added={added}/{budget['max_lines_added']}")
print(f"deleted={deleted}/{budget['max_lines_deleted']}")
for path in files:
print(f" {path}")
if errors:
print("BUDGET FAIL")
for err in errors:
print(f"- {err}")
return 1
print("BUDGET OK")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Untracked files count toward the file cap.
Their lines count only when the file is text.
Binary untracked files are a documented gap, not magic.
Weekend workflow in five steps
1. Freeze the cards on a clean tree
git status --short
git rev-parse --abbrev-ref HEAD
Do not start the agent on a dirty branch.
You need a honest baseline against main.
Commit the two cards and the script first.
mkdir -p scripts
chmod +x scripts/check_diff_budget.py
git add DIFFBUDGET.md DIFFBUDGET.json scripts/check_diff_budget.py
git commit -m "Add weekend diff budget gate"
2. Run the gate on an empty diff
python3 scripts/check_diff_budget.py
You want BUDGET OK and zero files listed.
If this fails, your base ref is wrong.
Fix the base name before you generate code.
3. Constrain the agent with the card
Paste DIFFBUDGET.md at the top of the prompt.
Tell the agent the checker is the merge rule.
Tell it not to add prefixes. Tell it not to raise caps.
Ask for a file list before any patch.
If the list is already six files, cut scope now.
Do not let it "just add a util."
4. Run the gate before you read the patch
python3 scripts/check_diff_budget.py; echo exit:$?
git diff --stat main
Read the gate output first, not the model prose.
A red budget means you do not review yet.
Review time is the scarce weekend resource.
5. Cut scope when the gate fails
Do not raise max_files as the first move.
Delete the extra helper and rerun the script.
Keep the done line. Drop the extra design.
Typical cuts that save a Sunday:
- Drop a new config loader. Hard-code one flag.
- Drop a shared util package. Keep a local function.
- Drop extra output formats. Ship JSON only.
- Drop refactors in untouched modules. Leave them.
- Drop docs rewrites unless the done line needs them.
Decision table: raise the cap or cut the demo
Use this table when the gate fails.
Pick one row. Do not mix raises with new features.
| Signal | Action | Skip |
|---|---|---|
| Sixth file is a test next to the CLI | Keep the test. Cut a helper file. | Do not add tests/ as a free-for-all prefix. |
Sixth file is utils.py in a new folder |
Delete it. Inline ten lines. | Do not raise max_files. |
| Line cap fails on a lockfile | Put lockfiles in a ignore list later | Do not ignore src/ noise. |
| Agent needs a new HTTP client | Stop. That is a different weekend. | Do not expand prefixes. |
| Done line cannot run in five files | Shrink the done line. | Do not keep the old done line. |
The table is the product of this demo.
The script only makes the table unavoidable.
Without the fail, you will negotiate with the model.
Local hook, still not CI
You skipped GitHub Actions on purpose.
A local pre-commit hook is enough this weekend.
Install nothing hosted. Keep the hook in the repo.
# .git/hooks/pre-commit
#!/bin/sh
exec python3 scripts/check_diff_budget.py
chmod +x .git/hooks/pre-commit
This hook is local and easy to bypass.
That is acceptable for a solo weekend branch.
It is not acceptable as your only production control.
Reproducible test plan
Do not trust a green run from one happy path.
Walk these four cases on a throwaway branch.
Stop if any case disagrees with the table.
- Clean tree versus
mainmust printBUDGET OK. - Six tiny files under allowed prefixes must fail.
- One file under
docs/must fail on prefixes. - A 121-line add under
src/cli/must fail lines.
Commands for case 2 look like this:
mkdir -p src/cli
printf 'x\n' > src/cli/a.py
printf 'x\n' > src/cli/b.py
printf 'x\n' > src/cli/c.py
printf 'x\n' > src/cli/d.py
printf 'x\n' > src/cli/e.py
printf 'x\n' > src/cli/f.py
python3 scripts/check_diff_budget.py; echo exit:$?
You should see BUDGET FAIL and files 6 > 5.
Reset the throwaway files after the check.
Keep the test plan in DIFFBUDGET.md if you forget.
What you skipped, and why that is the demo
You skipped CI because the lesson is the freeze.
You skipped auto-fix because failed diffs teach faster.
You skipped a database because scores do not need storage.
The working demo is a red exit code on Saturday.
The working demo is a green exit code on Sunday.
Anything beyond that is next weekend, not this one.
Limitations
This gate does not read code quality.
A five-file patch can still be wrong.
You still have to run the done-line command.
Prefix checks are string prefixes, not owners.
src/cli/../secret is not a solved case here.
Do not treat this script as a security boundary.
git diff --numstat skips binary tracked files.
Untracked binary files also skip line counts.
Lockfiles can burn the line budget without warning.
The hook is local. Anyone can --no-verify.
Teams with real release trains need real CI later.
This article does not claim hosted runtime numbers.
Who should not use this
Do not use this as your only review on production patches.
Do not use this if you will ignore a red gate.
Do not use this to justify merging unread agent code.
Skip it when an incident needs a wide, careful diff.
Skip it when CODEOWNERS already block path drift.
Skip it when the weekend goal is learning a new stack.
Where a free model box fits
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The gate is just git plus a JSON file.
You can plan the five-file change with MonkeyCode free model access if you have no paid API key this weekend, and you can run the checker on its free server option if you want that process off your laptop. Keep DIFFBUDGET.json in your repo either way. If you need a free-model box for the planner, try that server and leave the budget file in git.
The article still works if you never do that.
The scarce resource remains your Sunday review time.
Five files is enough for a real demo. Stop there.
Top comments (0)