DEV Community

Avery Li
Avery Li

Posted on

The Pairing Computed a Blame Window Before Any Free Model Saw the Bug

A pairing session should freeze a git blame window before any free model or free server sees the failing test. Unbounded agent search burns tokens on refactors, generated files, and comments that never caused the regression. The senior in this protocol treats blame as a contract, not as a hint, and refuses remote help until that contract exists. The kept decision is a small packet of paths, commits, and a replay command that any later model must respect.

The pairing problem

Agent debugging often starts with a paste of the repository tree and a request to find the bug. That opening move hides ownership, mixes generated noise with real edits, and makes later review almost impossible. The protocol below is a proposed pairing workflow, not a report of a single production incident. It exists so a senior and a driver can leave a replayable packet instead of a chat transcript.

Questions the senior asked

The senior does not start with model selection and does not start with a remote machine. The senior starts with a short question list that the driver must answer from local evidence. Each answer must name a file, a command, or a commit, because adjectives do not shrink a search. The pairing records those answers in questions.md before anyone discusses models or a free server.

The recorded file is a pairing artifact, not a chat log, and it stays in the repository beside the failing test. The driver fills every line from commands already run on the laptop, never from memory of an earlier agent session. Incomplete answers are treated as a stop condition rather than as a reason to widen the prompt. The proposed questions.md shape looks like the block below.

# questions.md — pairing record (proposed, unexecuted example)

Q1. Which test is the first unique failure, including file, case, and assertion text?
A1. tests/billing/test_proration.py::test_partial_month_keeps_credit
    signature: AssertionError: expected 420, got 399

Q2. Which source path last changed the failing assertion, according to git blame?
A2. billing/proration.py:88  commit 9f2c1aa  (not the formatting commit on line 1)

Q3. Does that path sit in generated code, vendor code, or a large rename window?
A3. No. Denylist rejected proto/*, vendor/*, and **/*.snap.

Q4. What command replays the failure in under one minute on a cold checkout?
A4. pytest -q tests/billing/test_proration.py::test_partial_month_keeps_credit

Q5. What will this pairing refuse to send off the machine?
A5. Anything outside blame_window.json paths, plus secrets, .env, and fixture dumps.
Enter fullscreen mode Exit fullscreen mode

Dead ends the pairing logged

The first dead end appeared when the driver blamed an entire service file after a generic test name. Git blame then pointed at a formatting commit, a rename, and a license header that never touched control flow. The pairing discarded that window because it could not name a unique assertion or a unique function. The dead-end register stored the rejected command so the next model prompt could not repeat it.

The second dead end appeared when vendor directories and generated protobuf stubs entered the same blame window. Those paths changed often, carried no readable intent, and would have dominated any later model context window. The senior required a path denylist before any further blame computation could be considered a complete packet. The register recorded the denylist as a first-class file rather than as a remembered conversation detail.

The next dead end used HEAD~50 as a lazy commit range after a large directory rename in history. Follow-up copies of the same function appeared twice, and the failing assertion no longer mapped to one side. The pairing replaced the numeric range with an explicit merge-base against the last known green default branch. That replacement became the only commit bound that the pairing later allowed inside the kept decision packet.

A further dead end sent a full stack trace without extracting the unique assertion message from the test runner. Models then proposed patches for log lines, retries, and timeouts that never appeared in the failing expectation. The pairing now requires a one-line failure signature copied from the test output before remote work starts. Without that signature, the gate script exits nonzero and no free model call is constructed at all.

The dead-end register is append-only JSONL so a later reader can see rejected paths without reconstructing Slack. Each line names the hypothesis, the command, the reason, and the next constraint the senior added. The pairing does not delete dead ends when a better window appears, because deleted failures return as fresh suggestions. A proposed log looks like the following example.

