A pairing session should refuse every remote agent job until three local artifacts already exist on disk. The first artifact is a rollback SHA that a clean checkout can restore without later debate. The second artifact is a file-ownership map that names a human reviewer for every path the agent may touch. The third artifact is a question ledger that records who answered each open item before any free-tier call leaves the machine.
This article describes a proposed pairing workflow for teams that already sit an AI coding agent beside a senior engineer. It is not a report of a private production incident, a customer win, or a measured latency benchmark. The workflow treats questions, dead ends, and the kept decision as first-class files rather than chat residue. Teams that skip those files tend to merge remote output that nobody can restore with one command.
The core conclusion stays small enough to pin beside the keyboard during a noisy pairing block. Remote generation may start only after rollback data, ownership maps, and answered ledgers already live under .pairing/. A free model call and a free server do not weaken that gate, because availability is not merge authority. The pairing below walks through senior questions, three failed dead ends, and the decision that remained on disk.
What the senior asked during the session
The senior engineer did not open with architecture slides, vendor comparisons, or a recycled prompt library. The first spoken request was to name the SHA that would restore the tree if the agent session failed. The second request was to list every path the agent might edit, with a human owner beside each path. The third request was to write every unanswered question into a ledger before anyone pasted a stack trace into a model.
Quoted pairing lines can stay short and still force a real file to appear on disk. The line naming the rollback SHA produced git rev-parse HEAD instead of a vague restoration plan. The line naming the owner of each path blocked glob patterns that hid review from the senior. The line naming who answered each item stopped the junior from treating the model as a silent teammate.
Dead end one: generate first, own later
The junior wanted to ship a dirty working tree to a remote host just to watch the agent move. That path failed because a dirty tree has no honest rollback SHA that a later bisect can trust. Generated files mixed with leftover debug prints, so ownership could not be assigned without a blame fight. The pairing reset the tree, wrote the SHA while HEAD was clean, and only then discussed remote work.
Dead end two: let the loop pick the files
The agent proposed a retry loop that grepped the repository and patched every match it considered similar. The senior rejected that loop because a grep result is not a review contract and not an owner list. Two extra iterations rewrote a logging helper that nobody in the pairing had agreed to touch. The pairing stopped the loop, restored the helper from the rollback SHA, and required an owners file.
Dead end three: treat the chat as the log
The junior saved a long chat export and then called that export the official pairing decision record. Chat order is not merge order, and later edits in the same thread silently replace earlier constraints. A reviewer who was absent from the call cannot reconstruct owners, answers, or the rollback point from prose. The pairing discarded the export as evidence and required three small files that a diff can review.
The decision that survived the three dead ends
After those three dead ends, the pairing kept one rule and wrote it into the repository. No remote agent job is allowed to start until .pairing/rollback, .pairing/owners.txt, and .pairing/questions.tsv all exist on disk. Those files are the kept decision, not a retrospective paragraph written after a merge already landed. If a later pairing wants a different policy, it must change the files and the gate together.
Artifact: a decision table, three files, and a gate
The following files and the gate script are a proposed, unexecuted example for a local pairing branch. They do not claim production uptime, token quotas, hardware capacity, or any permanent free-tier promise. A team should copy them into a scratch branch, run the commands, and then delete them if the gate is too strict. The script exits nonzero when any required pairing file is missing, empty, or still marked unanswered.
| Dead end | What the junior tried | Why the senior stopped it | File that remains |
|---|---|---|---|
| Generate first | Push a dirty tree to a remote host | No honest rollback SHA | .pairing/rollback |
| Loop picks files | Grep-and-patch similar symbols | No human owner contract | .pairing/owners.txt |
| Chat as log | Treat a transcript export as evidence | Not reviewable in a later diff | .pairing/questions.tsv |
The table is the pairing memory of the three dead ends, reduced to files a later reviewer can open. Owners and questions use boring text so the gate never needs a YAML library on a pairing laptop. A comment line starting with # is ignored, which keeps examples readable during the session. The rollback file remains a raw SHA because extra commentary would invite another chat-shaped decision log.
# .pairing/owners.txt
# format: path<TAB>human-owner
# globs are rejected by pairing_gate.py
src/billing/invoice.py alex
src/billing/totals.py alex
tests/billing/test_invoice.py alex
# .pairing/questions.tsv
# columns: id, status, owner, question, answer
# status must be answered or deferred; open fails the gate
id status owner question answer
Q1 answered sam What SHA restores this tree Use git rev-parse HEAD while porcelain is empty
Q2 answered alex Who owns invoice.py alex reviews any diff under src/billing/invoice.py
Q3 deferred sam Do we retune tax rounding Reopen after rollback plus owned invoice tests pass
# Proposed commands. Run them on a scratch branch, not on main.
git status --porcelain
git rev-parse HEAD
mkdir -p .pairing
git rev-parse HEAD > .pairing/rollback
printf '%s\n' 'src/billing/invoice.py alex' > .pairing/owners.txt
python3 pairing_gate.py
#!/usr/bin/env python3
"""Proposed pairing gate. Unexecuted example. Standard library only."""
from __future__ import annotations
import pathlib
import subprocess
import sys
ROOT = pathlib.Path.cwd()
PAIRING = ROOT / ".pairing"
ROLLBACK = PAIRING / "rollback"
OWNERS = PAIRING / "owners.txt"
QUESTIONS = PAIRING / "questions.tsv"
ALLOWED_STATUS = {"answered", "deferred"}
def fail(message: str) -> None:
print(f"pairing-gate: {message}", file=sys.stderr)
raise SystemExit(1)
def git(*args: str) -> str:
result = subprocess.run(
["git", *args],
cwd=ROOT,
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
fail(f"git {' '.join(args)} failed: {result.stderr.strip()}")
return result.stdout.strip()
def read_rows(path: pathlib.Path) -> list[str]:
if not path.is_file():
fail(f"missing {path}")
lines = []
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
lines.append(raw.rstrip("\n"))
if not lines:
fail(f"{path} has no data rows")
return lines
def main() -> None:
git("rev-parse", "--is-inside-work-tree")
porcelain = git("status", "--porcelain")
if porcelain:
fail("working tree is dirty; restore or stash before remote work")
sha = ROLLBACK.read_text(encoding="utf-8").strip() if ROLLBACK.is_file() else ""
if not sha or any(ch.isspace() for ch in sha):
fail("rollback file must contain a single SHA")
git("cat-file", "-t", sha)
head = git("rev-parse", "HEAD")
if sha != head:
fail(f"rollback {sha} does not match HEAD {head}")
owned = {}
for row in read_rows(OWNERS):
if "\t" not in row:
fail(f"owner row must be path<TAB>human: {row}")
path, owner = row.split("\t", 1)
path, owner = path.strip(), owner.strip()
if not path or not owner:
fail(f"empty path or owner: {row}")
if any(ch in path for ch in "*?["):
fail(f"globs are not owners: {path}")
if owner.lower() in {"agent", "model", "bot"}:
fail(f"owner must be a human: {owner}")
owned[path] = owner
answered = 0
for index, row in enumerate(read_rows(QUESTIONS)):
parts = row.split("\t")
if index == 0 and parts and parts[0] == "id":
continue
if len(parts) != 5:
fail(f"question row must have five TSV columns: {row}")
qid, status, owner, question, answer = [part.strip() for part in parts]
if status not in ALLOWED_STATUS:
fail(f"{qid} status {status} is not answered or deferred")
if not owner or not question:
fail(f"{qid} needs an owner and a question")
if status == "answered" and not answer:
fail(f"{qid} is answered but the answer cell is empty")
if status == "deferred" and not answer:
fail(f"{qid} is deferred but has no reopen reason")
answered += 1
print("pairing-gate: ok")
print(f"rollback={sha}")
print(f"owned_paths={len(owned)}")
print(f"ledger_rows={answered}")
if __name__ == "__main__":
main()
The gate is intentionally boring so a pairing can run it before anyone opens a browser tab. It refuses a dirty tree, an unknown SHA, an empty owner map, and any question still marked open. It does not compile the project, run the test suite, or judge whether the remote model wrote good code. Those checks stay with the human owners after the agent returns a patch against the rollback SHA.
Numbered pairing workflow
1. Freeze a clean rollback SHA
The pairing inspects git status --porcelain and restores or stashes anything that would poison the SHA. The pairing writes the output of git rev-parse HEAD into .pairing/rollback while the working tree is still empty. The pairing confirms restoration with a dry git reset --hard against that SHA on a throwaway clone. Only after that clone matches HEAD does the pairing treat the SHA as a real rollback point.
git clone --no-checkout . /tmp/rollback-check
git -C /tmp/rollback-check reset --hard "$(cat .pairing/rollback)"
git -C /tmp/rollback-check rev-parse HEAD
2. Write owners before any path is eligible
Each eligible path gets one human owner who will read the later diff before merge. Shared libraries and generated fixtures stay off the list until someone volunteers in the pairing notes. If the agent wants a new path, the pairing amends the owners file first and reruns the gate. An owner name that matches a model label is rejected, because models do not accept review mail.
3. Fill the question ledger before any prompt
Every question the senior asked becomes a row with an owner, a status, and a one-line answer. Unanswered rows block the gate even when the junior already drafted a long prompt for the agent. Deferred rows need a reason, a later SHA, and a human who will reopen the ledger. The pairing reads the ledger aloud once so silent assumptions cannot hide inside the prompt text.
4. Run the local gate
The pairing runs python3 pairing_gate.py from the repository root and treats a nonzero exit as a stop. A passing gate means the three files exist, the tree is clean, and no question remains open. A failing gate is not a prompt-engineering problem, and the pairing does not retry the model to fix it. The pairing edits the files, reruns the command, and only then discusses whether a remote job is still needed.
5. Bound the remote job against owned paths
Disclosure: This article was prepared as part of MonkeyCode's product outreach. Once the local gate passes, a bounded generation job may use MonkeyCode free model access and the free server option. The remote host is a scratch worker, not a source of truth and not a substitute for the owners file. The prompt must paste the owned paths and the rollback SHA so the agent cannot widen the blast radius. Output that touches an unowned path is discarded, even when the generated patch looks locally elegant.
# After a remote patch lands in /tmp/agent.patch, keep owned paths only.
git worktree add /tmp/owned-review "$(cat .pairing/rollback)"
git -C /tmp/owned-review apply --check /tmp/agent.patch
# Reviewers then inspect only paths listed in .pairing/owners.txt.
6. Merge only the owned diff
The pairing checks out the rollback SHA in a clean worktree and applies only the owned-path diff. Human owners read that diff against the question ledger rather than against the original chat transcript. Tests that the pairing already trusted must still pass on the rollback SHA plus the owned diff. Anything the remote host wrote outside those paths stays untracked and is deleted before merge.
git -C /tmp/owned-review diff --name-only "$(cat .pairing/rollback)"
# Compare that list to .pairing/owners.txt before anyone runs git merge.
Limitations and who should not use this
This workflow assumes a Git repository, two people who can interrupt each other, and a clean tree. Solo developers who need a fast scratch pad will find the gate louder than the bug they came to fix. Incident responders working on a live outage should not pause to invent owners files during a page. Teams without a senior who will actually reject unowned paths will only produce three more empty documents.
The article does not measure model quality, server throughput, or how long a free tier remains available. Those numbers change, and no pairing file in this workflow pretends to freeze a vendor capability. The gate also cannot detect a wrong but confidently answered ledger row, which remains a human failure mode. Security-sensitive repositories should keep secrets out of ledgers, prompts, and every remote scratch host they use.
What the pairing kept
The pairing asked for a rollback SHA, named file owners, and wrote answers before any remote agent work. Three dead ends tried to skip those files, and each one mixed generated noise with unowned edits. The decision that remained is local and boring: keep the three files, run the gate, then maybe generate. Remote availability can wait; an unowned patch cannot be honestly merged, even when the host itself was free.
Top comments (0)