DEV Community

Taylor Lin
Taylor Lin

Posted on

Name the Writable Surface Before You Accept the Diff

Consider a composite review incident, not a log from this account. A billing test failed in the afternoon. The assertion wanted 3 and got 4. The task note asked for a fix and a short pull-request comment.

A session summary came back green. The patch file lived on a scratch host the reviewer had not logged into. The default branch had not moved. CI on the real repository was still red.

That gap is a placement error. A model can draft a change. A remote machine can apply it.

Neither event names the surface that was allowed to change. Until that name exists, a green sentence is a chat verdict, not evidence you can merge on.

What follows is a glossary and a four-branch tree. Each leaf has a command sequence and a binding record. Treat both as a specification you can run locally. They are not timings, and they are not results captured from a vendor session.

Freeze the terms

Writable surface. The path, worktree, or user@host plus branch that this task may mutate. "The agent" is not a surface. If you cannot point at a directory or a host, you cannot review the write.

Scratch host. A machine you can delete without a migration plan. It may format code and emit a patch. Its disk is not the source of truth for the repository you ship.

Promotion host. The runner whose output you will quote in review. That is often CI. It is sometimes the laptop. It is never whichever host printed a zero most recently.

Mutation class. A label set before the first tool call. The allowed values are read_only, local_draft, remote_scratch, and publish. The class is a constraint. The model does not get to upgrade it mid-session.

Binding. The record that joins task id, mutation class, surface, and forbidden paths. An unbound task has no acceptable write, even when the diff looks small and correct.

Chat verdict. Prose in the session that says done, fixed, or passed. It can be long and still describe a different machine from the one you merge from.

Bound-host transcript. Output captured on the named surface. It includes a hostname or worktree path, a git revision, the exact command, and the exit code. A paragraph pasted without that frame is still a chat verdict.

Promotion check. The acceptance command, run on the promotion host, after the patch has arrived there. A zero on the scratch host does not travel with the diff file.

Four steps before any model call

  1. Write the mutation class into the task file before the prompt. A class chosen after a green summary is a story about a decision. It is not the decision.
  2. Name the surface as a local path or as ssh:host. Add the branch name if a push is conceivable, including a push you hope to avoid.
  3. List forbidden paths. Start with .env, private key directories, credentials.json, and the default branch names main and master.
  4. Name the promotion check: command plus host. If either half is missing, force the class to read_only and stop.

Add one line for copy direction. Scratch to promotion is allowed for a patch file. Promotion secrets must not move to scratch, even as context for a smarter edit.

The placement tree

Read the class, then enter one leaf. Do not blend leaves in a single session. If the work changes class, stop, write a new binding, and start a new session. Leftover tool permission from the old class is how a read-only review becomes a push.

Leaf A — read_only

Enter here when the task asks for an explanation, a review note, or a plan. No repository file should change.

Worked example. billing/test_proration.py fails with AssertionError: expected 3, got 4. You want a hypothesis about integer division. You do not want an edit while the file is open.

Binding:

{"task_id":"BILL-184","mutation_class":"read_only","surface":"","branch":"","paths":[]}
Enter fullscreen mode Exit fullscreen mode
git status --short
Enter fullscreen mode Exit fullscreen mode

Record an empty status, or only the dirty paths you already owned before the session. Allow a markdown note in the ticket. Reject any diff the session offers to apply.

If status is dirty afterward with new paths, the binding broke. Restore those paths with git checkout -- on the specific files, or remove the untracked ones. Do not keep them because they look related to the failure.

Quote the ticket note and your status output. Do not quote a claimed test run. No test run was authorized on this leaf.

Leaf B — local_draft

Enter here when a code change would help, secrets must stay on the laptop, and nothing should be pushed.

Worked example. Isolate a worktree. Let the draft land only there. After the session ends, run the promotion check yourself in that worktree.

Binding:

