It is 09:10 on a Monday, and the person who owned the agent overnight is already sitting in another meeting. The incoming owner opens the dashboard, sees a queue depth of eleven, and cannot tell whether that is normal, stale, or the first hour of an incident. Twenty minutes later the two of them are rebuilding the night from chat history, and nobody has checked the model tier label since Friday afternoon.
That scene repeats on every team that runs a small agent job on capacity nobody paid for. Free tiers rarely come with an SLA, capacity can move without notice, and the box underneath your process may reboot while you sleep. When there is no vendor promise to lean on, the shift handoff becomes the only continuity mechanism your team actually controls.
This playbook turns that handoff into a one-page artifact plus a gate script you can paste into a wiki today. The reference setup calls models through MonkeyCode's free token access and runs the agent on its free server option, so each handoff has to carry two kinds of state: what the code is, and what the platform currently looks like. Treat the free grant, which the operator currently describes as roughly ten million tokens, and the free server option as terms that can change; read the console before you plan a month around either number.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Why handoffs break specifically on free capacity
On paid infrastructure you can often answer a 3 a.m. question by reading a vendor status page or an invoice line. On free capacity, the honest answer is frequently that nobody knows, because there is no support channel and no contractual state to query. The handoff therefore has to record observations rather than assumptions, and it has to fail loudly when an observation is missing.
A second failure mode is ownership drift. When the outgoing owner writes a paragraph of prose, the incoming owner reads it as context rather than as a checklist, and unverified claims slip through. Structured fields and a script that exits non-zero fix more of this than any amount of documentation discipline.
Step 1: Name three roles and keep them separate
Use exactly three roles, because two roles collapse into a conversation and four roles create a committee. The outgoing owner is accountable for recording the shift state before they leave, and they are the only person allowed to declare the shift closed. The incoming owner accepts the state by re-running the gate themselves, never by trusting the pasted card alone. The reviewer is a third person who reads the card once per week, looks for patterns across shifts, and owns any decision to change the checklist.
Write those three sentences into the wiki page above the card template. A role definition that lives only in someone's head will not survive the first vacation.
Step 2: Freeze the shift state in one JSON file
Keep the state file small enough that filling it takes under three minutes, because a handoff ritual that costs fifteen minutes gets skipped on the busy nights when it matters most. Commit it under .shift/ so the history of your shifts is reviewable like any other change.
{
"shift": "2026-10-26-night",
"owner_outgoing": "alex",
"owner_incoming": "priya",
"reviewer": "sam",
"model_tier_label": "<paste the exact label your console shows>",
"open_incidents": [],
"queue_depth": 0,
"notes": "none"
}
The model_tier_label field is deliberately a string you copy rather than a value the script guesses. Free tiers rotate what they serve, and a label that changes without a changelog entry is the cheapest early warning you will ever get. If an incident is open, add an object with an owner key, because an incident without an owner is a rumour.
Step 3: Fingerprint the parts you actually control
Hashing your prompt file and your agent config gives the incoming owner a cheap way to answer whether the running system is the one in the repository. Combined with the git commit, three short hashes catch most silent drift between shifts.
#!/usr/bin/env python3
'''handoff_gate.py - refuse a shift handoff that is missing evidence.
Usage:
python3 handoff_gate.py --state .shift/state.json
python3 handoff_gate.py --state .shift/state.json --emit-card
'''
import argparse
import datetime as dt
import hashlib
import json
import pathlib
import subprocess
import sys
REQUIRED_FIELDS = [
'shift', 'owner_outgoing', 'owner_incoming', 'reviewer',
'model_tier_label', 'open_incidents', 'queue_depth', 'notes',
]
WATCHED_FILES = ['prompts/agent.md', 'config/agent.json']
def short_hash(path):
return hashlib.sha256(pathlib.Path(path).read_bytes()).hexdigest()[:12]
def git_commit():
try:
out = subprocess.check_output(
['git', 'rev-parse', '--short', 'HEAD'],
text=True, stderr=subprocess.DEVNULL,
)
return out.strip()
except Exception:
return 'no-git'
def snapshot(state):
prints = {}
for name in WATCHED_FILES:
prints[name] = short_hash(name) if pathlib.Path(name).exists() else 'missing'
return {
'commit': git_commit(),
'fingerprints': prints,
'generated_at': dt.datetime.now(dt.timezone.utc).isoformat(timespec='seconds'),
}
def checks(state, snap):
missing = [k for k in REQUIRED_FIELDS if state.get(k) in (None, '', [])]
results = [('required fields present', not missing, ', '.join(missing) or 'ok')]
label = str(state.get('model_tier_label', ''))
results.append(('tier label is not a placeholder', not label.startswith('<'), label))
results.append((
'watched files hashed',
all(v != 'missing' for v in snap['fingerprints'].values()),
json.dumps(snap['fingerprints']),
))
results.append((
'outgoing differs from incoming',
state.get('owner_outgoing') != state.get('owner_incoming'),
'{} -> {}'.format(state.get('owner_outgoing'), state.get('owner_incoming')),
))
incidents = state.get('open_incidents') or []
owned = all(i.get('owner') for i in incidents)
results.append(('every open incident has an owner', owned, '{} open'.format(len(incidents))))
return results
def card(state, snap, results):
lines = [
'## Shift handoff - {}'.format(state.get('shift')),
'',
'- Outgoing: {}'.format(state.get('owner_outgoing')),
'- Incoming: {}'.format(state.get('owner_incoming')),
'- Reviewer: {}'.format(state.get('reviewer')),
'- Commit: `{}`'.format(snap['commit']),
'- Model tier label: {}'.format(state.get('model_tier_label')),
'- Queue depth at handoff: {}'.format(state.get('queue_depth')),
'',
'### File fingerprints',
'',
'| file | sha256 (first 12) |',
'| --- | --- |',
]
for name, digest in snap['fingerprints'].items():
lines.append('| `{}` | `{}` |'.format(name, digest))
lines += ['', '### Gate results', '']
for name, ok, detail in results:
lines.append('- {} - {} ({})'.format('PASS' if ok else 'FAIL', name, detail))
lines += ['', '### Notes', '', str(state.get('notes')), '']
return '\n'.join(lines)
def main():
ap = argparse.ArgumentParser()
ap.add_argument('--state', default='.shift/state.json')
ap.add_argument('--emit-card', action='store_true')
args = ap.parse_args()
state = json.loads(pathlib.Path(args.state).read_text())
snap = snapshot(state)
results = checks(state, snap)
for name, ok, detail in results:
status = 'PASS' if ok else 'FAIL'
print('{} {} [{}]'.format(status, name, detail))
if args.emit_card:
out = pathlib.Path('.shift/card.md')
out.write_text(card(state, snap, results))
print('\nwrote {}'.format(out))
failed = [n for n, ok, _ in results if not ok]
if failed:
print('\ngate blocked: {} check(s) failed'.format(len(failed)), file=sys.stderr)
return 1
print('\ngate passed: handoff may proceed')
return 0
if __name__ == '__main__':
raise SystemExit(main())
The script uses only the standard library and needs Python 3.9 or newer, so it runs on whatever free server you already have. Nothing in it contacts a model or a control plane, which is intentional: a gate that depends on the platform being up cannot be trusted during an outage.
Step 4: Run the gate before you write prose
The outgoing owner fills the state file first and runs the gate second, because the report should tell them what is missing rather than confirm what they remember writing. A blocked run looks like this.
$ python3 handoff_gate.py --state .shift/state.json
FAIL required fields present [notes]
PASS tier label is not a placeholder [free-tier-b]
PASS watched files hashed {"prompts/agent.md": "9f2c1ab4e7d0", "config/agent.json": "41be77c0a9d3"}
PASS outgoing differs from incoming alex -> priya
PASS every open incident has an owner [0 open]
gate blocked: 1 check(s) failed
Fix the missing field and re-run with --emit-card, which writes .shift/card.md in the exact shape your wiki page expects. The incoming owner then re-runs the same command against the same commit and compares the printed fingerprints with the card they were given. A mismatch is not an accusation, it is a signal that something changed between the paste and the acceptance.
Step 5: Paste one page and sign it in the wiki
Your wiki page should hold the three role definitions, the JSON template, and the generated card, in that order and nothing else. Keep the previous five cards collapsed below the current one, because patterns across a week are more useful than any single night. Signing the handoff means the incoming owner appends one line to the card with their name and the local commit they verified, and the reviewer checks that line exists before reading anything else.
Decision table for the three interruptions that actually happen
| Signal at handoff | Action | Do not |
|---|---|---|
| Free server unreachable | Record the outage time, keep the queue frozen, and hand off a written resume condition | Redeploy or rebuild the box mid-handoff |
| Token grant close to exhausted | Record the remaining balance verbatim and reduce batch size for the next shift | Estimate the balance from memory |
| Tier label changed overnight | Pin the old label in a branch, run one comparison job, and hand off the diff | Assume the new label behaves the same |
| Fingerprint changed with no changelog | Block the handoff until the diff is explained in the card | Merge and move on |
That table is the part reviewers edit most often, and it should stay short enough to scan in under a minute.
What this gate cannot tell you
The script proves that paperwork exists, not that the agent behaves correctly, and confusing those two outcomes is the main way this playbook gets misused. Prompt and config hashes say nothing about how a remote model answers today, so they detect your changes rather than the platform's. Free capacity can be withdrawn or reshaped at any time, which means a passing gate is evidence about your process, never a guarantee about tomorrow.
The script also assumes a repository layout with prompts/agent.md and config/agent.json, and it has not been run against yours, so expect to spend ten minutes adjusting paths. It has no opinion about logs, latency, or cost attribution, and it should not replace metrics you already collect.
Who should not use this
Skip this playbook if you need per-request billing attribution, work under a regulated data regime, or depend on a paid SLA that already provides an incident record. Skip it as a solo developer too, because a handoff card you write to yourself is overhead with no reader. It also fails quietly on teams where nobody reads the card, and in that case the fix is organizational rather than technical.
If you want to test the pattern before buying capacity, the free token grant and the free server option are a cheap way to run a two-person rotation for a week and see whether the card earns its place in your wiki. Start with the gate script, add the wiki page only after the first blocked handoff annoys someone enough to fix the state file.
MonkeyCode provides free models that can run this workflow.
Top comments (0)