DEV Community

Charlie Hu
Charlie Hu

Posted on

Weekend Build Log: A Clock-Gated Cut List for a One-Weekend Release-Notes Bot

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:

  1. Only keep tasks count toward the hour budget. cut and fixture are free.
  2. A task is on the demo path or it is not. Demo-path tasks must be keep; nothing else gets a keep.
  3. fixture means "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

  • keep requires an estimated hour count. Unknown estimates are treated as 3h, which is a lie that hurts.
  • cut means the code path does not exist yet. Not disabled, not commented out — absent.
  • fixture requires a named file under fixtures/ 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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode
# 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())
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. Create cutlist.tsv with four columns plus est_h and demo_path, and fill it before writing any code.
  2. Run python checkpoint.py until it exits 0. Demote rows instead of extending the deadline.
  3. Write the pipeline with an injected transport and no network calls in the core module.
  4. Add smoke_demo.py with the SHA-leak and bullet-count assertions, then run it before every deploy.
  5. Deploy the webhook, then run the three-command preflight twice: once cold, once warm.
  6. Move every fixture row 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)