DEV Community

Quinn Zhu
Quinn Zhu

Posted on

Lock the Ticket Contract Before the Agent Touches Code

You should not prompt an agent on day one. Write a human-owned contract first. Then let the agent touch implementation only.

Green tests mean little if the model wrote them. Your first ticket needs a check the agent cannot rewrite. If that check moves, you reject the branch.

Why first tickets go fake

You clone a repo on hour one. Slack drops a “small” bug on you. You paste the ticket into an agent and watch files move.

CI turns green. You still cannot explain the patch. That is not onboarding. That is a demo your teammates cannot review.

Speed is not the failure mode here. Unsigned checks are the failure mode. A junior who cannot defend the assertion should not merge the assertion.

What a ticket contract is

A ticket contract is a tiny failing check you type by hand. It encodes only the behavior the ticket names. It lives on a path the agent must never edit.

You hash that path before the session starts. You hash it again before you open the PR. A moved digest means the session is invalid.

Keep the contract boring. Use numbers from the ticket. Do not invent extra business rules.

The artifact you create first

Create three files before any prompt. Put them on your branch. Do not ask an agent to draft them.

1. A tight path allowlist

# .agent/ticket-allowlist.txt
src/billing/proration.py
tests/unit/test_proration_impl.py
Enter fullscreen mode Exit fullscreen mode

Implementation tests may change. The contract path may not. Five files is already too many for ticket one.

2. A human-owned contract test

# tests/contract/test_ticket_42_proration.py
"""Human-owned. Do not edit in an agent session."""
from billing.proration import prorate_cents


def test_partial_month_rounds_down_not_half_up():
    # Ticket 42: February, 10 days, $30 plan, USD cents.
    assert prorate_cents(3000, days_used=10, days_in_month=28) == 1071


def test_zero_days_is_zero():
    assert prorate_cents(3000, days_used=0, days_in_month=28) == 0
Enter fullscreen mode Exit fullscreen mode

Write the expected cents from the ticket. If the function is missing, the test must fail. That failure is your starting line, not a problem to hide.

3. A guard script you run locally

Label this as a local workflow. Run it on a throwaway branch first. Do not treat it as production policy until your team agrees.

#!/usr/bin/env bash
# scripts/guard-contract.sh
set -euo pipefail

ROOT="$(git rev-parse --show-toplevel)"
cd "$ROOT"

CONTRACT_DIR="tests/contract"
STAMP=".agent/contract.sha256"
ALLOW=".agent/ticket-allowlist.txt"
BASE="${AGENT_BASE:-origin/main}"

stamp() {
  mkdir -p .agent
  find "$CONTRACT_DIR" -type f | sort | xargs sha256sum > "$STAMP"
  echo "stamped $(wc -l < "$STAMP" | tr -d ' ') contract files"
}

check_hash() {
  tmp="$(mktemp)"
  find "$CONTRACT_DIR" -type f | sort | xargs sha256sum > "$tmp"
  if ! diff -u "$STAMP" "$tmp"; then
    echo "contract files changed; reject this session" >&2
    rm -f "$tmp"
    exit 1
  fi
  rm -f "$tmp"
  echo "contract hash ok"
}

check_paths() {
  if [[ ! -f "$ALLOW" ]]; then
    echo "missing $ALLOW" >&2
    exit 1
  fi
  fail=0
  while IFS= read -r f; do
    [[ -z "$f" ]] && continue
    if grep -Fxq "$f" "$ALLOW"; then
      continue
    fi
    echo "blocked path: $f" >&2
    fail=1
  done < <(git diff --name-only "$BASE"...HEAD)
  if [[ "$fail" -ne 0 ]]; then
    exit 1
  fi
  echo "path allowlist ok"
}

case "${1:-}" in
  stamp) stamp ;;
  check) check_hash ;;
  paths) check_paths ;;
  test) python -m pytest tests/contract -q ;;
  *) echo "usage: $0 {stamp|check|paths|test}" >&2; exit 2 ;;
esac
Enter fullscreen mode Exit fullscreen mode

Make it executable once. Keep the stamp file in git so review can see it.

chmod +x scripts/guard-contract.sh
git checkout -b ticket-42-proration
# write the three files, then:
./scripts/guard-contract.sh stamp
./scripts/guard-contract.sh test   # expect fail until impl exists
Enter fullscreen mode Exit fullscreen mode