{"task_id":"BILL-184","mutation_class":"local_draft","surface":"worktree:../proration-draft","branch":"draft/proration-note","paths":["billing/test_proration.py"]}
Enter fullscreen mode Exit fullscreen mode
git worktree add ../proration-draft -b draft/proration-note
cd ../proration-draft
git status --short
git diff --stat HEAD
python -m pytest billing/test_proration.py -q
echo "local_exit=$?"
Enter fullscreen mode Exit fullscreen mode

The local_exit line belongs in the transcript. A chat line that says the tests passed is not a substitute. If the exit code is non-zero, keep the worktree and stop. Do not push the branch so CI can take a look. That push is Leaf D, and it needs its own binding.

Leaf C — remote_scratch

Enter here when a remote loop is useful, the worktree contains no production secrets, and a remote success will not count as merge evidence.

Worked example. A disposable server checks out a mirror and may edit it. You copy back a patch and a checksum. The promotion host applies that patch and runs the check.

Binding:

{"task_id":"BILL-184","mutation_class":"remote_scratch","surface":"ssh:scratch.example","branch":"","paths":["billing/test_proration.py"]}
Enter fullscreen mode Exit fullscreen mode

On the scratch host, in your own SSH session, after the agent stops:

  1. Record hostname and git rev-parse --short HEAD.
  2. Write the diff to a patch file.
  3. Compute sha256sum and copy that file back yourself.
  4. Do not copy the session summary back as if it were step 3.
hostname
git rev-parse --short HEAD
git diff > /tmp/proration.patch
sha256sum /tmp/proration.patch
Enter fullscreen mode Exit fullscreen mode

On the promotion host, in a clean worktree:

sha256sum proration.patch
git apply --check proration.patch
git apply proration.patch
python -m pytest billing/test_proration.py -q
echo "promo_exit=$?"
Enter fullscreen mode Exit fullscreen mode

Quote promo_exit only when the checksums match. If they differ, stop. You are not holding the bytes the scratch host produced.

If git apply --check exits non-zero, the patch does not match this tree. Do not retry with --reject. Do not switch to a forced three-way merge to see what sticks. Record the exit code, keep the patch, and open a new local_draft binding only if a person still wants the change.

A zero on the scratch host plus a non-zero on the promotion host means the chat verdict described the wrong surface. That split is the tree working.

Leaf D — publish

Enter here only when a non-default branch may move, the remote is named in the binding, and CI on the promotion host is the promotion check.

Binding:

{"task_id":"BILL-184","mutation_class":"publish","surface":"ssh:git.example","branch":"bot/proration-note","paths":["billing/test_proration.py"]}
Enter fullscreen mode Exit fullscreen mode
git checkout -B bot/proration-note
git push -u origin bot/proration-note
git status -sb
Enter fullscreen mode Exit fullscreen mode

The status line must show bot/proration-note, not main or master. Open a pull request if that remote allows it. Merge waits on the named CI job.

An agent message that the branch is green does not end the wait. If the branch field is empty, main, or master, this leaf is closed. Reclassify as local_draft, or stop.

Decision table

Class Surface shape Push Quote in review
read_only none no ticket note and git status
local_draft local worktree path no local_exit from your pytest
remote_scratch ssh: host no promo_exit after checksum match
publish named remote, non-default branch that branch only named CI job

If the Quote cell cannot be filled with a command you run, or a CI job you already configured, do not enter that row.

A classifier you can execute

surface_bind.py is a proposal you can run on your laptop. It encodes the four branches and the denial cases. It does not call a model. Nothing in this article is a benchmark of that script.

"""surface_bind.py — specification, not a measured run."""
from __future__ import annotations

import json
import sys

CLASSES = ("read_only", "local_draft", "remote_scratch", "publish")
FORBIDDEN = {".env", ".ssh", "credentials.json"}
DEFAULT_BRANCHES = {"", "main", "master"}

