DEV Community

Avery Li
Avery Li

Posted on

A Senior Stopped the Pairing Until the Session Lived in YAML

A senior pairing session should not treat a free LLM transcript as the working agreement. Chat history on a replaceable endpoint drifts, and retrying the same prompt does not restore shared constraints. The durable artifact is a small session contract plus a local verifier that fails closed. The walkthrough below labels an unexecuted pairing protocol so a team can reproduce the check without a war story.

Why the transcript is a weak pairing log

Free model endpoints remain useful for drafting helpers, tests, and review notes during a pairing hour. They remain a poor place to store the rules that the pair already agreed to keep. A tab refresh, a provider swap, or a truncated tool call can erase the only copy of a stop condition. The senior in this protocol therefore treated the chat pane as a whiteboard, not as source control.

The protocol assumes a local repository, a YAML contract, and a Python 3.11 verifier. It does not assume a particular model name, quota, hardware profile, or uptime promise. Some teams route the drafting step through a hosted assistant that offers free model access.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access and free server option can host that drafting lane after the contract file already exists on disk. The verifier still runs on the operator's machine, and it does not need the remote server to remain the system of record.

Pairing setup the pair actually used

The example session is a one-hour pairing block on a small HTTP probe helper. The junior drafts with a free model. The senior reviews invariants, budgets, and stop conditions rather than the fluency of the prose. The repository starts with a pairing/ directory and a verifier that is allowed to fail before any model call.

pairing/
  session_contract.yaml
  verify_session.py
  gate.py
  fixtures/
    sample_model_reply.json
src/
  probe_helper.py
Enter fullscreen mode Exit fullscreen mode

The pair writes the contract before the first model call. That order is the point of the session. A later model reply is evidence against the contract, never a replacement for it.

Questions the senior asked before the first prompt

The senior did not start with a coding task. The senior started with four constraints that had to live outside the chat. Each item below blocked the first request until it had a file-backed answer.

  1. Where does the invariant live after this browser tab is closed?
  2. What fails closed if the endpoint returns a plausible empty tool list?
  3. Who can change the token budget without leaving a diff?
  4. What is the stop condition for this pairing hour, in minutes and in retries?

The junior's first instinct was to paste those answers into the system prompt. The senior rejected that move because a system prompt is still transcript. The answers moved into session_contract.yaml instead, where a later reviewer can read one schema rather than a thread.

# pairing/session_contract.yaml
schema_version: 1
session:
  id: "probe-helper-2026-09-03"
  owner: "pairing-pair"
  max_minutes: 60
  max_model_calls: 8
  stop_on: ["verifier_fail", "budget_exhausted", "secret_detected"]
invariants:
  - id: no_secrets_in_prompts
    path: pairing/fixtures/sample_model_reply.json
    forbid_regex: "(api[_-]?key|authorization:\\s*bearer)"
  - id: json_object_only
    path: pairing/fixtures/sample_model_reply.json
    must_be_json_object: true
  - id: helper_entrypoint
    path: src/probe_helper.py
    must_contain: "def run_probe("
budgets:
  max_prompt_chars: 4000
  max_reply_chars: 8000
  max_retries_per_call: 1
Enter fullscreen mode Exit fullscreen mode

The file is deliberately boring. Boring files survive pairing hours. Clever chat summaries do not, because they cannot fail a build.

Dead end one: pinning the transcript

The first failed approach was to export the chat and commit the export as the pairing log. The senior allowed a ten-minute spike so the failure would be visible in the same hour. The export contained useful fragments and three contradictions about the retry limit. One message said one retry. A later message said three. The file had no schema, so both statements looked equally official.

The spike ended when the senior asked which line a later reviewer should trust. There was no answer that did not require rereading the whole thread. The export was deleted. The YAML stayed, because a schema can reject a second retry value that a transcript will happily absorb.

Dead end two: asking the model to restate the rules

The second failed approach was a prompt that asked the model to print the pairing rules before writing code. The reply was fluent and almost correct. It dropped stop_on: secret_detected and invented a retry count that the pair had never set. A fluent miss is worse than a hard failure because it looks like agreement.

The senior kept one rule from that dead end. Model output may propose a contract patch as a unified diff. The pair applies the patch only after the verifier passes on the current file. The model does not get to overwrite the contract in place, even when the prose sounds confident.

Dead end three: hiding the rules in CI comments

The third failed approach was to park the constraints in a workflow comment and in a README paragraph. Comments do not fail closed. A README cannot reject a reply that contains a bearer token pattern. The senior treated documentation as a pointer to the contract, not as the contract itself.

After the third dead end, the pair stopped searching for a softer location. The decision that survived the hour is the one in the next section.

The decision the senior kept

Keep a versioned session contract in the repository. Verify it locally before every model call and after every saved reply. Treat the free endpoint as a draft printer. Treat the verifier as the pairing partner that cannot be argued with during the hour.

The verifier is short on purpose. A pairing hour cannot absorb a framework, and a long harness becomes another place for hidden retries. The script below is an example, not a production SLA, and it should be read as a fail-closed checklist.

# pairing/verify_session.py
"""Fail closed on a pairing session contract. Example verifier, not a production SLA."""

from __future__ import annotations

