DEV Community

Taylor Zhu
Taylor Zhu

Posted on

No Complete Bundle, No Promote: A Fail-Closed Rehearsal Checklist

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_at and finished_at: UTC timestamps with a timezone
  • rehearsal_env and target_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: exactly clean or findings
  • reviewer and reviewed_at: empty until a person signs
  • action: hold or promote

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.

  1. Presence. Every required key exists and is non-empty, except reviewer fields while action is hold.
  2. Time order. finished_at is strictly after started_at. Equal timestamps fail. A zero-duration run is not evidence.
  3. Hash shape. Each *_sha256 matches 64 lowercase hex characters. Uppercase, prefixes, and truncated digests fail.
  4. Model pin. model_id is the raw API string. A nickname in your notes does not fill an empty field.
  5. Environment split. rehearsal_env must differ from target_env unless waiver_reason is present and a reviewer has signed. A rehearsal labeled prod fails.
  6. Command evidence. commands is a non-empty list, and every exit code is an integer. A missing test command is a failure, not a skip.
  7. Secret scan. Only clean may proceed. findings, skipped, or any other string fails closed. Do not encode an exception in this file.
  8. Human promote. action=promote requires reviewer and reviewed_at at or after finished_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.

  1. Write run_id and started_at before the first tool call. If you reconstruct them afterward, say that in the ticket and expect the times to be distrusted.
  2. Save the prompt and the lockfile you actually used. Hash those files. Store the hex digest only.
  3. Copy model_id from the response your client already logs. If the client does not log it, fix the client. Do not guess a name.
  4. Record every command the agent or you ran, with the exit code the shell returned.
  5. Run the secret scanner your team already trusts. Store clean only when that tool reported no findings. This checklist does not include a scanner.
  6. Hash the diff. Set action to hold.
  7. Ask a reviewer to read the diff. Only that person may set promote, reviewer, and reviewed_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])
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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)