def decide(task: dict) -> dict:
    kind = task.get("mutation_class")
    surface = task.get("surface") or ""
    branch = task.get("branch") or ""
    paths = set(task.get("paths") or [])
    if kind not in CLASSES:
        return {"allow": False, "reason": "unknown_class"}
    if paths & FORBIDDEN:
        return {"allow": False, "reason": "forbidden_path"}
    if kind == "read_only":
        return {"allow": True, "write": False, "quote": "status_only"}
    if not surface:
        return {"allow": False, "reason": "unbound_surface"}
    if kind == "local_draft":
        if surface.startswith("ssh:"):
            return {"allow": False, "reason": "local_class_on_remote"}
        return {"allow": True, "write": True, "quote": "local_pytest"}
    if kind == "remote_scratch":
        if not surface.startswith("ssh:"):
            return {"allow": False, "reason": "scratch_requires_remote"}
        return {"allow": True, "write": True, "quote": "promotion_host"}
    if branch in DEFAULT_BRANCHES:
        return {"allow": False, "reason": "default_branch"}
    return {"allow": True, "write": True, "quote": "ci_on_promotion_host"}

def main() -> int:
    task = json.loads(sys.stdin.read())
    result = decide(task)
    print(json.dumps(result, sort_keys=True))
    return 0 if result["allow"] else 2

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

Unexecuted test plan. Pipe one JSON object at a time. Check reason or quote. Run the denials first.

printf '%s\n' '{"mutation_class":"publish","surface":"ssh:git.example","branch":"main","paths":["billing/test_proration.py"]}' | python surface_bind.py
Enter fullscreen mode Exit fullscreen mode

Expected denial: reason is default_branch, and the process status is 2. Next, send local_draft with surface ssh:scratch.example. Expected reason: local_class_on_remote.

Then send remote_scratch with surface worktree:../proration-draft. Expected reason: scratch_requires_remote. Then send the Leaf B binding above. Expected allow, with quote equal to local_pytest.

Add a path of .env to any otherwise valid object. Expected reason: forbidden_path. If your copy allows the main publish, the default-branch check was removed. Do not use that copy as a gate.

How to file a denial

The reason strings are routing signals. unknown_class means the task file was skipped. Fill it in. Do not open a model session to discover the class.

forbidden_path means the path list already names a secret-shaped file. Drop that path, or move the secret out of the tree, before any remote leaf. unbound_surface means a write class has nowhere to land.

local_class_on_remote and scratch_requires_remote mean the class and the surface disagree. default_branch means publish was aimed at the branch you merge to.

Write the reason next to the task id in the same file you used for the binding. A later reviewer should see the denial without scrolling a chat. That file is the artifact. The session summary is optional context.

Where free model access and a free server sit

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

For this draft, the operator supplied two availability claims about MonkeyCode: free model access, and a free server option. No model name, token quota, hardware shape, or time limit is stated here. None of those details was verified for publication, and a stale number would be worse than a gap.

Place the two claims on the tree, and nowhere else. Free model access is enough for Leaf A, and for drafting text inside Leaf B. A free server can be the scratch host in Leaf C.

A zero invoice does not promote that host into CI. Leaf D still needs a named remote and a CI job you configured before the session. If a console you open later shows a different limit, keep the binding. Do not change the mutation class to match a plan page.

Use the model to propose bytes. Use the scratch host to emit a patch and a checksum. Use the promotion host for the exit code you will quote. Remove any one of those three, and the session summary is doing a job it cannot do.

Limits

The script checks names, not file contents. A path called notes.txt can still hold a secret. The tree does not prove the scratch disk was empty when the session started.

A hostname inside a log you did not capture over your own SSH session can be written by anyone. git apply plus one pytest target will miss behavior that target does not exercise. A task that must update two remotes does not fit one binding. Split it, then run the four steps again on each half.

Who should not use this

Skip Leaf C and Leaf D when the worktree can see production credentials, customer records, or unpublished keys. Skip Leaf D when you cannot name the CI job that will run the promotion check.

Skip the article entirely if you need a model comparison. It does not measure quality, latency, or price. Those numbers need your own runs and primary sources, not a glossary.

If a disposable server and a model call are already available to you, write the mutation class into the task file before the first prompt. The diff can wait until the surface has a name.

Top comments (0)