import json
import re
import sys
from pathlib import Path

import yaml

ROOT = Path(__file__).resolve().parents[1]
CONTRACT = ROOT / "pairing" / "session_contract.yaml"


def load_contract() -> dict:
    data = yaml.safe_load(CONTRACT.read_text(encoding="utf-8"))
    if not isinstance(data, dict) or data.get("schema_version") != 1:
        raise SystemExit("contract schema_version must be 1")
    return data


def check_budgets(data: dict) -> None:
    budgets = data["budgets"]
    if budgets["max_retries_per_call"] > 1:
        raise SystemExit("pairing hour forbids hidden retry amplification")
    if data["session"]["max_model_calls"] > 12:
        raise SystemExit("session max_model_calls looks like an unbounded loop")
    if budgets["max_prompt_chars"] > 8000:
        raise SystemExit("prompt budget is too large for a one-hour pairing block")


def check_invariants(data: dict) -> None:
    for item in data["invariants"]:
        path = ROOT / item["path"]
        if not path.exists():
            raise SystemExit(f"missing invariant path: {item['path']}")
        text = path.read_text(encoding="utf-8")
        pattern = item.get("forbid_regex")
        if pattern and re.search(pattern, text, re.I):
            raise SystemExit(f"{item['id']} matched forbidden pattern")
        if item.get("must_be_json_object"):
            parsed = json.loads(text)
            if not isinstance(parsed, dict):
                raise SystemExit(f"{item['id']} is not a JSON object")
        needle = item.get("must_contain")
        if needle and needle not in text:
            raise SystemExit(f"{item['id']} missing {needle!r}")


def main() -> int:
    data = load_contract()
    check_budgets(data)
    check_invariants(data)
    print(f"ok: {data['session']['id']}")
    return 0


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

A second example script gates any later HTTP call. The pair runs it instead of pasting a prompt while the verifier is red. The network section is omitted on purpose so the article does not invent an endpoint, a model name, or a quota.

# pairing/gate.py
"""Refuse to start a drafting call until the local contract passes."""

from __future__ import annotations

import subprocess
import sys
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]


def main() -> int:
    check = subprocess.run(
        [sys.executable, str(ROOT / "pairing" / "verify_session.py")],
        check=False,
    )
    if check.returncode != 0:
        print("gate: contract failed; do not call the model")
        return check.returncode
    print("gate: contract passed; drafting call is allowed by local policy")
    return 0


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

Numbered pairing hour

The senior kept the hour on a short checklist. The checklist is the workflow. The model is an optional printer inside step four.

  1. Create src/probe_helper.py with a run_probe( stub so the helper invariant has a real path.
  2. Write pairing/fixtures/sample_model_reply.json as an empty object, then replace it only with saved replies.
  3. Run the verifier and keep it red until the stub and fixture exist.
  4. Draft with the free model only after gate.py prints a pass.
  5. Save the model reply into the fixture path, run the verifier again, and stop on the first red check.
  6. End the hour at max_minutes or max_model_calls, even when the helper still looks unfinished.
python3 -m pip install pyyaml
printf '%s\n' 'def run_probe(url: str) -> dict:' '    return {"url": url}' > src/probe_helper.py
printf '%s\n' '{}' > pairing/fixtures/sample_model_reply.json
python3 pairing/verify_session.py
python3 pairing/gate.py
Enter fullscreen mode Exit fullscreen mode

The empty JSON object is a valid starting fixture because the contract asks for an object, not for a finished probe. A later reply that includes a bearer token pattern should fail no_secrets_in_prompts before anyone copies it into src/.

Decision table the pair left in the notes

The senior refused to keep a long retrospective. The table below is the entire decision record from the hour. It is meant to be copied into a pull request, not into a chat summary.

Approach tried during pairing What broke in the same hour Keep or drop
Commit the chat export Two retry limits, no schema Drop
Ask the model to restate rules Fluent omission of a stop condition Drop as source of truth
Store rules in CI comments No fail-closed path for secrets Drop
YAML contract plus local verifier Red on missing files and forbidden patterns Keep
Free endpoint as draft printer Useful only after the gate is green Keep as optional lane

The kept decision is narrow. The pair did not decide that free models are trustworthy. The pair decided that trust belongs in a file the verifier can reject.

Limitations and who should not use this

This protocol does not measure latency, quality, or cost, and it does not claim a ranking among assistants. It also does not create an availability contract for any free server. Teams that need a signed SLA, regional residency, or a dedicated review queue should not treat a free drafting lane as production infrastructure.

Do not put secrets, customer payloads, or regulated records into the fixture file or the prompt body. The forbid_regex check is a shallow net, not a secret scanner. People who cannot run Python locally, and teams that cannot keep the contract in source control, should not use this hour structure. Solo beginners who need an interactive tutorial more than a fail-closed file may find the ceremony heavier than the helper under review.

The approach also fails when the pair wants the model to own session state. If the desired workflow is "continue from yesterday's chat," this protocol will feel hostile, and that hostility is intended. A replaceable endpoint should not be the only memory the pair has.

Readers who already have the verifier passing locally can point the drafting step at a free model lane, including MonkeyCode's free model access and free server option, without moving the contract off disk.

Top comments (0)