Weekend side projects stall after the first working demo. The coding agent keeps adding login, a second store, and a deploy path that nobody asked for. A ship receipt stops that loop: it names the cut, runs the demo, and treats skipped work as a failing check.
This article is a labeled weekend build log, not a production case study. The receipt below is a proposal with runnable stdlib Python. No live traffic, customer, or benchmark is claimed.
What the log is for
A side project on a Saturday has three honest outputs. The cut is the file list that may change. The demo is one command with one expected substring. The skip list is work that must stay unbuilt until a later weekend.
Agents expand scope because the prompt is still open. A receipt closes it. The next session reads the same file and fails if a skipped path or phrase appears.
This is not a merge gate and not a token budget. It is a ship log. The demo can be ugly. The skip list cannot be empty.
The failure after the demo works
A typical Saturday board looks like this:
- 11:00 — a CLI prints three status rows from a JSON file.
- 13:00 — the agent adds SQLite "so it can grow."
- 16:00 — Docker files appear for a service that still has no users.
- 19:00 — the original demo command no longer runs without a compose stack.
The useful artifact was the 11:00 CLI. Everything after it was unlogged scope. The receipt records the 11:00 cut and makes later files a red check, not a surprise git status.
One JSON file, three fields
Keep the receipt next to the demo. JSON avoids extra parsers. The example project is a local status board: board.json plus board.py. Auth, deploy, and billing are skipped on purpose.
{
"project": "status-board",
"weekend": "2026-09-06",
"cut": [
"board.py",
"board.json",
"weekend_receipt.json",
"check_receipt.py"
],
"demo": {
"command": ["python", "board.py"],
"expect_substring": "OPEN"
},
"skip": {
"paths": [
"auth.py",
"deploy/",
"docker-compose.yml",
"schema.sql"
],
"phrases": [
"JWT",
"postgres",
"stripe",
"kubernetes"
],
"notes": [
"No login. The demo is a local JSON file.",
"No deploy. A terminal screenshot is the ship artifact.",
"No schema. Persistence stays a single file."
]
}
}
The cut list is allow, not hope. Files outside it fail the receipt. Skip paths fail if they exist. Skip phrases fail if they appear in any cut file. Notes are for humans; the checker prints them and does not parse them.
A demo small enough to freeze
Label: example only. The board is three rows. That is the entire product for the weekend.
board.json:
{
"title": "lab-board",
"items": [
{"id": "demo", "state": "OPEN", "note": "CLI renders JSON"},
{"id": "tests", "state": "OPEN", "note": "receipt checker"},
{"id": "auth", "state": "SKIP", "note": "next weekend, maybe"}
]
}
board.py:
#!/usr/bin/env python3
"""Render a local status board. Proposal demo, not a product."""
from __future__ import annotations
import json
from pathlib import Path
ROOT = Path(__file__).resolve().parent
BOARD = ROOT / "board.json"
def main() -> None:
data = json.loads(BOARD.read_text(encoding="utf-8"))
print(data.get("title", "board"))
for item in data.get("items", []):
state = str(item.get("state", "?")).upper()
note = item.get("note", "")
print(f"{item.get('id')}: {state} {note}")
if __name__ == "__main__":
main()
Run once before any agent session:
python board.py
Expected lines include OPEN. That substring is the demo contract. If the agent later requires a database URL to print the same three rows, the demo has already been lost.
The checker that prints the log
check_receipt.py uses the standard library only. It loads the receipt, runs the demo, lists files, and scans phrases. Exit code 0 is a ship. Any other code is an unlogged expansion.
#!/usr/bin/env python3
"""Weekend ship receipt: cut, demo, skip. Stdlib only. Labeled example."""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent
RECEIPT = ROOT / "weekend_receipt.json"
def fail(msg: str) -> None:
print(f"RECEIPT FAIL: {msg}")
raise SystemExit(1)
def load_receipt() -> dict:
if not RECEIPT.is_file():
fail("weekend_receipt.json missing")
return json.loads(RECEIPT.read_text(encoding="utf-8"))
def tracked_files() -> list[Path]:
files = [p for p in ROOT.rglob("*") if p.is_file()]
return [p.relative_to(ROOT) for p in files if ".git" not in p.parts]
def check_cut(cut: list[str], files: list[Path]) -> None:
allowed = set(cut)
extra = sorted({str(p).replace("\\", "/") for p in files} - allowed)
# nested skip dirs are handled in check_skip_paths; extra files still fail
extra = [e for e in extra if not e.startswith(".")]
if extra:
fail("files outside cut: " + ", ".join(extra))
def check_demo(demo: dict) -> None:
cmd = demo["command"]
expect = demo["expect_substring"]
proc = subprocess.run(cmd, cwd=ROOT, capture_output=True, text=True)
out = (proc.stdout or "") + (proc.stderr or "")
if proc.returncode != 0:
fail(f"demo exit {proc.returncode}: {out[-400:]}")
if expect not in out:
fail(f"demo missing {expect!r}")
print("RECEIPT DEMO: ok")
print(out.rstrip())
def check_skip_paths(paths: list[str]) -> None:
for rel in paths:
target = ROOT / rel
if target.exists():
fail(f"skipped path exists: {rel}")
def check_skip_phrases(phrases: list[str], cut: list[str]) -> None:
hits = []
for rel in cut:
path = ROOT / rel
if not path.is_file():
continue
text = path.read_text(encoding="utf-8", errors="replace")
for phrase in phrases:
if phrase in text:
hits.append(f"{rel}: {phrase}")
if hits:
fail("skipped phrases in cut files: " + "; ".join(hits))
def print_log(data: dict) -> None:
print(f"PROJECT: {data.get('project')}")
print(f"WEEKEND: {data.get('weekend')}")
print("CUT:")
for name in data["cut"]:
print(f" - {name}")
print("SKIP NOTES:")
for note in data.get("skip", {}).get("notes", []):
print(f" - {note}")
print("RECEIPT SHIP: yes")
def main() -> None:
data = load_receipt()
files = tracked_files()
skip = data.get("skip", {})
check_cut(data["cut"], files)
check_skip_paths(skip.get("paths", []))
check_skip_phrases(skip.get("phrases", []), data["cut"])
check_demo(data["demo"])
print_log(data)
if __name__ == "__main__":
main()
Wire it as a single command:
python check_receipt.py
A green run prints the cut, the demo output, and the skip notes. That block is the weekend build log. Paste it into the project README or a gist. The next agent prompt can start with "receipt must stay green" instead of a vague "keep it small."
Weekend sequence
Use the receipt before the model writes code, not after the tree is already wide.
- Write
board.json,board.py, andweekend_receipt.jsonby hand. Fifteen minutes is enough. - Run
python board.pyuntilOPENappears. - Run
python check_receipt.py. Fix extras until it ships. - Commit those four files. That commit is the cut.
- Open the coding session. Paste the skip notes. Ask only for changes inside
cut. - Re-run the checker after every agent batch. A new file is a failed receipt, not a bonus feature.
- Stop when the receipt is green. Do not "just add tests in a new package" on the same day.
The skip notes belong in the prompt. The skip paths belong in the checker. Prompts drift. Files on disk do not.
Where a free coding server fits
Some weekend loops stay on a laptop. Others move the editor session to a free coding server with free model access so the local machine remains a git remote and a screenshot host. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is optional infrastructure for that split: free model access and a free server path, used only after weekend_receipt.json already names the demo command. The checker does not import a vendor SDK. Readers who already have a green local receipt can try that environment for the next loop; the skip list still owns the scope.
Remote sessions make skip lists more valuable. An off-laptop agent cannot see the Saturday time box. It only sees an open repo. A failing receipt is the time box, encoded as paths and phrases.
What this log records, and what it does not
The receipt records:
- files that were allowed to change
- one command that still represents the product
- features that were refused, not forgotten
It does not record:
- model names or token counts
- deploy URLs
- performance numbers
- a claim that the board is used in production
If a later weekend adds auth, edit the receipt first. Move auth.py into cut, delete it from skip.paths, and change the demo command if login is now required. The log stays honest only when the JSON moves with the product.
Limitations
The checker is a path and substring tool. It will not catch a skipped feature implemented under a new name. login.py is not auth.py. Phrase scans are brittle: JWT inside a comment still fails, which is intended, but a rewritten token scheme will not.
rglob sees every file under the project root except .git. Virtualenvs, __pycache__, and editor swap files will fail the cut list. Add a ignore convention before using this on an existing tree, or keep the weekend repo empty except the four files.
The demo contract is a substring. OPEN in an error message would pass. Tighten the demo if that matters. The example does not parse JSON from the CLI; it only asks that the process exit 0 and print the marker.
There is no lock against rewriting weekend_receipt.json itself. An agent can empty the skip list. Protect that file in review, or keep it read-only in the session. The receipt is a social contract with a cheap automated check, not a security boundary.
Who should not use this
Skip this receipt when the weekend goal is exploration. Spike work needs extra files. A failing cut list will fight that.
Skip it on a shared production repo. The extra-file rule is hostile to normal tooling. Use a real CI policy there.
Skip it when the demo cannot be expressed as one local command. A mobile binary, a hardware flash, or a multi-service mesh needs a different ship definition.
Skip it when nobody will read the skip notes. An unused JSON file is not a log. The value is the Saturday stop, not the schema.
Close the weekend on a green receipt
The core conclusion stays the same at 19:00. The demo from 11:00 still runs. The skip list still names the work that did not ship. The agent session is over because the receipt is green, not because the prompt ran out of ideas.
Copy the four files into an empty folder, run python check_receipt.py, and keep the output with the commit. That is the build log. The next weekend starts by editing the JSON, not by pretending the unused Docker file was always in scope.
Top comments (0)