A weekend side project fails in a predictable way: not from bad code, but from a scope list that grows faster than the clock shrinks. The fix in this build log is mechanical. Every task carries one of three states — keep, cut, or fixture — and a small script refuses to let the kept work exceed the hours actually left before the demo. The demo path stays green because a smoke test runs the whole pipeline against an injected fake model reply, with no network involved.
This is a build log for a release-notes bot: it reads git log between two tags, asks a model to write one bullet per meaningful commit, and posts the result to a webhook. The interesting part is not the bot. It is the triage loop that made the bot demoable in two days.
Why a checklist loses to a clock
A feature checklist is a wish list. It has no opinion about time.
A clock gate has one job: compare planned hours against remaining hours and exit non-zero when the plan is a lie. That exit code is the whole value. It fires on Saturday afternoon, when the temptation to add "just one more" surface is strongest, and it fires again on Sunday when the same task list is suddenly 40% too big.
Three rules make it work:
- Only
keeptasks count toward the hour budget.cutandfixtureare free. - A task is on the demo path or it is not. Demo-path tasks must be
keep; nothing else gets akeep. -
fixturemeans "the demo shows this, but a static value stands in for the real implementation". It is a promise, not a shortcut — and it gets deleted or labeled before anyone else sees it.
The artifact: a cut list with three states
The ledger is a tab-separated file, cutlist.tsv, checked into the repo next to the code. It is boring on purpose.
| id | task | state | est_h | demo_path |
|---|---|---|---|---|
| T1 | git-log to prompt builder | keep | 1.5 | yes |
| T2 | model transport + one retry | keep | 1.0 | yes |
| T3 | webhook server with /healthz
|
keep | 1.0 | yes |
| T4 | markdown renderer for the changelog widget | keep | 1.5 | yes |
| T5 | history store in sqlite | cut | 2.0 | no |
| T6 | per-repo auth tokens | fixture | 2.5 | no |
| T7 | settings page | cut | 3.0 | no |
| T8 | Slack block formatting | cut | 1.5 | no |
The kept column totals five hours. The cut column totals nine. That ratio is the real output of Saturday morning, and it is the reason the demo exists at all.
State rules that survive contact with a real weekend
-
keeprequires an estimated hour count. Unknown estimates are treated as 3h, which is a lie that hurts. -
cutmeans the code path does not exist yet. Not disabled, not commented out — absent. -
fixturerequires a named file underfixtures/so the stand-in is visible in review.
checkpoint.py: the gate that refuses work
Standard library only. It reads the ledger, sums the kept hours, and compares against wall-clock time remaining before the rehearsal slot.
#!/usr/bin/env python3
"""checkpoint.py - refuse to keep more scope than the clock allows."""
import csv
import sys
from datetime import datetime
HARD_STOP = datetime.fromisoformat("2026-10-25T17:30:00") # rehearsal starts
NOW = datetime.now()
def load(path="cutlist.tsv"):
with open(path, newline="") as f:
return list(csv.DictReader(f, delimiter="\t"))
def hours_left(now=NOW, stop=HARD_STOP):
return max(0.0, (stop - now).total_seconds() / 3600)
def report(rows, now=NOW):
kept = [r for r in rows if r["state"] == "keep"]
planned = sum(float(r["est_h"]) for r in kept)
left = hours_left(now)
print(f"kept={len(kept)} planned_h={planned:.1f} left_h={left:.1f}")
for r in kept:
print(f" KEEP {r['id']:<4} {float(r['est_h']):>4.1f}h {r['task']}")
for r in rows:
if r["state"] != "keep":
print(f" {r['state'].upper():<7} {r['id']:<4} {float(r['est_h']):>4.1f}h {r['task']}")
return planned <= left
if __name__ == "__main__":
sys.exit(0 if report(load()) else 1)
Example output on Sunday morning, when the plan is already too big:
$ python checkpoint.py; echo "exit=$?"
kept=4 planned_h=5.0 left_h=3.2
KEEP T1 1.5h git-log to prompt builder
KEEP T2 1.0h model transport + one retry
KEEP T3 1.0h webhook server with /healthz
KEEP T4 1.5h markdown renderer for the changelog widget
CUT T5 2.0h history store in sqlite
exit=1
Exit code 1 is the useful signal. The response is not "work faster"; it is to demote one keep row to cut or fixture and re-run until the exit code is 0. In this log, T4 dropped to fixture — the demo renders plain bullets from a fixture file and the widget formatting stays out of scope.
The demo-path smoke test, with the model faked
A demo breaks when a hidden network call is slow, or when the output shape drifts. Both are testable without a network. The pipeline takes an injected transport, so the smoke test swaps in a deterministic string.
# notes.py
import re
from dataclasses import dataclass
NOISE = re.compile(r"^(chore|test|ci)(\(|:)")
@dataclass
class Notes:
bullets: list[str]
body: str
def filter_commits(commits):
return [c for c in commits if not NOISE.match(c["subject"])]
def build_prompt(commits):
lines = "\n".join(f"- {c['subject']}" for c in commits)
return "One markdown bullet per commit. No SHAs. Max 110 chars.\n" + lines
def draft_notes(commits, transport):
kept = filter_commits(commits)
raw = transport(build_prompt(kept)) # network or fixture, caller decides
bullets = [l.lstrip("- ").strip() for l in raw.splitlines() if l.strip()]
return Notes(bullets=bullets, body="\n".join(f"- {b}" for b in bullets))
# smoke_demo.py
import sys
from notes import draft_notes, filter_commits
COMMITS = [
{"sha": "a1b2c3d", "subject": "feat: add /webhook endpoint"},
{"sha": "b2c3d4e", "subject": "chore: bump deps"},
{"sha": "c3d4e5f", "subject": "fix: handle empty tag range"},
]
def fake_transport(prompt: str) -> str:
return "- add /webhook endpoint\n- handle empty tag range\n"
def main() -> int:
out = draft_notes(COMMITS, transport=fake_transport)
expected = len(filter_commits(COMMITS))
failures = []
if len(out.bullets) != expected:
failures.append(f"bullets={len(out.bullets)} expected={expected}")
if any("chore" in b for b in out.bullets):
failures.append("noise commit leaked into public notes")
if any(len(b) > 110 for b in out.bullets):
failures.append("bullet too long for the widget")
if "a1b2c3d" in out.body:
failures.append("raw sha leaked into public notes")
for f in failures:
print("FAIL", f)
print("PASS demo path" if not failures else "demo path broken")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())
The SHA leak check is the one that earned its place. Model-written notes happily inline a commit hash when the prompt does not forbid it, and a hash in a public changelog is a small but real information leak. The test now blocks it before rehearsal, not after.
Wiring the real transport once
The production path calls a model with the same signature. This is where the free model access matters: the transport can be pointed at a free endpoint during the weekend and swapped later without touching draft_notes. MonkeyCode's free model access and free server option are operator-supplied availability claims; no quota, model list, or latency figure is asserted here, and the scripts above do not depend on any of them.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
# transport_http.py - keep the shape, swap the endpoint
import json
import urllib.request
def http_transport(prompt: str, url: str, token: str, timeout: float = 20.0) -> str:
body = json.dumps({"prompt": prompt}).encode()
req = urllib.request.Request(
url, data=body,
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return json.loads(resp.read())["text"]
Two rules keep the injection honest. The transport is the only place that knows about the network, and the timeout is shorter than the shortest acceptable demo pause. If the call exceeds it, the pipeline falls back to the last fixture — visibly, with a banner in the output.
Preflight before the demo, on a free server
A free server option makes the deployment step short, but it does not make it instant. The preflight is three commands, run before rehearsal, not during it.
BASE="https://$HOST"
curl -sS -m 5 "$BASE/healthz" || echo "cold start - retry in 30s"
curl -sS -m 20 -X POST "$BASE/webhook" \
-H 'Content-Type: application/json' \
-d '{"range":"v0.1..v0.2"}' | head -c 400
python smoke_demo.py && echo "demo path green"
If /healthz times out and the second call succeeds, the service was cold and the fix is a warm-up ping two minutes before the demo, not a rewrite. That distinction is worth writing down once, because it saves twenty minutes of panic later.
What got skipped, and why that was the point
- History store (T5). Cut. Re-running the demo means re-posting the same range; a viewer cannot tell, and sqlite would have eaten two hours.
- Per-repo auth (T6). Fixture. A single static token outside the repo is enough for one demo, and the fixture is labeled in the README.
- Settings page (T7). Cut. Configuration lives in environment variables.
- Slack formatting (T8). Cut. Plain markdown bullets render fine in the widget used for the demo.
The skipped list is nine hours of work. None of it appears in the demo, and all of it appears in the "next" section of the README, which is the honest place for it.
Limitations and known failure modes
- Clock gates assume one developer on one machine. Timezone drift or a shared laptop silently moves the deadline; the stop time is hardcoded and should be an environment variable in any shared setup.
- Fixture mode can lie. A fixture that returns perfect output makes a broken pipeline look healthy. The smoke test catches shape drift, not prompt quality.
- Free tiers change. The preflight exists because availability and cold-start behavior are not contractual. Treat any free server as a demo venue, not an uptime guarantee.
- Model output is not stable. The bullet-count assertion will fail on a chatty reply; the retry in T2 exists for that, and the fixture fallback covers the rest.
- No cost or performance claims are made here. Nothing in this log was benchmarked, and the scripts do not measure tokens, latency percentiles, or throughput.
Who should not use this approach
Skip the clock gate if the project has an external deadline set by someone else, since the script would only restate pressure the team already feels. Skip fixture fallbacks entirely for anything user-facing with real data, where a stand-in value is a correctness bug rather than a demo choice. And skip this whole loop for production services with an SLA — a two-day build log is not a substitute for a release process.
Reproduce it in six steps
- Create
cutlist.tsvwith four columns plusest_handdemo_path, and fill it before writing any code. - Run
python checkpoint.pyuntil it exits 0. Demote rows instead of extending the deadline. - Write the pipeline with an injected
transportand no network calls in the core module. - Add
smoke_demo.pywith the SHA-leak and bullet-count assertions, then run it before every deploy. - Deploy the webhook, then run the three-command preflight twice: once cold, once warm.
- Move every
fixturerow into a "next" section of the README before anyone else reads the repo.
If the ledger gets reused on another weekend build, the free model access and free server option are enough to run that loop end to end without a billing decision getting in the way of the first commit.
Top comments (0)