First-hour sequence

Follow these steps in order. Do not skip the fail.

  1. Read the ticket out loud. Circle every number.
  2. Create tests/contract for those numbers only.
  3. Run the contract test. Confirm it fails for the right reason.
  4. Stamp the hash before you open an editor for the agent.
  5. Write the allowlist. Keep it under five paths.
  6. Prompt with paths and the failing test name. Do not say “update tests as needed.”
  7. Re-run check, paths, and test on your machine.
  8. Open the PR with the template below.
  9. If review calls the contract weak, you rewrite it.
  10. If the agent mutated the contract, reset the branch.

A useful prompt stays narrow. Paste paths, not permission.

Implement prorate_cents in src/billing/proration.py so
tests/contract/test_ticket_42_proration.py passes.
Do not edit tests/contract or any path outside:
- src/billing/proration.py
- tests/unit/test_proration_impl.py
Do not add dependencies. Do not rewrite git history.
Enter fullscreen mode Exit fullscreen mode

Then run the guard again. Do not trust the agent's summary.

./scripts/guard-contract.sh check
./scripts/guard-contract.sh paths
./scripts/guard-contract.sh test
git diff --stat origin/main...HEAD
Enter fullscreen mode Exit fullscreen mode

First PR body you can defend

Your PR description is part of the contract. Fill it before you request review.

## Ticket
42 — February proration rounds down in cents.

## Human contract
- tests/contract/test_ticket_42_proration.py (hash-locked)
- stamp: .agent/contract.sha256

## Agent-touched paths
- src/billing/proration.py
- tests/unit/test_proration_impl.py

## Commands I ran
- ./scripts/guard-contract.sh check
- ./scripts/guard-contract.sh paths
- ./scripts/guard-contract.sh test

## What I can explain
- 10/28 of 3000 cents is 1071 after truncating toward zero
- zero days returns zero

## Rollback trigger
Reset this branch if the contract digest moved.
Enter fullscreen mode Exit fullscreen mode

If you cannot complete the “What I can explain” section, you are not ready. Close the PR. Do not ask the agent to write that section for you.

Where a free agent loop fits

You still need an implementation loop after the stamp. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is an open-source coding assistant with free model access and a free server option. Point it at allowlisted files only. Keep the contract on your side of the line.

The product does not replace the hash check. It does not prove the contract is correct. It only writes candidate implementation under a path fence you already set.

Decision table for hour one

Signal Your action
Contract test never failed You wrote a tautology. Rewrite it.
Agent edited tests/contract Reset the branch. Do not diff-fix.
Digest matches, test still red Do not widen the allowlist.
Extra files in git diff Restore those paths, then restamp nothing.
Review says the contract is weak You edit it. Then stamp again.
You cannot explain the cents Do not merge. Ask a human.

Reset is cheaper than a second agent pass. Use it.

git checkout -- tests/contract
git reset --hard origin/main
# recreate the contract by hand, then stamp again
Enter fullscreen mode Exit fullscreen mode

Do not “fix forward” with a broader prompt. Broader prompts hide the first mistake.

What this does not prove

A locked hash does not prove the ticket was understood. You can stamp a weak contract. Reviewers still need to read the numbers.

A passing contract does not prove the rest of billing is safe. Adjacent functions can still break. Run the repo’s normal suite after the guard.

An allowlist does not stop leaked secrets in prompts. Do not paste .env files. Do not paste production dumps into the session.

This workflow also fails open if you skip paths. The hash only watches tests/contract. It will not see a rewritten README unless you block that path.

Who should not use this

Skip this gate for production incident patches with a human driver. Skip it for throwaway spikes with no merge target. Skip it when the repo has no test runner you can invoke locally.

Staff engineers pairing on a known module may not need the stamp. Juniors on hour one do. If you cannot name the files, you are not ready for an agent.

Do not use this as cover for merging code you did not read. The contract is a floor. It is not a review substitute.

Close the first ticket cleanly

Your first merge should be small and explainable. The agent may type the implementation. You own the check that made it fail, then pass.

Stamp. Allowlist. Prompt. Guard. Explain. That is the whole loop. If any step needs a story, the branch is not ready.

If you want a free implementation loop behind that fence, try MonkeyCode’s free model access and free server on allowlisted paths only. Leave the contract human-owned.

Top comments (0)