You open the shared board on Monday and find three tickets marked done by an overnight agent run. The comments look confident, the diffs are large, and nobody can say which model or machine produced them. A teammate almost merged one change because the summary read like careful human work.
Later that afternoon, staging failed because the run had used fixture data and a looser tool allowlist. You did not lack another model, another prompt, or another late-night retry. You lacked a named owner for the boundary between sandbox lanes and ship lanes.
This playbook gives you that owner, a handoff map, and a one-page wiki run. You can paste the SOP into an internal wiki and enforce it with a small check script. Treat the script as an example you adapt, not as a production control plane.
Why mixed lanes fail quietly
Shared agent work usually starts on a cheap or free path because tickets are messy and humans are tired. That is reasonable for exploration, reproduction, and first drafts that nobody should trust yet. Trouble starts when the same queue, labels, and bots treat sandbox output as ship-ready evidence.
You then get three failure modes that look like productivity from a distance. First, reviewers skim agent prose and skip the command that would prove the environment. Second, retry storms from the sandbox lane steal capacity from jobs that already passed a human gate. Third, fixture credentials and production secrets drift onto the same runtime because someone said it was only a draft.
A lane owner does not pick the smartest model for the team. The lane owner keeps two roads visible, named, and boring enough that midnight operators can follow them. If you cannot point at a person who may recategorize a run, you do not have a lane. You only have a hope that chat history will remember the difference.
Roles and handoffs
Name people, not chat threads. If a role is vacant, the SOP is not in force, and you should stop launching shared agent jobs.
- Lane Owner — decides sandbox versus ship for each agent job, and can recategorize a run after the fact.
- Ticket Driver — the human who asked for the work and must attach a receipt before asking for review.
- Runtime Steward — keeps sandbox machines, free-model endpoints, and ship CI from sharing disks, secrets, and queues.
- Reviewer of Record — a human who may reject "done" if the lane label is missing, stale, or contradicted by the receipt.
Handoff rule: sandbox output never assigns the Reviewer of Record by itself. The Ticket Driver promotes a run by adding evidence, not by pasting a longer summary into the ticket. If the Lane Owner is away, the backup owner must already be named on the wiki page, or the queue stays closed.
Write the names in the same meeting where you adopt the page. Do not wait for a perfect org chart, because mixed-lane failures happen on the first unsupervised night. A vacant backup is a hidden single point of failure, and you should treat it as an incident waiting for a quiet weekend.
Decide the lane before the first token
Use this decision table as a proposal. If two columns conflict, choose ship controls or do not run the job.
| Question | Sandbox lane | Ship lane |
|---|---|---|
| May the model or server change under you? | Yes | No, pin and record the stack |
| Are production secrets in scope? | No | Only through approved CI brokers |
| Is the output allowed to close a ticket? | No | Only after a human receipt |
| May tools write to main or prod? | No | Behind merge and deploy gates |
| Is retry-on-failure automatic? | Bounded, then stop | Bounded, then page a human |
| Can the job share a scratch disk with drafts? | Yes, if wiped | No |
You should print this table above the wiki SOP so operators do not invent a third lane at 1 a.m. A third lane is how fixture data, unpaid experiments, and merge candidates collapse into one comment thread. If a job needs one ship property and one sandbox property, split it into two jobs instead of bargaining.
One-page wiki SOP (paste this)
Copy the block below into your team wiki. Fill names on the same day you adopt it, then keep the page to a single screen.
# Lane SOP (sandbox vs ship)
Lane Owner: [name]
Runtime Steward: [name]
Backup Lane Owner: [name]
Reviewers of Record: [names]
## Before any agent job
1. Label the ticket `lane:sandbox` or `lane:ship`. No label means no run.
2. Sandbox jobs use the free-model / free-server path only.
3. Ship jobs use pinned CI, recorded tool allowlists, and no shared scratch disks.
4. Paste the lane, runtime, and exact command into the ticket before launch.
## During the run
1. If the job needs production data, stop and recategorize; do not "just this once."
2. If the sandbox path is saturated, queue or drop sandbox jobs. Never steal ship capacity.
3. If output will be used as a spec, test, or runbook, it has already left sandbox.
## After the run
1. Ticket Driver attaches a receipt: command, exit code, artifact path, and lane label.
2. Lane Owner may demote a ship run back to sandbox if the receipt is incomplete.
3. Reviewer of Record ignores unlabeled "done" comments, including confident ones.
## Promotion (sandbox -> ship)
Promotion is a new job, not a label edit. Re-run with ship controls, then review.
That page is the entire operating agreement for mixed agent work. If your wiki grows past one screen, you are adding folklore again, and folklore is how sandbox drafts become canon. Keep links to longer runbooks off this page, or people will stop reading the only section that matters during an incident.
What a receipt must contain
A receipt is a small text artifact, not a paragraph of model self-praise. You want a future teammate to replay the claim without opening a chat transcript. Store it next to the ticket identifier so the lane check can find it without crawling message history.
ticket=PAY-4418
lane=ship
runtime=ci-pinned
allowlist=tools.allow
cmd=pytest tests/test_refund_paths.py -q
exit=0
artifact=artifacts/PAY-4418.xml
closed_ticket=false
If closed_ticket is true on a sandbox receipt, the Lane Owner should demote the run in public on the ticket. Do not debate tone, because the failure is categorical. You are teaching the queue that labels beat eloquence, and you only get that lesson if demotion is visible.
A reproducible lane check you can run today
The following example is unexecuted here. You should run it in a throwaway directory and read the exit codes before wiring it to chatbots or webhooks.
Create lane_check.py:
#!/usr/bin/env python3
"""Example lane gate. Adapt labels to your tracker; do not treat this as policy."""
from __future__ import annotations
import json
import os
import sys
from pathlib import Path
ALLOWED_LANES = {"sandbox", "ship"}
SHIP_REQUIRED = ("LANE_RUNTIME", "LANE_ALLOWLIST", "LANE_RECEIPT_DIR")
def load_ticket(path: Path) -> dict:
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ValueError("ticket JSON must be an object")
return data
def main() -> int:
if len(sys.argv) != 2:
print("usage: lane_check.py TICKET.json", file=sys.stderr)
return 2
ticket = load_ticket(Path(sys.argv[1]))
lane = str(ticket.get("lane", "")).strip().lower()
if lane not in ALLOWED_LANES:
print("reject: missing or unknown lane", file=sys.stderr)
return 1
if lane == "sandbox" and ticket.get("closes_ticket"):
print("reject: sandbox output cannot close a ticket", file=sys.stderr)
return 1
if lane == "ship":
missing = [name for name in SHIP_REQUIRED if not os.environ.get(name)]
if missing:
print(f"reject: ship lane missing {missing}", file=sys.stderr)
return 1
receipt = Path(os.environ["LANE_RECEIPT_DIR"]) / f"{ticket.get('id', 'unknown')}.txt"
if not receipt.is_file():
print(f"reject: no receipt at {receipt}", file=sys.stderr)
return 1
print(f"ok: lane={lane} ticket={ticket.get('id')}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Keep a tiny ticket fixture so newcomers can fail the check on purpose:
{
"id": "PAY-4418",
"lane": "sandbox",
"closes_ticket": true
}
Commands you can run locally:
python3 lane_check.py ticket.json
echo $?
mkdir -p /tmp/lane-receipts
export LANE_RUNTIME=ci-pinned
export LANE_ALLOWLIST=tools.allow
export LANE_RECEIPT_DIR=/tmp/lane-receipts
printf 'cmd=pytest\nexit=0\nlane=ship\n' > /tmp/lane-receipts/PAY-4418.txt
python3 - <<'PY'
from pathlib import Path
Path("ticket-ship.json").write_text(
'{"id":"PAY-4418","lane":"ship","closes_ticket":true}\n',
encoding="utf-8",
)
PY
python3 lane_check.py ticket-ship.json
If the first command does not exit 1, your copy of the script is not enforcing the SOP. You should fix the gate before you teach people to trust it. A green check that ignores closes_ticket is worse than no check, because it launders sandbox work into the ship column.
Optional wrapper for humans who live in the shell:
#!/usr/bin/env bash
set -euo pipefail
ticket="${1:?ticket json}"
lane="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["lane"])' "$ticket")"
if [[ "$lane" == "sandbox" ]]; then
echo "sandbox: free-model path only, no production secrets, no ticket close"
fi
if [[ "$lane" == "ship" ]]; then
: "${LANE_RUNTIME:?}" "${LANE_ALLOWLIST:?}" "${LANE_RECEIPT_DIR:?}"
echo "ship: pinned runtime only"
fi
python3 lane_check.py "$ticket"
Wire this wrapper to your tracker only after five manual failures have been recorded on real tickets. Automation without those scars will encode the wrong social rule, and you will spend a month arguing with a bot instead of naming an owner.
Where a free model path belongs
You need a sandbox lane that is allowed to be slower, cheaper, and occasionally wrong. MonkeyCode's free model access and free server option can sit in that sandbox lane as a place for drafts, reproductions, and throwaway tool trials.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Do not point that path at production secrets, customer exports, or merge queues. The value is isolation, not a promise about models, quotas, uptime, or hardware. If the free path disappears tomorrow, the SOP should still name a sandbox lane and a ship lane, because the failure mode is mixed evidence, not missing tokens.
The Runtime Steward should treat the free server as disposable scratch, with a wipe rule after each job. That is the opposite of using it as quiet overflow for ship work when CI is slow. Overflow is how lanes collapse, and collapsed lanes recreate the Monday board you started with.
Limitations, and who should skip this
This SOP does not measure model quality, and it does not replace security review, license review, or incident command. The example script only reads a JSON file and a few environment variables. It cannot see your chat tools, your Kubernetes queue, or a model that changed its behavior overnight.
You should not adopt this if you are a solo hobbyist with one machine and no shared tickets. You should also not adopt it if your rules already forbid third-party model endpoints, or if you were hoping a free server would become silent production capacity. Mixed-lane failures are social failures. A wiki page without a named owner will not save you, and a named owner without receipts is only a title.
Skip the approach when the work is a legal hold, a production incident with customer data, or a change that needs a formal change-advisory record. Those jobs start in the ship lane or they do not start. Sandbox theater during an incident wastes the only people who can still read the system.
A one-week rollout that stays small
- Put names on the wiki page during the next standup, including a backup Lane Owner who can recategorize runs.
- Add
lane:sandboxandlane:shipto the tracker, and reject unlabeled agent comments for seven days. - Run
lane_check.pyon five real tickets; keep the failures in the ticket, not in a slide deck. - Only after those five receipts exist, connect a sandbox path such as a free-model server to
lane:sandbox.
If you already have a free-model sandbox, take one labeled ticket this week and force it through the receipt step before anyone calls the work done. That single boring pass is the whole lesson: the lane is real only when a missing file can stop a confident agent.
Top comments (0)