Weekend agent builds rarely die from a missing feature. They die from an unbounded loop that reopens every skipped path. A local step ledger with a hard maximum, an allowlisted tool set, and a closed skip column keeps the demo finite even when generation runs on another machine.
The rest of this note is a proposed weekend kit. It is not a production agent framework and it is not a benchmark.
Cut the loop, not the repository
A side project has two scopes. Feature scope is the list of files the demo may touch. Loop scope is the number of tool steps the agent may take before the weekend is over.
Feature scope is the usual cut. Loop scope is the one that actually burns the Saturday. An agent that can still inspect one more file will inspect twenty.
The ledger below treats loop scope as the primary budget. Eight steps is a default, not a law. The number is small on purpose. A demo that needs forty tool calls is not a weekend demo.
What "done" means this weekend
The working demo is a single command with a frozen expected line on stdout. No UI. No extra endpoints. No README rewrite.
Proposed demo contract:
- Command:
python demo.py - Expected stdout:
ok: greeter ready - Allowed files:
demo.py,tests/test_demo.py,loop_ledger.json - Forbidden: new dependencies, git push, refactors outside the allowlist
Anything else goes in the skip column with a reason code. The next prompt is not allowed to reopen a skip without a human edit to the ledger.
The ledger file
Keep the ledger in git. The model may append steps. The model may not raise max_steps, may not delete skips, and may not expand allowed_files.
{
"goal": "demo.py prints a frozen greeting and tests/test_demo.py asserts it",
"max_steps": 8,
"allowed_tools": ["read_file", "write_file", "run_cmd"],
"allowed_files": ["demo.py", "tests/test_demo.py", "loop_ledger.json"],
"allowed_cmds": ["python demo.py", "python -m pytest tests/test_demo.py -q"],
"steps": [],
"skips": [
{
"id": "S1",
"item": "HTTP server and /health route",
"reason": "out_of_demo"
},
{
"id": "S2",
"item": "config file and env loader",
"reason": "out_of_demo"
},
{
"id": "S3",
"item": "retry/backoff helper",
"reason": "reopens_loop"
}
],
"status": "open"
}
Reason codes stay short. out_of_demo means the item is real work for another weekend. reopens_loop means the item would invite more tool calls than the budget. needs_human means the agent must stop rather than guess.
A tiny runner
The runner is ordinary Python. It does not call a model. It only enforces the ledger so a coding agent, or a tired human, cannot spend the budget on "one more step."
# loop_ledger.py
# Proposed weekend fixture. Not a production orchestrator.
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
LEDGER = Path("loop_ledger.json")
CLOSED_REASONS = {"out_of_demo", "reopens_loop", "needs_human"}
def load() -> dict:
data = json.loads(LEDGER.read_text(encoding="utf-8"))
data.setdefault("steps", [])
data.setdefault("skips", [])
return data
def save(data: dict) -> None:
LEDGER.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
def assert_open(data: dict) -> None:
if data.get("status") == "closed":
raise SystemExit("ledger is closed; demo is frozen")
if len(data["steps"]) >= int(data["max_steps"]):
raise SystemExit("max_steps reached; record a skip or stop")
def record_step(tool: str, target: str, note: str) -> None:
data = load()
assert_open(data)
if tool not in data["allowed_tools"]:
raise SystemExit(f"tool not allowed: {tool}")
if tool in {"read_file", "write_file"} and target not in data["allowed_files"]:
raise SystemExit(f"file not allowed: {target}")
if tool == "run_cmd" and target not in data["allowed_cmds"]:
raise SystemExit(f"cmd not allowed: {target}")
data["steps"].append({"tool": tool, "target": target, "note": note})
save(data)
print(f"step {len(data['steps'])}/{data['max_steps']} {tool} {target}")
def add_skip(item: str, reason: str) -> None:
data = load()
if reason not in CLOSED_REASONS:
raise SystemExit(f"unknown skip reason: {reason}")
next_id = f"S{len(data['skips']) + 1}"
data["skips"].append({"id": next_id, "item": item, "reason": reason})
save(data)
print(f"skip {next_id} {reason}")
def close_if_demo_passes() -> None:
data = load()
result = subprocess.run(
[sys.executable, "-m", "pytest", "tests/test_demo.py", "-q"],
check=False,
)
if result.returncode != 0:
raise SystemExit("demo tests failed; ledger stays open")
data["status"] = "closed"
save(data)
print("demo passed; ledger closed")
def main(argv: list[str]) -> None:
if len(argv) < 2:
raise SystemExit("usage: record|skip|close ...")
cmd = argv[1]
if cmd == "record" and len(argv) == 5:
record_step(argv[2], argv[3], argv[4])
return
if cmd == "skip" and len(argv) == 4:
add_skip(argv[2], argv[3])
return
if cmd == "close":
close_if_demo_passes()
return
raise SystemExit("usage: record|skip|close ...")
if __name__ == "__main__":
main(sys.argv)
Commands stay boring on purpose:
python loop_ledger.py record write_file demo.py "print frozen greeting"
python loop_ledger.py record write_file tests/test_demo.py "assert stdout"
python loop_ledger.py record run_cmd "python -m pytest tests/test_demo.py -q" "prove demo"
python loop_ledger.py skip "add FastAPI /health" out_of_demo
python loop_ledger.py close
If a step is rejected, that is the product. The rejection is the scope cut.
The two files the demo may touch
# demo.py
def greeting() -> str:
return "ok: greeter ready"
if __name__ == "__main__":
print(greeting())
# tests/test_demo.py
import subprocess
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_demo_stdout_is_frozen():
proc = subprocess.run(
[sys.executable, str(ROOT / "demo.py")],
check=True,
capture_output=True,
text=True,
)
assert proc.stdout.strip() == "ok: greeter ready"
The test pins stdout. A later agent that "improves" the copy will fail closed. That failure is cheaper than arguing with a chat log on Sunday night.
A worked Saturday sequence
Start with an empty steps array and the three skips already filled. The first records are reads. They cost budget. Reading is not free in this kit, because unbounded read_file is how loops pretend to be research.
Proposed sequence:
-
record read_file demo.py— observe the file is missing. -
record write_file demo.py— addgreeting(). -
record write_file tests/test_demo.py— freeze stdout. -
record run_cmdfor pytest — fail because the print has a trailing period. -
record write_file demo.py— match the frozen line. -
record run_cmdfor pytest — pass. -
skip "extract a Greeter class" reopens_loop. -
close.
Two steps remain unused. Unused steps are not a dare. They are leftover budget. Closing the ledger spends them on nothing, which is the correct spend.
The runner cannot parse intent inside a file. A write to demo.py that quietly starts a web server would still record if the path is allowlisted. The skip column and the frozen stdout test are the backstop, and they are incomplete on purpose. Honesty about that gap belongs in the weekend log.
Decision table for skips
| Candidate change | Ledger action | Reason code |
|---|---|---|
| Extra CLI flag that the demo command never uses | skip | out_of_demo |
| New dependency to make the greeting nicer | skip | reopens_loop |
Refactor into a package with __init__
|
skip | reopens_loop |
| Ambiguous product copy | skip | needs_human |
| Fix the frozen greeting so the test passes | record_step |
in budget |
| Add a test that asserts the same line | record_step |
in budget |
The table is the weekend policy. The agent does not get a parallel policy in a system prompt that contradicts it.
Prompt text that defers to the ledger
Proposed system fragment. Unexecuted. Paste it next to the repo. Do not treat it as an evaluated prompt.
You may only change files in allowed_files.
Before any extra feature, add a skip with reason out_of_demo or reopens_loop.
If max_steps is reached, stop. Do not ask to raise it.
Do not implement any item already in skips.
Four lines. The ledger is the source of truth. The prompt is a pointer.
Where generation can run
The ledger is local. The generator does not have to be.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
A laptop that is already compiling tests does not also need to host a long coding session. MonkeyCode's free model access and free server option can run the generator while loop_ledger.json stays in the repo and keeps rejecting out-of-scope tool calls. The interesting part is still the ledger. Remove the remote session and the same budget still applies.
If that remote option is already in reach, point the agent at the two allowed files and the commands above. Do not raise max_steps because the session is free. A free loop with no cap is still an unbounded loop.
What this weekend skips on purpose
- No streaming UI for token traces.
- No multi-agent debate over the greeting.
- No prompt store, vector index, or memory layer.
- No automatic reopen of S1–S3 when tests pass early.
Those skips are the build. A closed ledger with three skips and a green demo is a finished weekend. An open ledger with an almost-working HTTP stack is not.
Limitations
The kit does not measure model quality. It does not stop a determined editor from changing max_steps by hand. It does not replace code review, secret scanning, or a real merge gate.
JSON is not a workflow engine. Concurrent agents will clobber the ledger. There is no file locking in the script above.
Stdout matching is brittle on purpose. It will reject useful changes that alter the greeting. That is the point for a weekend freeze. It is the wrong tool for a product that must evolve copy every day.
The allowlist is path-based, not AST-based. Scope still depends on a human who will not "just" expand allowed_files at 11 p.m.
Who should not use this
- Teams shipping production agents with real users or compliance needs.
- Exploratory research whose goal is to watch the model wander.
- Anyone who needs the agent to add dependencies or touch infrastructure.
- Builders who will not keep the skip column honest.
A finite loop is a constraint. Constraints only help when the demo is small enough to finish.
The next weekend can open a new ledger. It should not reopen this one.
Top comments (0)