DEV Community

Quinn Zhu
Quinn Zhu

Posted on

New Hire Rule: Cap Your First AI PR With Median Merges

Your first AI pull request should lose to recent merges.
Measure main before any agent edits a file.

An agent will happily touch twenty unrelated paths.
Your teammates will not review that on day one.
The last five merges already show house style.

The rule

Treat recent merges as a hard size contract.
Your first PR must stay under that contract.
If the agent exceeds it, you stop and split.

This check is not a vague vibe test.
It is a git measurement you can rerun.
Finish it in hour one, before the first prompt.

Why recent merges beat model confidence

The model did not join your onboarding call.
It did not watch your team's review comments.
It only sees files and one ticket sentence.

Juniors fail here in a predictable pattern.
They paste the whole ticket into an agent.
The agent then "finishes" work across six packages.

Reviewers reject that PR for scope, not bugs.
You burn a day defending generated noise.
Rollback hurts because the diff has no center.

Recent merges tell the opposite, quieter story.
They show typical file counts and path prefixes.
They show whether tests land beside production code.

Copy those bounds instead of the agent's plan.
Your first week is for a reviewable shape.
Heroic generated patches can wait until week two.

What you capture in hour one

You need four facts, not an architecture tour.

  1. A baseline tag on a clean tree.
  2. Median files changed across five merges.
  3. Median insertions and deletions from those merges.
  4. Path prefixes that actually appear in those merges.

Write the facts to a file you control.
The agent never edits that budget file.
You own the numbers and the rollback tag.

Artifact: derive a first-PR budget from git

The scripts below are a proposed local workflow.
Run them on a throwaway clone first.
Read each command before you execute it.

Step 1: Tag a baseline

Start from a clean worktree on the default branch.

git fetch origin
git switch -C onboarding origin/main
git status --porcelain
test -z "$(git status --porcelain)" || exit 1
git tag -f onboarding-baseline
git rev-parse --short onboarding-baseline
Enter fullscreen mode Exit fullscreen mode

You now have a named tree you can restore.
Hour one is incomplete without that tag.
Do not start the agent before the tag exists.

Step 2: Sample five first-parent commits

Use merge commits when the team actually merges.
Fall back to first-parent history when they squash.

# Proposed: inspect what you are about to sample
git log --first-parent --merges origin/main -n 5 --pretty=format:'%h %s'

# If that list is empty, sample regular commits
git log --first-parent origin/main -n 5 --pretty=format:'%h %s'
Enter fullscreen mode Exit fullscreen mode

Look at the subjects before you trust the sample.
Drop a release train or a repo-wide rename.
Keep five boring feature merges if you can.

Step 3: Write the budget file

Save this helper as scripts/first_pr_budget.py.
It is labeled proposed because you must review it.
It only reads git and prints JSON to stdout.

#!/usr/bin/env python3
"""Proposed: median first-PR budget from first-parent commits."""
from __future__ import annotations

import json
import subprocess
import sys
from collections import Counter
from pathlib import Path

IGNORE_SUFFIXES = (".lock", "-lock.json", ".sum")
IGNORE_NAMES = {"package-lock.json", "yarn.lock", "pnpm-lock.yaml", "Cargo.lock"}


def git(*args: str) -> str:
    return subprocess.check_output(["git", *args], text=True)


