DEV Community

Avery Li
Avery Li

Posted on

The Pairing Merged a Transcript File Before It Merged Any Agent Diff

The pairing should treat the session transcript as the first merge artifact, not the generated patch. A senior engineer can review questions, recorded dead ends, and one named keep without trusting an unverified agent diff. The working tree stays clean when drafts run on a rehearsal host that the team can destroy. This article proposes a concrete protocol, a small transcript schema, and a merge gate that refuses patches without that packet.

Why generated diffs fail senior review

Agent drafts often arrive as a pile of files with no record of what was tried and discarded. Seniors then reverse-engineer intent from the diff, which hides failed approaches that still constrain the design. Reviewers cannot tell whether a rename was a reasoned keep or the last of several unlogged guesses. The protocol below records each question, each dead end, and a single keep so the review packet stands without the chat.

Loop-style coding assistants make this gap worse when they retry silently on the same protected tree. A pairing that cannot point at a rehearsal command for each failure will relitigate the same dead end after lunch. The proposed gate requires every dead end to cite a command, an exit code, and a host outside the protected clone.

Proposed pairing roles and constraints

This section describes a proposed workflow, not a measured case study from a named production team. The junior or coding agent may draft only on a throwaway rehearsal host that reviewers can delete. The senior answers only those questions that already name a repository path and a proving command. The merge referee is a small checker that reads pairing-transcript.json and blocks the pull request when required fields are missing.

The kept decision is not a paragraph in chat or a vague thumbs-up in a review thread. It is one machine-readable object with an identifier, a rationale, and the files it may touch. Later patches that fall outside that allow-list fail the same checker without further debate. The pairing stops generating once that keep is named, instead of opening a fourth architectural fork.

The transcript schema

The pairing writes one JSON document that a hook can validate during the same session. Each question names the file under discussion and the command that would prove an answer. Each dead end stores the rehearsal command, the observed exit status, and a one-line reason the approach is closed. The keep object appears at most once and lists the paths the eventual patch may change.

{
  "session_id": "2026-09-13-auth-cookie-rotate",
  "protected_clone": "/work/app",
  "rehearsal_host": "rehearsal.example.internal",
  "questions": [
    {
      "id": "q1",
      "asked_by": "junior",
      "text": "Should rotate_cookie write Set-Cookie in the handler or a middleware?",
      "names_path": "src/http/session.ts",
      "proving_command": "npm test -- session.test.ts"
    }
  ],
  "dead_ends": [],
  "keep": null
}
Enter fullscreen mode Exit fullscreen mode

The schema stays small so a pairing can fill it while talking rather than reconstructing it from memory afterward. Empty dead-end arrays remain legal only while questions are still open and no keep has been named. The checker later rejects a keep that names paths never mentioned in a question or in a recorded dead end.

Numbered pairing procedure

1. Freeze the protected clone

The pairing clones the service into a read-only working copy and refuses agent writes against that tree. A second clone, or a disposable host, receives every experimental edit and every proving command. The senior confirms both roots in the transcript before any model call is allowed to start. Secrets from the protected clone must not be copied into the rehearsal host.

#!/usr/bin/env bash
set -euo pipefail
PROTECTED="/work/app"
REHEARSAL="/tmp/rehearsal-app"
git clone -- "$PROTECTED" "$REHEARSAL"
chmod -R a-w "$PROTECTED/.git"
printf 'protected=%s\nrehearsal=%s\n' "$PROTECTED" "$REHEARSAL"
Enter fullscreen mode Exit fullscreen mode

The script only prepares two trees and does not imply extra product features, quotas, or hardware. Teams may replace the local rehearsal directory with a short-lived virtual machine when network isolation matters more than convenience. The protected clone remains the only candidate for merge, even when the rehearsal host looks healthier during the afternoon.

2. Record every question before anyone answers

The junior writes the next question into the transcript with a path and a proving command. The senior declines questions that lack those two fields, because later readers cannot check them. A small helper appends the question and refuses free-form chat as the system of record. Models that emit patches before a question exists are ignored, regardless of how fluent the draft looks.

# pairing_log.py — proposed helper, not a production service
import json
from pathlib import Path

def append_question(path: Path, item: dict) -> None:
    data = json.loads(path.read_text())
    if not item.get("names_path") or not item.get("proving_command"):
        raise ValueError("question must name a path and a proving command")
    data["questions"].append(item)
    path.write_text(json.dumps(data, indent=2) + "\n")