{"id":"de-01","hypothesis":"blame whole billing/proration.py","command":"git blame billing/proration.py","reason":"window included license header and a rename","next":"blame only the assertion line"}
{"id":"de-02","hypothesis":"include generated stubs for extra context","command":"git diff --stat proto/","reason":"generated paths churn every build","next":"add proto/ and vendor/ to denylist"}
{"id":"de-03","hypothesis":"HEAD~50 is a safe default window","command":"git log --oneline HEAD~50..HEAD -- billing/","reason":"rename duplicated the function","next":"use merge-base with origin/main"}
{"id":"de-04","hypothesis":"paste full traceback into the model","command":"pytest -vv","reason":"trace mixed timeouts with the real assertion","next":"keep one-line failure signature only"}
Enter fullscreen mode Exit fullscreen mode

The decision the pairing kept

After the dead ends, the pairing kept one decision and wrote it down as a machine-checkable packet. The packet contains a blame window, a path denylist, a replay script, and a one-line failure signature. Remote or free-model help may read only those files, plus the source paths listed in the window. Anything outside that set is treated as leakage and is a protocol failure, not a convenience.

The kept decision is boring on purpose, because boring packets are reviewable in a pull request. The senior signs the packet by merging blame_window.json before any agent diff is allowed to land. The driver may still type in a model product after that merge, but the prompt is generated by a gate, not by memory. The rest of this article is the packet format and the commands that enforce it.

Artifact: the blame window packet

The packet lives in the repository root beside the failing test, not in a disposable chat sidebar. The pairing treats missing keys as a failed build rather than as an invitation to improvise. The following workflow is labeled as a proposed, unexecuted example and should be adapted to the local default branch name. Numbered steps keep the senior and the driver on the same checklist.

Step 1: Capture a unique failure signature

The driver reruns one test until the assertion line is stable across two consecutive executions. The pairing copies that one line into the packet and ignores the surrounding traceback frames. A signature that still mentions retries, sockets, or timestamps is rejected as too broad. The command below is the only replay the later gate will accept.

# Proposed local replay. Label: unexecuted example.
pytest -q tests/billing/test_proration.py::test_partial_month_keeps_credit
# Expected unique signature:
# AssertionError: expected 420, got 399
Enter fullscreen mode Exit fullscreen mode

Step 2: Compute a candidate blame window

The driver blames the assertion line, not the file, and prints porcelain output for a single range. The senior watches for formatting commits, copy-right headers, and generated markers before accepting the sha. If blame lands on a generated path, the pairing logs a dead end and stops. The helper script below encodes that stop condition.

#!/usr/bin/env bash
# compute_blame_window.sh — proposed pairing helper, unexecuted example.
set -euo pipefail

FAIL_FILE="${1:?path to failing source file}"
FAIL_LINE="${2:?1-based line number}"
DEFAULT_BRANCH="${3:-origin/main}"
DENY_FILE="${4:-blame_denylist.txt}"

if [[ ! -f "$FAIL_FILE" ]]; then
  echo "dead_end: missing_file $FAIL_FILE" >&2
  exit 2
fi

if [[ -f "$DENY_FILE" ]] && echo "$FAIL_FILE" | grep -E -f "$DENY_FILE" >/dev/null; then
  echo "dead_end: path_denied $FAIL_FILE" >&2
  exit 3
fi

MERGE_BASE="$(git merge-base HEAD "$DEFAULT_BRANCH")"
echo "merge_base=$MERGE_BASE"

git blame -L "$FAIL_LINE,$FAIL_LINE" --porcelain "$FAIL_FILE" | awk '
  /^[0-9a-f]{40} / { sha=$1 }
  /^filename / { fname=$2 }
  END { print "blame_sha=" sha; print "blame_file=" fname }
'
Enter fullscreen mode Exit fullscreen mode

A tiny denylist keeps the second dead end from returning on the next session. The pairing reviews that file the same way it reviews .gitignore, because both files define what must stay local. Generated stubs, snapshots, and vendored trees belong there even when they appear in the stack. The example denylist is intentionally short.

# blame_denylist.txt
^proto/
^vendor/
\.snap$
\.pb\.go$
\.generated\.
Enter fullscreen mode Exit fullscreen mode