def median(nums: list[int]) -> int:
    if not nums:
        return 0
    s = sorted(nums)
    n = len(s)
    if n % 2:
        return s[n // 2]
    return (s[n // 2 - 1] + s[n // 2]) // 2


def prefixes_for(path: str) -> str:
    parts = Path(path).parts
    if len(parts) >= 2:
        return f"{parts[0]}/{parts[1]}/"
    return path


def numstat(hash_: str) -> tuple[int, int, int, list[str]]:
    files = ins = dels = 0
    prefixes: list[str] = []
    for line in git("show", "--numstat", "--format=", hash_).splitlines():
        parts = line.split("\t")
        if len(parts) != 3:
            continue
        added, removed, path = parts
        name = Path(path).name
        if name in IGNORE_NAMES or path.endswith(IGNORE_SUFFIXES):
            continue
        if added == "-" or removed == "-":
            continue
        files += 1
        ins += int(added)
        dels += int(removed)
        prefixes.append(prefixes_for(path))
    return files, ins, dels, prefixes


def main() -> None:
    range_ = sys.argv[1] if len(sys.argv) > 1 else "origin/main"
    n = int(sys.argv[2]) if len(sys.argv) > 2 else 5
    hashes = [
        h for h in git("log", "--first-parent", range_, f"-n{n}", "--pretty=%H").split()
        if h
    ]
    file_counts: list[int] = []
    insertions: list[int] = []
    deletions: list[int] = []
    prefix_counter: Counter[str] = Counter()
    for hash_ in hashes:
        files, ins, dels, prefixes = numstat(hash_)
        file_counts.append(files)
        insertions.append(ins)
        deletions.append(dels)
        prefix_counter.update(prefixes)
    top_prefixes = [p for p, _ in prefix_counter.most_common(3)]
    print(
        json.dumps(
            {
                "range": range_,
                "sample_size": len(hashes),
                "max_files": median(file_counts),
                "max_insertions": median(insertions),
                "max_deletions": median(deletions),
                "path_prefixes": top_prefixes,
                "baseline_tag": "onboarding-baseline",
            },
            indent=2,
        )
    )


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

Run it against origin/main and read the result aloud.

python3 scripts/first_pr_budget.py origin/main 5 > first-pr-budget.json
cat first-pr-budget.json
Enter fullscreen mode Exit fullscreen mode

If max_files looks like a migration, resample.
Remove that hash from your mental sample set.
Rerun until the median looks like normal team work.

Step 4: Enforce the budget before you push

Save this check as scripts/check_first_pr_budget.py.

#!/usr/bin/env python3
"""Proposed: compare worktree diff against first-pr-budget.json."""
from __future__ import annotations

import json
import subprocess
import sys


def git(*args: str) -> str:
    return subprocess.check_output(["git", *args], text=True)


def main() -> None:
    budget_path = sys.argv[1] if len(sys.argv) > 1 else "first-pr-budget.json"
    base = sys.argv[2] if len(sys.argv) > 2 else "onboarding-baseline"
    budget = json.loads(open(budget_path, encoding="utf-8").read())
    names = [n for n in git("diff", "--name-only", base).splitlines() if n]
    ins = dels = 0
    for line in git("diff", "--numstat", base).splitlines():
        parts = line.split("\t")
        if len(parts) != 3 or parts[0] == "-" or parts[1] == "-":
            continue
        ins += int(parts[0])
        dels += int(parts[1])
    failed = False
    if len(names) > budget["max_files"]:
        print(f"FAIL files {len(names)} > {budget['max_files']}")
        failed = True
    if ins > budget["max_insertions"]:
        print(f"FAIL insertions {ins} > {budget['max_insertions']}")
        failed = True
    if dels > budget["max_deletions"]:
        print(f"FAIL deletions {dels} > {budget['max_deletions']}")
        failed = True
    allowed = budget.get("path_prefixes") or []
    if allowed:
        for name in names:
            if not any(name.startswith(prefix) for prefix in allowed):
                print(f"FAIL path outside prefixes: {name}")
                failed = True
    if failed:
        sys.exit(1)
    print("PASS first-PR budget")


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

Run the check every time the PR feels finished.

python3 scripts/check_first_pr_budget.py first-pr-budget.json onboarding-baseline
Enter fullscreen mode Exit fullscreen mode

A failed check is a split, not a debate.
Move leftover files onto a second branch.
Your first PR stays inside a reviewable shape.

Do not encode prefixes until they look stable.
Wrong prefixes will block legitimate test files.
Keep that rule in comments for the first day.

Decision table for hour one

Use this table before you type a prompt.

Signal from the last five merges First PR move First rollback move
Median files are 1–4 Keep the agent in one package Restore the baseline tag if extra packages appear
Median files are 5–12 Cap the ticket to one behavior Revert path by path from the tag
One sample is a lockfile bump Drop that sample and rerun Do not revert unrelated lockfiles
Prefixes disagree across merges Skip prefix fail; keep size caps Restore only files you touched
Sample includes CI or infra Forbid those prefixes in the prompt Checkout CI files from the tag

Write your chosen row into the PR body.
Reviewers then see the contract you copied.
They are not guessing why the patch is small.

First PR: stay inside the budget

You now have numbers from real history.
Use them as the only size argument.

Tell the agent the budget inside the prompt.

Ticket: PAY-1843 add retry on 429 for the billing client.
Max files: 4.
Max insertions: 80.
Max deletions: 20.
Allowed prefixes: src/billing/ tests/billing/
Do not edit CI, lockfiles, or docs.
Stop when the budget would break.
Return the file list before you edit.
Enter fullscreen mode Exit fullscreen mode

If the agent returns twelve files, you stop.
You revert the extra paths yourself.
Then you rerun the budget check until it passes.

Name the PR after the ticket, not the model.
Keep the description inside the measured bounds.
Reviewers should recognize a familiar team shape.

First rollback: reset to the baseline tag

Your rollback target is the hour-one tag.
It is not a guessed reset from memory.

If the branch is still local and unpushed, restore it.

git switch -C recovery onboarding-baseline
git status --porcelain
git diff --stat onboarding-baseline
Enter fullscreen mode Exit fullscreen mode

If you already pushed the first PR branch, isolate the undo.

git fetch origin
git switch your-first-pr-branch
git checkout onboarding-baseline -- src/billing/
git status --short
Enter fullscreen mode Exit fullscreen mode

Practice the unpushed path once on a junk branch.
Confirm git diff onboarding-baseline is empty after recovery.
That empty diff is the whole point of hour one.

Do not delete the tag until the first PR merges.
The tag is your personal undo, not a release.
Teammates do not need to adopt the tag name.

Where a free model still helps

Git will not write the paragraph reviewers want.
You still need a one-page contract from the JSON.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Paste first-pr-budget.json and the five merge subjects into a model.
Ask for a short first-PR contract in plain English.
MonkeyCode's free model access and free server option can draft that note while the scripts stay local.
The model may summarize. It may not raise your budget.

Limitations

Medians lie when history is mixed.
A release week will inflate every number.
A docs-only week will shrink them too far.

Lockfiles and generated code wreck raw numstat.
Filter them before you trust max_insertions.
If you cannot name those files, delay the agent.

This workflow does not prove the change is correct.
It only caps blast radius for a junior first PR.
Tests, types, and review still sit on you.

Squash culture hides real merge boundaries.
First-parent sampling is an approximation, not truth.
Ask a teammate which five commits look normal.

Prefix guards fail on monorepos with shared tests.
Size caps still help when prefixes do not.
Do not pretend the JSON is a complete policy.

Who should skip this

Skip this if you already own the module.
Skip this if the ticket is an incident hotfix.
Skip this if main has fewer than five useful commits.

Skip this if your change is a one-line config.
A budget check would only add ceremony there.
Keep the baseline tag anyway; it is cheap.

Close

Hour one is measurement, not generation.
The first PR is a capped, familiar diff.
The first rollback is a tag, not a story.

Copy recent merges on your first day.
Leave the wide generated patch for later.

Top comments (1)

Collapse
 
marcusykim profile image
Marcus Kim

Splitting the 429 retry example just to stay below 80 insertions could make the first PR harder to review if its tests land separately. I'd use the median from five recent merges to flag scope for discussion, while keeping the behavior and its tests together. Unrelated cleanup is a good second PR; the evidence that the retry works belongs with the change.