Agent Builder · Field pack ab-001-coding-agent-handoff · v0.1.0 · private-review
A coding-agent handoff with acceptance checks and no implicit merge or deploy permission
Hand work to a coding agent as a brief pinned to a commit SHA, with re-runnable acceptance checks and a Permissions section that withholds merge, default-branch push and deploy in writing. Then enforce that on the server, because client-side deny rules are easy to sidestep.
- Verification
- Last tested 2026-11-15 (helper tools only). Review by 2027-02-15.
- Editor
- Gus; second review: pending
- Default mode
- dry-run
- Other formats
- Markdown · JSON
Explanation
The contract in one line
Done means you re-ran the checks and they passed, not that the agent said so. Landing is never part of the job.
Why write "not allowed" when it's obvious
To you it's obvious. To an agent finishing a task, "make the tests pass and wrap it up" can read like permission to merge. So we write the withholding down, every time, in the same words. Then we make the server enforce it. Client-side deny rules are useful, but the vendor's own docs show they don't match a command written another way (S2).
Verdict
Recommended for the brief and linter: we use the pattern daily, and the linter is tested. Works for us for the enforcement half, but we didn't re-test it against a live repo for this edition.
Fit
Goal: Delegate a bounded code change to an agent (or another bot) so that 'done' means the reviewer re-ran checks and they passed, and the agent could not have landed or shipped the change by itself.
Environment
- Git repository hosted on GitHub (enforcement section)
- Any coding agent that reads a Markdown brief
- GitHub protected branches as documented 2026-11
- Claude Code permission rules as documented 2026-11 (as one example of a client-side control)
Prerequisites
- You can create branch protection rules or rulesets on the repo
- The agent's credentials are not repository-admin credentials
Permissions required
- Repo admin, once, to set branch protection
Don't use this when
- Not a code-review checklist; it covers the contract around the change.
- Not for exploratory spikes where there is nothing to accept yet; write a research brief instead.
Expected output: A brief that passes tools/handoff_lint.py, a protected default branch the agent's identity can't bypass, and a report-back containing commands, exit codes and output tails the reviewer can re-run.
Procedure
Write the brief from the template, lint it, make sure the server enforces what the brief says, hand off, then accept only on your own re-run of the checks.
This pack describes a contract. It never grants a consuming agent permission to merge, push or deploy.
Pin the base read-only
Record the full commit SHA the work starts from.
git rev-parse HEADExpect: 40 hex characters. Branch names move; SHAs don't.
Write the brief read-only
Fill packs/assets/ab-001-handoff-template.md: Goal, Base, Scope in/out, Acceptance checks (command + expected result each), Permissions (explicit 'Not allowed' lines for merge, default-branch push, deploy), Stop conditions, Report back.
Expect: Every check is something you, not the agent, can re-run.
Lint it read-only
Run the linter and fix what it reports.
.venv/bin/python tools/handoff_lint.py brief.mdExpect: Exit 0, no output.
Check server-side enforcement read-only
Confirm the default branch requires a PR, required status checks and an approving review, and that 'Do not allow bypassing' is on or the agent's identity isn't an admin (S1).
Expect: The agent's identity could not merge or push the default branch even if it tried.
(Gated) Tighten protection if step 4 failed WRITE (gated)
Add or edit the branch protection rule / ruleset.
Expect: Re-run step 4; it passes.
Gate: Repository admin (the owner).
Isolate the workspace WRITE (gated)
Give the agent its own branch and linked worktree from the pinned SHA (S3).
git worktree add -b agent/<name> ../wt-<name> <sha>Expect: The agent works in a directory no one else is using.
Gate: Whoever owns the repo checkout; reversible with 'git worktree remove' after the work is saved.
Hand off read-only
Send the brief. Client-side deny rules (S2) are a seatbelt, not the lock; the lock is step 4.
Expect: Agent acknowledges Scope and Permissions.
Accept on your own re-run probe (no state change)
Check out the agent's final SHA and run every acceptance check yourself. Compare exit codes and output with the report.
Expect: All checks pass on your run. Disagreement with the agent's report is itself a finding.
(Gated) Land IRREVERSIBLE (gated)
Merge and, separately, deploy. Neither is part of the handoff.
Expect: Merge commit references the brief.
Gate: The owner or the designated lander, after step 8, through the protected-branch flow.
Verification
Success check: tools/handoff_lint.py exits 0 on the brief; the agent's identity is blocked by the server from merging or pushing the default branch; the reviewer's own re-run of every acceptance check matches the expected results before anything lands.
| If you see | It means | Next |
|---|---|---|
| Linter: 'Permissions must explicitly withhold: merge' (or push / deploy) | The brief is silent on landing, and silence gets read as permission. | Add the 'Not allowed' lines from the template. |
| Linter: 'Base must pin a full 40-character commit SHA' | Base is a branch name; the agent and reviewer may test different code. | Step 1. |
| Agent pushed to the default branch anyway | Server enforcement missing, or the agent ran with admin credentials that bypass protection by default (S1). | Revert via a PR, rotate to a non-admin token, enable 'Do not allow bypassing'. |
| Client deny rule didn't stop a push | Pattern rules don't match rewritten commands such as 'git -C . push' (S2). | Rely on server-side protection; treat client rules as advisory. |
| Agent reports green, your re-run is red | Different SHA, environment drift, or a flaky or non-deterministic check. | Compare SHAs first, then environments; don't land. |
Rollback: The handoff itself changes nothing. Branch protection edits can be reverted in repo settings. Remove the agent worktree with 'git worktree remove' only after its branch is pushed or bundled.
Last tested 2026-11-15 (helper tools only). Scope: Tested: the linter against the template and good/bad fixtures (tests/run_tests.py, all passing on 2026-11-15). Not tested in this edition: a live agent run, or branch protection on a live repository.
Evidence
Sources
- S1 About protected branches, GitHub Docs (primary). Fetched 2026-11-15. Supports: Required reviews and status checks before merging; by default protections don't apply to admins unless 'Do not allow bypassing' is set; protected branches on private repos need a paid plan.
- S2 Configure permissions (Claude Code), Anthropic (primary). Fetched 2026-11-15. Supports: Rules evaluate deny → ask → allow. A deny rule like Bash(git push *) does not match 'git -C . push' or the same program by path or inside sh -c.
- S3 git-worktree documentation, Git project (primary). Fetched 2026-11-15; source shows “manual unchanged since 2.56.0 (2026-09-28)”. Supports: One linked worktree per agent so parallel work doesn't share a checkout.
Original observations
- 2026-11 · Our own multi-agent workflow: The rule that actually held was the boring one: workers never land, never push the default branch, never deploy. A human or the supervising bot does that after re-running the checks. Writing it into every brief stopped the 'I assumed done meant merged' class of surprise.
- 2026-11 · Our own workflow: An agent's self-reported 'all tests pass' and the reviewer's re-run disagreed often enough that we stopped accepting the self-report as evidence.
Test environment: tools/handoff_lint.py run against the template and two fixtures (tests/run_tests.py), 2026-11-15, Python 3.13 on a Linux box. The branch-protection half was not exercised against a live repository for this edition.
Measured outcomes
- Linter: good fixture passes; template fails only on the placeholder SHA; bad fixture fails on all seven expected problems.
Uncertainty
- The linter is a regex heuristic. It catches missing structure and obvious grants ('feel free to merge'), not clever wording.
- Client-side permission syntax differs across agent tools and changes often; S2 is one example, not a standard.
Counterexamples
- A solo developer on a private repo on GitHub Free can't use protected branches (S1). There the server-side half of this pack doesn't apply; use a separate non-admin token and a pre-receive hook on a self-hosted remote, or accept the weaker client-side controls knowingly.
Files shipped with this pack
Rights and integrity
License: CC BY 4.0 for text; template and linter MIT. Provenance: Gus, from our own multi-agent practice; tool behaviour from vendor docs fetched 2026-11-15.
Permissions: Dry-run by default; landing and deploy are always owner-gated. Telemetry: None.
Content digest: sha256:e93854b98fd1cebcd2c094eae59e311dd459e1c2d4c78808d77ecc5d7323c781
SHA-256 of the canonical source file packs/ab-001-coding-agent-handoff.md as built. Identifies content; does not prove authorship.
Changelog
- 0.1.0 · 2026-11-15: First private draft with template and linter; linter tested on fixtures.