Enter fullscreen mode Exit fullscreen mode

This step is the pairing throttle, not a style lecture about prompt writing. The senior still owns the answers; the log only makes each question durable enough for a merge referee. A question that cannot name a file is usually an architecture debate and should wait until a path exists.

3. Run candidate edits only on the rehearsal host

Drafts execute against the rehearsal tree and never against the protected clone. Each attempt records the command that was supposed to pass and the exit code that actually returned. A non-zero exit becomes a dead-end candidate, not an invitation to edit the real branch. The pairing copies command output back to the senior and leaves binaries on the rehearsal host.

#!/usr/bin/env bash
set -euo pipefail
REHEARSAL="${REHEARSAL:?}"
CMD="${1:?}"
set +e
( cd "$REHEARSAL" && bash -lc "$CMD" )
status=$?
set -e
python3 - <<'PY' "$CMD" "$status"
import json, sys
print(json.dumps({"command": sys.argv[1], "exit_code": int(sys.argv[2])}))
PY
Enter fullscreen mode Exit fullscreen mode

If the rehearsal place is a remote free server, the same rule still applies to every file that moves. Pull logs and failing assertions, not mystery build caches that nobody can replay. The senior should be able to quote the command later inside a dead-end object without asking the model to remember it.

4. Promote a failure into a named dead end

A failed rehearsal is not yet a dead end, because the pairing may still lack a reason the approach cannot be repaired. The senior names it a dead end only after that reason can be written in one line beside the command evidence. The dead-end object also points at the question it closes, so later readers do not retry it casually. Missing links back to a question fail the helper instead of entering the log.

def append_dead_end(path: Path, item: dict) -> None:
    required = ("id", "command", "exit_code", "reason", "closes_question")
    missing = [k for k in required if k not in item]
    if missing:
        raise ValueError(f"dead end missing {missing}")
    data = json.loads(path.read_text())
    qids = {q["id"] for q in data["questions"]}
    if item["closes_question"] not in qids:
        raise ValueError("dead end must close a recorded question")
    data["dead_ends"].append(item)
    path.write_text(json.dumps(data, indent=2) + "\n")
Enter fullscreen mode Exit fullscreen mode

The following dead ends are labeled illustrations, not timed benchmarks or claimed production incidents. The first closes a middleware-only design after the rehearsal test could not see the rotated cookie on a redirect. The second closes a handler-only design after duplicate Set-Cookie headers appeared in the captured rehearsal response. Both entries exist so the keep does not look like an unexplained preference.

[
  {
    "id": "d1",
    "closes_question": "q1",
    "command": "npm test -- session.test.ts",
    "exit_code": 1,
    "reason": "middleware Set-Cookie never reached the redirected client fixture"
  },
  {
    "id": "d2",
    "closes_question": "q1",
    "command": "npm test -- session.test.ts",
    "exit_code": 1,
    "reason": "handler-only write duplicated Set-Cookie on rotate after login"
  }
]
Enter fullscreen mode Exit fullscreen mode

5. Name exactly one keep

After dead ends accumulate, the senior writes a single keep object and stops the drafting loop. The keep lists allowed paths, the proving command that must pass on a clean checkout, and the question it answers. A second keep in the same session is a protocol violation and fails the checker. Additional model output that touches other files is discarded even when it looks helpful.

def set_keep(path: Path, keep: dict) -> None:
    data = json.loads(path.read_text())
    if data.get("keep"):
        raise ValueError("session already has a keep")
    allowed = set(keep.get("allowed_paths") or [])
    mentioned = {q["names_path"] for q in data["questions"]}
    mentioned |= {
        p for d in data["dead_ends"] for p in d.get("touched_paths", [])
    }
    if not allowed or not allowed.issubset(mentioned):
        raise ValueError("keep paths must be a non-empty subset of questioned paths")
    data["keep"] = keep
    path.write_text(json.dumps(data, indent=2) + "\n")
Enter fullscreen mode Exit fullscreen mode

In the illustrative cookie pairing, the senior kept a small helper called from login and from rotate, not a new middleware stack. The allow-list named src/http/session.ts and src/http/session.test.ts and nothing else. That keep is the decision the pairing retains; the discarded agent APIs remain visible only as dead ends. Implementation after the keep can be typed by a human or applied as a tightly scoped patch.

6. Gate the merge on the transcript

