Do not promote an agent change from a rehearsal until the evidence bundle is complete. A green log line is not that bundle. If any required field is missing, empty, or inconsistent, the gate fails closed and the change stays put.
You already know a fluent answer can still hide a bad diff. What you need is a record you can re-check without trusting the chat scroll. This checklist is that record. It does not grade model quality, and it does not deploy anything.
Why a rehearsal is not a release
A shared or free server is a convenient place to run the loop once. It is not your production host, and it is not a signed release.
You can learn which tools fired, which files changed, and which command exited non-zero. You cannot learn your latency budget, your data-handling rules, or your rollback path from that single run. Treat the rehearsal as an input to a gate, not as the gate itself.
The failure mode is ordinary. Someone pastes a tidy summary into the ticket and calls the work done. The summary omits the model identifier the API actually returned, the lockfile hash, or the secret-scan result. Next week nobody can replay the decision. Fail closed before that summary becomes the only artifact.
Fields the bundle must hold
Keep one JSON file per run. Name it after the run id, not after a branch nickname.
Required fields:
-
run_id: a unique string you generate before the run starts -
started_atandfinished_at: UTC timestamps with a timezone -
rehearsal_envandtarget_env: labels you define, not marketing names -
model_id: the identifier string the API returned, copied verbatim -
prompt_sha256,lock_sha256,diff_sha256: 64 lowercase hex characters -
tool_calls: an integer count recorded by your harness -
commands: a non-empty list of{cmd, exit_code}actually executed -
secret_scan: exactlycleanorfindings -
reviewerandreviewed_at: empty until a person signs -
action:holdorpromote
Do not add a field called looks_good. A boolean without a source file is how checklists rot. If you need a comment, put it in the ticket, not in a field the gate ignores.
Eight gates that fail closed
Walk these in order. Stop at the first failure. Do not collect a warning and continue.
-
Presence. Every required key exists and is non-empty, except reviewer fields while
actionishold. -
Time order.
finished_atis strictly afterstarted_at. Equal timestamps fail. A zero-duration run is not evidence. -
Hash shape. Each
*_sha256matches 64 lowercase hex characters. Uppercase, prefixes, and truncated digests fail. -
Model pin.
model_idis the raw API string. A nickname in your notes does not fill an empty field. -
Environment split.
rehearsal_envmust differ fromtarget_envunlesswaiver_reasonis present and a reviewer has signed. A rehearsal labeledprodfails. -
Command evidence.
commandsis a non-empty list, and every exit code is an integer. A missing test command is a failure, not a skip. -
Secret scan. Only
cleanmay proceed.findings,skipped, or any other string fails closed. Do not encode an exception in this file. -
Human promote.
action=promoterequiresreviewerandreviewed_atat or afterfinished_at. The script never flips the action for you.
Automation can refuse. It should not approve. If you want a ninth gate later, add it as another hard failure, not as a score.
Fill the bundle before you argue about the diff
You fill fields from artifacts, in this order.
- Write
run_idandstarted_atbefore the first tool call. If you reconstruct them afterward, say that in the ticket and expect the times to be distrusted. - Save the prompt and the lockfile you actually used. Hash those files. Store the hex digest only.
- Copy
model_idfrom the response your client already logs. If the client does not log it, fix the client. Do not guess a name. - Record every command the agent or you ran, with the exit code the shell returned.
- Run the secret scanner your team already trusts. Store
cleanonly when that tool reported no findings. This checklist does not include a scanner. - Hash the diff. Set
actiontohold. - Ask a reviewer to read the diff. Only that person may set
promote,reviewer, andreviewed_at.
Three of those steps are clerical. They are still the ones that get skipped when the summary sounds confident. The script exists so a skipped step cannot hide inside a cheerful paragraph.
A validator you can copy
The script below is a proposal. It was not executed against a live host for this article, and it does not call any model API. It checks shape and a few inequalities. Drop it in a repo and point it at a bundle you produced.
#!/usr/bin/env python3
"""Fail closed unless a rehearsal bundle is complete. Proposal, not a benchmark."""
import json, re, sys
from datetime import datetime
HEX = re.compile(r"^[0-9a-f]{64}$")
REQUIRED = [
"run_id", "started_at", "finished_at", "rehearsal_env", "target_env",
"model_id", "prompt_sha256", "lock_sha256", "diff_sha256",
"tool_calls", "commands", "secret_scan", "action",
]
def parse_time(value):
return datetime.fromisoformat(str(value).replace("Z", "+00:00"))
def fail(msg):
print(f"FAIL: {msg}")
sys.exit(1)
def main(path):
with open(path, encoding="utf-8") as fh:
bundle = json.load(fh)
for key in REQUIRED:
if key not in bundle or bundle[key] in ("", None, []):
fail(f"missing {key}")
start, end = parse_time(bundle["started_at"]), parse_time(bundle["finished_at"])
if end <= start:
fail("finished_at must be after started_at")
for key in ("prompt_sha256", "lock_sha256", "diff_sha256"):
if not HEX.match(str(bundle[key])):
fail(f"{key} is not 64 lowercase hex chars")
if bundle["rehearsal_env"] == "prod":
fail("refusing a rehearsal labeled prod")
if bundle["rehearsal_env"] == bundle["target_env"] and not bundle.get("waiver_reason"):
fail("rehearsal_env equals target_env without waiver_reason")
if not isinstance(bundle["tool_calls"], int) or bundle["tool_calls"] < 0:
fail("tool_calls must be a non-negative int")
if not isinstance(bundle["commands"], list) or not bundle["commands"]:
fail("commands empty")
for i, step in enumerate(bundle["commands"]):
if "cmd" not in step or not isinstance(step.get("exit_code"), int):
fail(f"commands[{i}] needs cmd and integer exit_code")
if bundle["secret_scan"] != "clean":
fail("secret_scan must be clean")
if bundle["action"] not in ("hold", "promote"):
fail("action must be hold or promote")
if bundle["action"] == "promote":
if not bundle.get("reviewer") or not bundle.get("reviewed_at"):
fail("promote requires reviewer and reviewed_at")
if parse_time(bundle["reviewed_at"]) < end:
fail("reviewed_at is earlier than finished_at")
print("PASS: complete enough to review, not proof of production fitness")
sys.exit(0)
if __name__ == "__main__":
if len(sys.argv) != 2:
fail("usage: check_bundle.py <bundle.json>")
main(sys.argv[1])
A hold bundle, with fake hashes
Replace every digest with real sha256sum output from your tree. The hex below is only there so the shape checker has something to parse. It is not a measurement, and it is not your run.
{
"run_id": "2026-10-12T09-14-02Z-west",
"started_at": "2026-10-12T09:14:02Z",
"finished_at": "2026-10-12T09:21:40Z",
"rehearsal_env": "free-server-rehearsal",
"target_env": "staging-app",
"model_id": "<paste the id your API returned>",
"prompt_sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"lock_sha256": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
"diff_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"tool_calls": 4,
"commands": [
{"cmd": "python -m unittest discover -s tests", "exit_code": 0}
],
"secret_scan": "clean",
"action": "hold"
}
A bundle that must fail
Save this as bad.json when you want to see the gate refuse a promote with no reviewer. You should get a non-zero exit. If your wrapper turns that into success, fix the wrapper.
{
"run_id": "missing-reviewer",
"started_at": "2026-10-12T09:14:02Z",
"finished_at": "2026-10-12T09:21:40Z",
"rehearsal_env": "free-server-rehearsal",
"target_env": "staging-app",
"model_id": "<paste the id your API returned>",
"prompt_sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"lock_sha256": "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
"diff_sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"tool_calls": 1,
"commands": [{"cmd": "true", "exit_code": 0}],
"secret_scan": "clean",
"action": "promote"
}
Commands
Hash the inputs you actually sent, then run the gate. Keep the exit code.
sha256sum prompt.txt > prompt.sha
sha256sum requirements.lock > lock.sha
git diff --binary > candidate.diff
sha256sum candidate.diff > diff.sha
python3 check_bundle.py rehearsal.json
echo "gate_exit=$?"
python3 check_bundle.py bad.json
echo "bad_exit=$?"
Exit 0 means the file is complete enough for a person to read. Exit 1 means stop. Do not catch that code and continue the pipeline.
Compare the stored diff digest with the file on disk before you discuss the patch:
sha256sum candidate.diff
python3 -c 'import json; print(json.load(open("rehearsal.json"))["diff_sha256"])'
If the two digests differ, fix the bundle. Do not edit the digest so the story matches.
Decision table
| What you see | Gate result | What you do |
|---|---|---|
| Any required field missing | Fail closed | Hold. Fill the field from logs, not from memory. |
secret_scan is not clean
|
Fail closed | Stop. Follow your leak process. Do not promote. |
| Rehearsal env equals target env | Fail closed | Split the labels, or add a reviewed waiver. |
Rehearsal env is prod
|
Fail closed | Relabel. A production host is not a rehearsal. |
Commands recorded, action hold, file check passes |
File check only | A human may still reject the diff. |
action=promote without reviewer |
Fail closed | The script will not invent approval. |
| Bundle passes and a reviewer signed | Still not prod proof | Use your normal change window. This file is evidence, not a deploy. |
Where a free model and a free server fit
You can rehearse a non-production loop where a free model and a free server are available, then freeze the bundle before anyone talks about promotion. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option matter here only as a possible place to run that rehearsal. This article does not state quotas, hardware, duration, model names, uptime, or that the free option will remain as it is. Read the current product terms before you depend on them.
Map rehearsal_env to the label you use for that server. Map model_id from the response you received, not from a nickname in a status page. If the client cannot show an identifier you can store, do not invent one. Hold.
Use one small task that contains no customer data. Run it, write the bundle by hand the first time, and only then decide whether the checker belongs in CI. If the data does not belong on that server, do not send it there.
Limitations
This checker looks at shape and a few inequalities. It does not execute your tests, scan for secrets, or judge whether the diff is correct.
A passing exit code can sit next to a wrong fix. Hash fields can be honest and still describe a bad prompt. Timestamps lie if the host clock is skewed. You still need log retention, access control, and a rollback you have practiced on the target environment.
No latency numbers, cost figures, or accuracy scores are claimed here. Placeholder hashes are not results. A free rehearsal that passes this file check is still not a capacity plan, a security review, or a production certification.
Who should not use this
Skip the approach if you need attested hardware, a private network, or a data boundary the rehearsal server does not provide. Skip it if the run would include secrets, production credentials, or personal data. Skip it if your release process already requires a signed artifact this JSON would only duplicate. Skip it if you want a script to approve production. This one refuses that job.
If the bundle is incomplete, you hold. That is the whole policy.
Top comments (0)