AI coding assistants invent files, APIs, and environment variables when a repository is incomplete. A weekend-sized countermeasure is enough. This recipe keeps a JSON assumption ledger, turns that ledger into a constrained prompt, and checks the resulting patch against paths that actually exist. The loop is local, boring, and small enough to finish in one sitting.
The goal is not an agent platform. The goal is a working demo that refuses invented architecture before a human wastes a Saturday merging it.
The failure this project is built to catch
Agentic coding tools fill missing context. They propose modules that are not in the tree. They call helpers that were never merged. They assume a .env key because a README mentioned it once.
That pattern shows up in brownfield apps more than in greenfield toys. A model that cannot see package.json, a private SDK, or a migration folder will still emit a confident patch. The patch compiles in the model's head. It does not compile in the checkout.
A ledger does not make the model smarter. It makes the model's guesses visible, countable, and testable.
Weekend scope: what ships, what is skipped
This is a labeled recipe, not a production postmortem. No personal benchmark, customer, or production incident is claimed. The working demo is three files plus a sample repository snapshot.
In scope
- A JSON schema for assumptions the assistant is allowed to make.
- A prompt assembler that injects those assumptions and forbids the rest.
- A checker that flags patch hunks touching missing paths or undeclared symbols.
- A 90-minute command-line walkthrough a reader can run on a laptop.
Explicitly skipped
- Multi-agent orchestration, MCP servers, and tool-calling loops.
- A web UI, a database, and authentication.
- Token accounting, secret redaction, and merge gates already covered in other write-ups.
- Live evaluation against a named model or a claimed latency number.
Skipping those items is the point. A weekend build that tries to become a platform usually ships nothing.
Artifact 1: the assumption ledger
Store the ledger next to the repo, not in chat history. Chat history evaporates. A file can be grepped, reviewed, and reset.
{
"task": "Add a dry-run flag to the invoice export CLI",
"repo_root": ".",
"allowed_paths": [
"src/cli.py",
"src/export/invoices.py",
"tests/test_export_invoices.py"
],
"forbidden_paths": [
"src/legacy/**",
"migrations/**"
],
"known_symbols": [
"export_invoices",
"InvoiceRow",
"parse_args"
],
"assumptions": [
{
"id": "A1",
"claim": "InvoiceRow already has a `status` field",
"evidence": "src/export/invoices.py:41",
"status": "verified"
},
{
"id": "A2",
"claim": "CLI uses argparse, not click",
"evidence": "src/cli.py:1-20",
"status": "verified"
},
{
"id": "A3",
"claim": "Dry-run should skip S3 upload",
"evidence": "none-in-repo",
"status": "unverified"
}
],
"hard_rules": [
"Do not create files outside allowed_paths",
"Do not invent symbols absent from known_symbols unless marked new",
"Every unverified assumption must be listed before code"
]
}
Unverified rows are first-class. They are not shame. They are the only honest output when the tree does not support a product request.
A later pass can flip A3 to rejected if S3 upload lives in a worker the CLI does not own. The model then has to propose a smaller patch or stop.
Artifact 2: assemble a constrained prompt
The assembler is a short Python script. It reads the ledger and prints a prompt block. The assistant never sees the whole monorepo. It sees the task, the allowed paths, and the open assumptions.
# recipe: assemble_prompt.py — unexecuted example for a weekend checkout
from __future__ import annotations
import json
from pathlib import Path
LEDGER = Path("assumption_ledger.json")
def load_ledger() -> dict:
return json.loads(LEDGER.read_text(encoding="utf-8"))
def render(ledger: dict) -> str:
unverified = [a for a in ledger["assumptions"] if a["status"] != "verified"]
lines = [
"You are editing an existing repository.",
f"Task: {ledger['task']}",
"Allowed paths:",
*[f"- {p}" for p in ledger["allowed_paths"]],
"Forbidden paths:",
*[f"- {p}" for p in ledger["forbidden_paths"]],
"Known symbols:",
*[f"- {s}" for s in ledger["known_symbols"]],
"Hard rules:",
*[f"- {r}" for r in ledger["hard_rules"]],
"Unverified assumptions (do not treat as facts):",
]
if not unverified:
lines.append("- none")
else:
for item in unverified:
lines.append(f"- {item['id']}: {item['claim']} ({item['evidence']})")
lines.append("Reply with: (1) assumption updates (2) a unified diff limited to allowed_paths.")
return "\n".join(lines)
if __name__ == "__main__":
print(render(load_ledger()))
Run it from the repo root:
python3 assemble_prompt.py > /tmp/constrained_prompt.txt
wc -l /tmp/constrained_prompt.txt
The prompt is intentionally small. A weekend project that dumps the entire tree into context recreates the original problem: the model invents structure because the signal is noisy.
Artifact 3: check the patch against the real tree
After the assistant returns a unified diff, do not read it like prose. Parse it. Compare every +++ path to allowed_paths. Compare added identifiers to known_symbols plus an explicit new: list if the ledger grows that field later.
# recipe: check_patch.py — unexecuted example
from __future__ import annotations
import json
import re
from pathlib import Path
LEDGER = Path("assumption_ledger.json")
DIFF = Path("/tmp/proposed.patch")
PLUS_FILE = re.compile(r"^\+\+\+ b/(.+)$")
ADD_DEF = re.compile(r"^\+\s*def\s+([A-Za-z_][A-Za-z0-9_]*)")
def failures(ledger: dict, diff_text: str) -> list[str]:
allowed = set(ledger["allowed_paths"])
known = set(ledger["known_symbols"])
problems: list[str] = []
for line in diff_text.splitlines():
m = PLUS_FILE.match(line)
if m:
path = m.group(1)
if path != "/dev/null" and path not in allowed:
problems.append(f"path-not-allowed: {path}")
d = ADD_DEF.match(line)
if d and d.group(1) not in known:
problems.append(f"new-symbol-undeclared: {d.group(1)}")
return problems
if __name__ == "__main__":
ledger = json.loads(LEDGER.read_text(encoding="utf-8"))
diff_text = DIFF.read_text(encoding="utf-8")
found = failures(ledger, diff_text)
if not found:
print("CHECK_OK")
else:
print("CHECK_FAIL")
for item in found:
print(item)
raise SystemExit(1)
The checker is crude on purpose. It does not understand Python scoping. It catches the expensive class of errors: brand-new files and brand-new functions that nobody agreed to.
A sample failing patch looks like this:
--- a/src/cli.py
+++ b/src/cli.py
@@ -10,0 +10,6 @@
+def upload_to_s3(path):
+ raise NotImplementedError
+
+++ b/src/export/s3_client.py
@@ -0,0 +1,8 @@
+def build_client():
+ return None
Expected checker output:
CHECK_FAIL
new-symbol-undeclared: upload_to_s3
path-not-allowed: src/export/s3_client.py
new-symbol-undeclared: build_client
That failure is the demo. The weekend is successful when the script says no.
A 90-minute working path
The following sequence is a recipe. Treat it as a checklist, not as a claim that a specific machine already ran it.
- Copy a small CLI repo or a single package directory into a throwaway folder.
- Hand-write
assumption_ledger.jsonwith at most eight allowed paths. - Mark every claim without a file:line evidence as
unverified. - Generate
/tmp/constrained_prompt.txtwithassemble_prompt.py. - Paste that prompt into an AI coding session on a laptop or a free coding server.
- Save the model output as
/tmp/proposed.patchwithout manual cleanup. - Run
python3 check_patch.pyand keep the first failure list. - Either shrink the task or promote an assumption to
verifiedwith evidence. - Repeat once. Stop after two loops even if the patch is imperfect.
Stopping is part of the scope cut. A third loop usually means the task is too large for the ledger.
Where a free coding server fits this loop
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The ledger and checker do not require a vendor. They run with files and Python 3. A coding assistant still has to propose the diff. MonkeyCode's free model access and free server option are relevant when the weekend machine should not install a local GPU stack and should not send the whole company monorepo to an unbounded cloud session.
Use the free server as the place the constrained prompt is pasted. Keep the ledger and checker on disk the human controls. The server proposes. The laptop verifies. That split is the architecture.
Do not treat free access as infinite access. Do not treat a free server as a compliance boundary. Quotas, model catalogs, hardware, and uptime are out of scope here because they change and because this recipe does not depend on a named SKU.
A natural next step, if a hosted coding workspace is useful, is to try the same ledger loop there rather than expanding the script.
Decision table for the second hour
| Symptom | Ledger change | Code change |
|---|---|---|
| Patch adds a new file | Keep allowed_paths closed |
Reject the patch |
| Patch needs a real new file | Add one path with a reason | Allow a second loop |
Model treats A3 as fact |
Leave status unverified
|
Require a comment, not code |
| Checker flags a helper that exists | Add the symbol with evidence | Re-run once |
| Task needs migrations | Out of weekend scope | Split into a later change |
The table prevents the usual Saturday drift: one more file, then a client, then a config loader, then a rewrite.
Limitations
The checker does not parse imports, types, or test coverage. A model can still mutate a allowed file into nonsense. Path allowlists do not stop a destructive edit inside src/cli.py.
Unified diffs that rename files, use a/ prefixes inconsistently, or include binary blobs will confuse the regex. Human inspection remains mandatory.
The ledger can freeze a wrong architecture if verified rows are lazy. Evidence must be a path and a line range, not a vibe. Unverified rows that linger for weeks become folklore.
This workflow also assumes a reader can read a diff. It is a poor fit for purely conversational, no-repo coding.
Who should not use this approach
Skip it when the repository is generated from scratch and invention is the product. Skip it when legal review requires a vendor DPA, audit logs, or an approved model list this recipe does not provide. Skip it when the team already has a mature CI policy bot that checks the same invariants with real parsers.
Solo beginners can still use the ledger as a thinking aid. They should not treat CHECK_OK as proof the patch is correct.
What a later weekend can add, and why it waited
A real AST pass, a test runner hook, and a new_symbols field are useful. They are also how this project would have missed the demo. The valuable output of the weekend is a failing checker and a prompt that names its guesses.
Ship the refusal first. Expand parsers after a human has watched the script catch one invented file. That is the whole build log: a smaller surface, a visible assumption list, and a hard stop when the model starts building a parallel repository in the patch.
Top comments (0)