Maintainers merge evidence packets, not clever one-line diffs. A green local test still leaves scope and docs unchecked. This workflow freezes a contribution packet before any pull request.
Cheap model output makes patches easy to draft. It does not make them cheap to review. Agents also invent missing context and then patch the invention. The packet below blocks that habit with four numbered gates.
The example uses a fictional library named linebuf. The library should keep one trailing newline. It currently drops blank lines at end of file. The walkthrough is labeled as an unexecuted template, not a live claim.
What the packet must contain
Every outbound OSS change needs four artifacts on disk. They live beside the branch, not in chat history. Reviewers can clone the branch and replay the same files.
-
issue.mdwith the original report text. -
repro.mdwith exact commands and observed output. -
failing_test.patchcreated before production edits. -
review.jsonwritten after tests turn green.
Skip the packet and the model starts guessing. Guessing is how drive-by PRs waste maintainer time.
Gate 1: lock the reproduction
Do not open an editor on library code yet. Recreate the failure with a test the project already runs. The test file becomes the contract for later review.
git fetch origin
git switch -c issue-884-trailing-newline origin/main
mkdir -p tests/repro
Add a focused test that names the issue and the input. Keep the first commit limited to that file.
# tests/repro/test_issue_884_trailing_newline.py
from linebuf import normalize
def test_issue_884_keeps_single_trailing_newline():
raw = "alpha\nbeta\n"
assert normalize(raw).endswith("\n")
assert not normalize(raw).endswith("\n\n")
Run only that node before touching src/.
pytest tests/repro/test_issue_884_trailing_newline.py -q
# expected: 1 failed
git add tests/repro/test_issue_884_trailing_newline.py
git commit -m "test: lock issue 884 trailing newline repro"
A passing test at this gate means the report is already stale. Stop and re-read the issue. Do not invent a new bug for the model to solve.
Gate 2: lock the file scope
List nouns from the issue. Map each noun to one path. Reject extra files before they enter the index. Models pad diffs with helpers, renames, and drive-by lint.
printf '%s\n' 'normalize' 'trailing newline' 'end of file' > /tmp/issue-nouns.txt
git diff --name-only origin/main...HEAD > /tmp/changed.txt
Keep a hard allowlist for this issue. linebuf only needs the normalizer and the new test.
# /tmp/allowlist.txt
src/linebuf/normalize.py
tests/repro/test_issue_884_trailing_newline.py
A small checker fails the branch when the diff escapes that list.
# tools/scope_lock.py
from pathlib import Path
import sys
allow = set(Path("/tmp/allowlist.txt").read_text().splitlines())
changed = set(Path("/tmp/changed.txt").read_text().splitlines())
extra = sorted(p for p in changed if p and p not in allow)
if extra:
print("scope lock failed:")
print("\n".join(extra))
sys.exit(1)
print("scope lock passed")
Run it after every commit. Scope creep is a review reject, not a style nit.
Gate 3: lock the public contract
Green tests can still ship a silent API break. Gate 3 is a checklist, not a linter plugin. Fill it in review.json by hand first.
{
"issue": "884",
"public_signatures_changed": false,
"changelog_entry": true,
"issue_linked_in_pr_body": true,
"test_names_include_issue_id": true,
"docs_touched": false,
"notes": "normalize() return type unchanged"
}
Use this reject matrix when a box cannot be honest. The matrix is the original artifact for this account.
| Reject reason | Signal on the branch | Required packet fix |
|---|---|---|
| Missing reproduction | No failing test commit | Add Gate 1 commit first |
| Scope creep | Files outside allowlist | Revert extras, rerun scope lock |
| Silent API change | Signature or types shifted | Restore signature or version docs |
| Unlinked issue | PR body lacks Fixes #
|
Rewrite the PR template |
| Hidden behavior change | Changelog skipped | Add a one-line user note |
| Model-authored junk | Comments, renames, dead helpers | Delete and rerun tests |
Do not ask a model to tick these boxes from memory. Read git diff origin/main and the test output. Write the JSON only after that read.
Gate 4: second reader, never author
The patch already exists. Tests already pass. A model now reviews the packet against Gates 1-3. It does not propose new files. It does not rewrite the diff.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option. Those two facts are the only product claims used here. No model names, quotas, or hardware details are stated.
The second reader can run on that free model path when a paid API is not needed. The same prompt also runs in any local chat window. The workflow does not depend on one host.
Save this prompt as tools/second_reader.md. Treat it as a proposal, not a measured eval.
You are a maintainer, not a patch author.
Read issue.md, repro.md, git.diff, pytest.txt, and review.json.
Reject the packet if any gate fails.
Return only YAML with keys: verdict, failed_gates, maintainer_comment.
verdict must be accept or reject.
Do not suggest extra refactors.
Do not rewrite source.
Quote failing evidence with file paths.
Collect the inputs with a single script. The script writes a folder a human can attach.
# tools/build_packet.py
from pathlib import Path
import subprocess
import sys
root = Path("packet")
root.mkdir(exist_ok=True)
def run(cmd: list[str]) -> str:
p = subprocess.run(cmd, check=False, text=True, capture_output=True)
return (p.stdout or "") + (p.stderr or "")
(root / "git.diff").write_text(run(["git", "diff", "origin/main"]))
(root / "pytest.txt").write_text(
run([sys.executable, "-m", "pytest", "tests/repro", "-q"])
)
for name in ("issue.md", "repro.md", "review.json"):
src = Path(name)
if src.exists():
(root / name).write_text(src.read_text())
print(f"packet ready in {root.resolve()}")
Feed that folder to the second reader. Keep the YAML verdict next to the branch. A reject result is success. It saved a maintainer round trip.
Full loop on the fictional linebuf issue
The numbered path below is the whole tutorial. Each step produces a file the next step consumes.
- Copy the issue body into
issue.mdwithout edits. - Write
repro.mdwith install commands and one failing run. - Land the failing test as its own commit.
- Patch
src/linebuf/normalize.pyuntil that test passes. - Run the full suite, not only the new file.
- Run
tools/scope_lock.pyagainst the allowlist. - Fill
review.jsonfrom the reject matrix. - Run
tools/build_packet.pyand storepacket/. - Send
packet/through the second-reader prompt. - Open the PR only after
verdict: accept.
python tools/scope_lock.py
python tools/build_packet.py
# human reads packet/review.json before any model call
If Gate 4 rejects, fix the packet, not the prompt. Prompt iteration without new evidence creates fluent excuses. The files on disk remain the source of truth.
What this does not prove
The loop does not measure model quality. It does not claim merge-rate gains. It does not replace CI, CODEOWNERS, or human review. Maintainer taste still decides comments, commit splits, and release timing.
Free model access can omit repository context. Large monorepos will overflow a naive packet dump. Secret-bearing diffs must never leave the laptop. Embargoed security issues stay off hosted models.
Projects that forbid AI-assisted contributions should skip Gate 4. The first three gates still apply. They are ordinary engineering, not a product feature.
Who should not use this
Typo-only PRs do not need a four-file packet. First-time docs fixes should stay small. New contributors on tiny READMEs will feel blocked. Maintainers already drowning in process should not add YAML theater.
Teams without tests cannot lock Gate 1. They should add a reproduction script first. A model cannot invent a missing harness and stay honest.
Closing
Ship the packet, then ship the PR. The failing test commit is the real patch. Scope lock, contract JSON, and a second reader only confirm it. Readers can run tools/build_packet.py on any current issue; MonkeyCode free model access and the free server option are optional hosts for Gate 4 when a paid endpoint is not in budget.
Top comments (0)