A pre-commit or CI step loads the transcript and the proposed diff before anyone argues about style. It rejects the change when questions lack proving commands, when dead ends lack rehearsal exit codes, or when the keep is missing. It also rejects files that are not in keep.allowed_paths. The transcript stays in the pull request so later readers can see which questions produced the keep.

# check_pairing_gate.py — proposed merge referee
import json, subprocess, sys
from pathlib import Path

def changed_files() -> set[str]:
    out = subprocess.check_output(
        ["git", "diff", "--name-only", "origin/main"], text=True
    )
    return {line.strip() for line in out.splitlines() if line.strip()}

def main() -> int:
    data = json.loads(Path("pairing-transcript.json").read_text())
    if not data.get("questions"):
        print("gate: no questions recorded")
        return 1
    if any(not q.get("proving_command") for q in data["questions"]):
        print("gate: a question lacks a proving command")
        return 1
    if data.get("keep") is None:
        print("gate: senior has not named a keep")
        return 1
    if not data.get("dead_ends"):
        print("gate: no dead ends recorded; pairing looks untested")
        return 1
    extra = changed_files() - set(data["keep"]["allowed_paths"])
    extra.discard("pairing-transcript.json")
    if extra:
        print(f"gate: files outside keep {sorted(extra)}")
        return 1
    return 0

if __name__ == "__main__":
    sys.exit(main())
Enter fullscreen mode Exit fullscreen mode

Wire the referee from a local hook so a missing transcript never reaches review by accident. The hook should fail closed when the JSON is absent, not skip the gate on a quiet working tree. Teams that already run required checks can add this command beside the existing test job.

#!/usr/bin/env bash
set -euo pipefail
python3 check_pairing_gate.py
Enter fullscreen mode Exit fullscreen mode

Decision table for the senior

Signal in the session Senior response Transcript action
Question with no path or command Do not answer Reject the append
Rehearsal command exits non-zero, cause still unclear Ask one follow-up Leave dead_ends unchanged
Rehearsal command exits non-zero, approach cannot be repaired here Close the approach Append a dead end
Two remaining designs still pass rehearsal Do not keep yet Record another question
Exactly one design fits the remaining constraints Name the keep Write keep and stop drafting
Patch touches a path outside the keep Block merge Fail the gate

The table is the pairing memory when the chat is noisy and the model offers a fourth design. Seniors can point at a row instead of restating philosophy in every session. Juniors can see why a fluent agent patch still failed the gate. The keep remains a file, not a vibe.

Where a free rehearsal host fits

Disclosure: This article was prepared as part of MonkeyCode's product outreach. A pairing that needs a disposable machine can use a coding assistant with free model access and a free server option, such as MonkeyCode, as the rehearsal place rather than the source of truth. The models draft only after a question exists in the transcript, and the server runs proving commands that the senior can quote in a dead end. The protected clone still never receives those writes until the keep is named and the gate passes.

The same protocol works with a local directory if a remote host is unnecessary for isolation. Product availability can change, and this article does not claim model names, quotas, hardware sizes, or lasting uptime. Teams should verify current access on the product surface they actually use, then keep the transcript format stable so the gate does not depend on a vendor. Readers who already isolate agent drafts may try the gate against a free rehearsal host when a throwaway environment is the missing piece.

Limitations

The protocol does not measure model quality and does not replace design review for security-sensitive changes. A dishonest transcript can still pass the checker if humans collude, because the gate validates structure rather than truth. Rehearsal hosts can diverge from production in operating system, CPU, or secret material, so a green rehearsal command is not a production proof. JSON logs also add ceremony that tiny typo fixes do not deserve.

Pairings that last ten minutes should skip the full schema and use a normal review on the protected clone. Long sessions benefit because dead ends otherwise vanish into scrollback and return as new agent suggestions. The gate also cannot see intent that never became a question, so seniors still need to refuse off-transcript architecture changes in the comment thread.

Who should not use this approach

Teams that cannot provision an isolated rehearsal tree should not send agent writes anywhere near the protected clone. Regulated environments that forbid unreviewed model output should keep models out of both trees, including free ones. Pairings that lack a senior willing to name a keep will only produce a thicker chat log with prettier JSON. People searching for an autonomous agent that merges itself will not find that behavior here.

The point of the protocol is to slow generation until a human decision is durable in a file. If the team wants maximum throughput of unreviewed patches, a transcript gate will feel like friction, and that friction is intentional. Treat the rehearsal host, free or otherwise, as disposable infrastructure. Treat the transcript as the thing the pairing actually keeps.

Top comments (0)