Step 3: Bind the merge-base instead of a numeric range

Numeric ranges look fast and then fail after renames, squash merges, and bot commits. The pairing asks git for the merge-base with the last known green default branch and stores that sha. Later prompts may read diffs only inside that bound, which keeps rename copies from doubling the window. The driver records the bound in the packet rather than in a spoken agreement.

# Proposed bound. Label: unexecuted example.
git merge-base HEAD origin/main
git diff --name-only "$(git merge-base HEAD origin/main)" -- billing/proration.py
Enter fullscreen mode Exit fullscreen mode

Step 4: Write the packet the gate will trust

The packet is JSON because a missing key should fail in CI, not in a hallway conversation. The senior reads the paths array aloud and confirms each path is a real tracked file. The replay field must be a single command the driver has already run twice. The proposed packet for this writeup is the object below.

{
  "failure_signature": "AssertionError: expected 420, got 399",
  "merge_base": "b4e91c0d9f3a7c1e2a8b0d4f6c7e9a1023344556",
  "paths": ["billing/proration.py", "tests/billing/test_proration.py"],
  "denylist": ["proto/", "vendor/", "*.snap"],
  "replay": "pytest -q tests/billing/test_proration.py::test_partial_month_keeps_credit",
  "kept_decision": "Send only the blame window packet off-machine. Reject whole-repo prompts."
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Gate any remote or free-model prompt

The gate reads the packet, builds a constrained prompt, and refuses to run when keys are missing. Humans can still bypass the gate, which is why the pairing also reviews the prompt file in the same pull request. The script prints the prompt to stdout so the driver can see exactly what leaves the laptop. This is proposed code, not a production security boundary.

#!/usr/bin/env python3
"""gate_before_remote.py — proposed pairing gate, unexecuted example."""
import json
import sys
from pathlib import Path

REQUIRED = ("failure_signature", "merge_base", "paths", "replay", "denylist")


def load_packet(path: Path) -> dict:
    data = json.loads(path.read_text())
    missing = [key for key in REQUIRED if key not in data]
    if missing:
        raise SystemExit(f"dead_end: missing_keys {missing}")
    if not str(data["failure_signature"]).strip():
        raise SystemExit("dead_end: empty_failure_signature")
    if not data["paths"]:
        raise SystemExit("dead_end: empty_paths")
    if not str(data["replay"]).strip():
        raise SystemExit("dead_end: empty_replay")
    return data


def build_prompt(data: dict) -> str:
    path_lines = "\n".join(f"- {path}" for path in data["paths"])
    deny_lines = "\n".join(f"- {rule}" for rule in data["denylist"])
    return (
        "Stay inside the blame window. Do not request other files.\n"
        f"Failure signature: {data['failure_signature']}\n"
        f"Merge base: {data['merge_base']}\n"
        f"Paths:\n{path_lines}\n"
        f"Denylist:\n{deny_lines}\n"
        f"Replay: {data['replay']}\n"
        "Propose a patch only if the replay command stays unchanged.\n"
    )


if __name__ == "__main__":
    packet = load_packet(Path(sys.argv[1]))
    sys.stdout.write(build_prompt(packet))
Enter fullscreen mode Exit fullscreen mode

Step 6: Compare any later patch to the replay, not to the chat

The pairing applies a candidate patch on a fresh branch and runs only the stored replay command. If the unique signature disappears, the pairing still reads the diff against the blame paths before merge. If the patch touches a denylisted path, the pairing logs another dead end and reverts. Chat agreement never overrides a failed replay.

# Proposed verification. Label: unexecuted example.
git checkout -b pairing/proration-window
# apply the candidate patch, then:
python3 gate_before_remote.py blame_window.json > /tmp/pairing-prompt.txt
pytest -q tests/billing/test_proration.py::test_partial_month_keeps_credit
git diff --name-only
Enter fullscreen mode Exit fullscreen mode

Decision table the senior keeps on the desk

The table is part of the artifact because it turns arguments into rows the pairing can reject quickly. The driver does not negotiate extra files after the packet is written, except by logging a new dead end and starting over. The senior uses the table during review to explain why a convenient paste was refused. The rows below match the dead ends already recorded.

Observation Pairing action Allowed off-machine
Unique assertion plus one blamed line Write blame_window.json Packet paths only
Blame hits vendor/ or generated stubs Log dead end, extend denylist Nothing until packet is rewritten
Numeric HEAD~N after a rename Replace with git merge-base Nothing until merge-base is stored
Full traceback without a one-line signature Stop the gate Nothing
Candidate patch changes denylisted paths Revert and log dead end Nothing
Replay still fails with the same signature Keep the packet, reject the patch Packet may be reused

Using free model access only after the packet exists

Once the packet exists, the pairing still does not paste the repository into a chat box. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open-source coding assistant with operator-supplied free model access and a free server option. Those two availability claims are the only product capabilities this protocol depends on, and no quota, model name, or hardware size is asserted here. The pairing uses that access only as a runner for the already-narrowed packet, never as a search engine for the whole tree.

The practical sequence is local packet, local gate, then a constrained session if the pairing still wants a second reader. The free server option matters when the laptop should not keep a long agent process, not when the repository still lacks a blame window. The free model path matters when the pairing wants another pass over the same four files without opening a paid account first. Neither option repairs a missing signature, a missing merge-base, or a denylist the senior has not reviewed.

A pairing that skips the packet and jumps straight to any hosted agent is doing a different activity. That activity can still find bugs, but it leaves no replayable ownership trail for the next reviewer. This protocol prefers a smaller, duller prompt that a later engineer can regenerate from git. The product mention stays secondary because the gate would work with any later runner that accepts a text prompt.

Limitations

The blame window protocol fails when the regression is a configuration change with no line to blame in application code. It also fails when the failing assertion is nondeterministic, because the signature will not stay unique across two replays. Binary fixtures, data migrations, and time-dependent tests need a different packet, usually built from seeds and clocks rather than from git blame. Pairings that ignore those cases will freeze the wrong files and then trust a confident patch.

The gate script is not a sandbox, not a secret scanner, and not an access-control system for a shared server. A driver can still paste extra files, and a model can still ask for them. The protocol only makes that extra paste visible in questions.md, dead_ends.jsonl, and the generated prompt file. Teams that need isolation still need their existing policy for credentials, production data, and licensed dependencies.

Git history itself can lie after copy, squash, or filter-repo, which is why the senior still reads the blamed commit. Merge-base against a moving default branch can shift between pairing sessions if someone lands unrelated work. The packet should therefore pin the merge-base sha rather than the branch name at review time. Without that pin, two pairings can believe they share a window while reading different diffs.

Who should not use this approach

Solo developers who already know the failing line and can patch it in one edit do not need this packet. Incident responders who must change production configuration in minutes should not wait on blame porcelain and a denylist review. Pairings working in generated-only repositories, such as some schema-first trees, will spend the session fighting the denylist. Beginners who have not yet learned git merge-base should practice that command on a toy repo before adding a model runner.

Teams without a unique automated test should also skip this workflow, because the replay field would be theater. If the only reproduction is a manual click path, the pairing needs a scripted client first, then a blame window. The protocol assumes the senior can reject extra files without political cost, which is not true in every organization. Where that rejection is impossible, the packet becomes documentation of a leak rather than a gate.

What the pairing leaves behind

The useful residue is not a clever prompt. The useful residue is questions.md, dead_ends.jsonl, blame_window.json, and a replay that still fails or still passes on a clean checkout. A later engineer can regenerate the same constrained prompt without trusting anyone's memory of the session. That is the whole point of computing the blame window before any free model sees the bug.

Pairs that already keep a blame window packet can run the same gate, then try MonkeyCode's free model access and free server option against that packet and compare the replay log with the kept decision.

Top comments (0)