Your remote agent does not share your disk. Every patch arrives from a workspace you cannot see. Merge it like a rumor until the trees match.
You treat the chat pane as a coworker at your desk. The pane is a second machine with a copied sketch. That sketch ages while you keep typing locally.
Think of two kitchens sharing one stained recipe card. You keep chopping on your own counter without pause. The other kitchen still reads yesterday's faded card.
This is an architecture review, not a prompt trick. You will name constraints and trace the moving bytes. You will tighten the contract instead of adding retries.
The constraint set stays small and sharp here. Your local git tree moves without asking permission. The remote context window cannot watch that motion.
The emitted patch then claims to span both worlds. Those three facts define the system you actually run. Ignore one of them and the merge still looks polite.
Latency is a hard constraint, not a small nuisance. While the model thinks, you switch branches or save. The returned hunks then target files you already rewrote.
The apply command may still report a clean success. That success is the architecture bug you missed. A clean apply can still write on a stranger tree.
Token budget is a constraint, not a prize. A free allotment still packs a truncated tree. Truncation decides which files the remote node never saw.
Invisible files become invented files in the patch. Those invented files become merged fiction after apply. The chat log will not admit the omission.
Trust is a constraint you keep ignoring on purpose. A free server is not another folder on your laptop. Source and secrets can leave in a single request.
You would not copy private env files onto a shared box. Do not pack those files into a remote prompt either. The contract below assumes you already stripped them.
Follow the bytes on the wire, not the chat log. Data starts in your index and your dirty worktree. You choose paths and a packer serializes them.
The remote model emits a patch from that pack. Your client applies the hunks onto the local tree. Tests run after that, or they quietly never run.
The path looks linear on a whiteboard diagram. It is a race between your commits and the model. Your tree hash can change under the in-flight request.
The model can cite a file that never entered the pack. The apply step can succeed on the wrong base. Polite output does not repair that miss.
Name every hop with a stable identifier you record. A request without a base tree hash is folklore. A patch without path checksums remains a guess.
Folklore should not merge into your main branch. Keep the hash before you keep the prose. The review starts with that order, not with style.
Decide the source of truth before the first remote call. The local HEAD is the only durable truth you have. The remote context is a cache with no invalidation.
Caches without invalidation are rumors with extra latency. You would not let a CDN define your database schema. Do not let a prompt cache define your next commit.
Here is a small contract to run before any remote call. Treat it as a labeled local helper, not a platform.
#!/usr/bin/env python3
"""Labeled example: workspace contract for a remote coding pass."""
from __future__ import annotations
import hashlib
import json
import subprocess
import sys
from pathlib import Path
MAX_BYTES = 120_000 # labeled local budget, not a vendor quota
def git_output(*args: str) -> str:
return subprocess.check_output(["git", *args], text=True).strip()
def file_digest(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()[:16]
def build_manifest(paths: list[Path]) -> dict:
head = git_output("rev-parse", "HEAD")
dirty = git_output("status", "--porcelain")
entries = []
total = 0
truncated = []
for path in paths:
if not path.is_file():
continue
raw = path.read_bytes()
total += len(raw)
if total > MAX_BYTES:
truncated.append(str(path))
continue
entries.append(
{
"path": str(path),
"sha256_16": file_digest(path),
"bytes": len(raw),
}
)
return {
"head": head,
"dirty": bool(dirty),
"files": entries,
"truncated": truncated,
"budget_bytes": MAX_BYTES,
}
def main() -> None:
paths = [Path(p) for p in sys.argv[1:]]
if not paths:
print("usage: workspace_contract.py <file> [file...]", file=sys.stderr)
sys.exit(2)
manifest = build_manifest(paths)
Path(".agent-manifest.json").write_text(json.dumps(manifest, indent=2))
print(json.dumps(manifest, indent=2))
if __name__ == "__main__":
main()
Run it only on files you believe the agent will see. Freeze that output beside the request you are about to send.
date -u +%Y-%m-%dT%H:%M:%SZ > .agent-started-at
python3 workspace_contract.py src/app.py src/db.py tests/test_app.py
git rev-parse HEAD
git status --porcelain
You now hold a base the remote node must honor. If it cannot speak to those hashes, drop the patch. You do not bargain with a ghost tree after that.
Measure drift with clocks you already own in git. Record request start time beside the manifest hash. If status porcelain changes, the request is already stale.
Chat text is not the interface you should trust. The unified diff is the real interface across hosts. Parse it like untrusted input from another machine.
A coworker in the same room still needs review. So does a patch that arrived from a free server. Physical distance does not make hunks more true.
A patch can rename a file you never packed. A patch can rewrite a lockfile from pure memory. A patch can apply with fuzz and still look tidy.
Tidiness on a diff is not a checksum of the base. You already know this from bad cherry-picks at work. Remote assist is cherry-pick without a known parent.
Gate the apply with the manifest you just saved. The next script is a labeled example you can copy. It refuses work that cannot name its recorded base.
#!/usr/bin/env python3
"""Labeled example: refuse patches that miss the recorded base."""
from __future__ import annotations
import hashlib
import json
import subprocess
import sys
from pathlib import Path
def current_digest(path: str) -> str:
return hashlib.sha256(Path(path).read_bytes()).hexdigest()[:16]
def main() -> None:
manifest = json.loads(Path(".agent-manifest.json").read_text())
patch_path = Path(sys.argv[1])
patch = patch_path.read_text()
head_now = subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip()
if head_now != manifest["head"]:
raise SystemExit(f"head moved: {manifest['head']} -> {head_now}")
if manifest["dirty"]:
porcelain = subprocess.check_output(["git", "status", "--porcelain"], text=True)
if porcelain.strip():
raise SystemExit("worktree still dirty; freeze or stash before apply")
for entry in manifest["files"]:
path = entry["path"]
if not Path(path).is_file():
raise SystemExit(f"packed file missing: {path}")
if current_digest(path) != entry["sha256_16"]:
raise SystemExit(f"base drifted for {path}")
mentioned = []
for line in patch.splitlines():
if line.startswith("+++ b/"):
mentioned.append(line[6:])
packed = {e["path"] for e in manifest["files"]}
unknown = [p for p in mentioned if p not in packed and p != "/dev/null"]
if unknown:
raise SystemExit(f"patch touches unpacked paths: {unknown}")
if manifest["truncated"]:
print("warning: remote node was blind to", manifest["truncated"])
subprocess.check_call(["git", "apply", "--check", str(patch_path)])
print("contract ok")
if __name__ == "__main__":
main()
python3 apply_gate.py incoming.patch
git apply incoming.patch
python3 -m pytest -q
If the gate fails, you do not soothe the model. You rebuild the manifest and send a smaller pack. You keep both trees honest before another call.
Silent truncation is the quiet killer in this loop. Your budget cuts db.py and the model invents schema. The patch applies on the files that still remained.
Tests then hit a column that never existed on disk. Green apply hid a blind spot in the packer. The failure lived in the envelope, not the assertion.
Dirty trees cause a different lie during the wait. Uncommitted edits sit on disk while you keep coding. The packer hashed a moment that already died locally.
Without a gate, hunks land on a stranger tree. Git will still shrug if the fuzz context matches. Matching context is not matching identity.
Interleaving finishes the mess on a busy afternoon. Two remote jobs can return out of order. Each patch looks sane against its private memory.
Together they rewrite the same function twice. The useful version is the one you already lost. Request ids exist to make that collision visible.
Tests will not save a missing failure oracle. An apply that succeeds is not a working product. Keep one command in the loop that can fail.
These bugs are coupling problems, not model moods. Architecture review starts at that coupling point. Prompt polish will not pin a moving HEAD.
A healthy loop is boring on purpose here. One request id, one base hash, one apply, one test. Anything concurrent belongs on another worktree or branch.
You can run this contract against any remote helper. The split gets sharper when inference leaves your laptop. That remote node is both the convenience and the risk.
MonkeyCode is one concrete instance of that split. Disclosure: This article was prepared as part of MonkeyCode's product outreach. It currently offers free model access for this workflow.
It also offers a ten million token allotment right now. A free server option sits beside that model access. Those facts matter because work leaves your disk.
The other node then proposes patches against a copy. Do not treat that node as a bigger laptop. Treat it as an untrusted compiler of incoming patches.
Your manifest is the visa for each request. Your tests remain the border check after apply. A free server also means you do not own the queue.
Delay is drift, and drift is why hashes exist. Free tokens without a base hash print fiction faster. Skip slogans and keep the envelope strict instead.
What you change next is the envelope, not the model. You stop sending whole repos as an unstructured vibe. You send a manifest, a budget, and one question.
You pin HEAD in the request body every time. You do not store that pin only in chat memory. Memory on the remote side is not your git object.
You add a request id to the patch filename. A name like req-9f3c.patch beats later archaeology. Refuse two patches that share no id and no hash.
You record truncated paths in the pull request body. Reviewers deserve to know the remote node was blind. Hidden truncation is how ghost trees survive review.
You keep secrets out of the pack with a boring scan.
rg -n "AKIA|BEGIN PRIVATE KEY|SECRET=" src tests || true
git ls-files -o --exclude-standard
You also split control plane from data plane here. Hashes, budgets, and request ids are control data. File bytes and diffs are the data plane payload.
Mixing both into one chat blob kills the contract. Replace the chat log with that pair next feature. Keep the model if it helps your actual task.
Change the envelope either way before you scale usage. More tokens will not fix an unnamed base. They will only accelerate the same race.
This helper does not authenticate any remote vendor. It does not prove the server later forgot your code. It does not replace a human reader of the domain.
Do not use it as a compliance story at work. If code cannot leave the building, skip the remote node. Stay on the laptop and keep the packer closed then.
Do not use it as a license to skip tests. The gate only proves the recorded base did not move. It proves nothing about behavior or ugly abuse cases.
Do not paste production secrets to exercise the sample. The byte budget in the script is only a label. It is not a published quota for every host plan.
Teams that need mandatory provenance need signed artifacts. A json manifest in the repo is not a signature. Build real attestation if that is your actual world.
Pick one messy file pair on this branch today. Build the manifest and send the bounded pack only. Run the apply gate before you run git apply.
If it fails, you learned the architecture in time. You avoided merging a rumor into the default branch. That refusal is the whole review paying rent.
The remote workspace will still drift again tomorrow. Your job is not to stop the clock on disk. Your job is to refuse a patch with no base.
If you already have a free remote node, use the gate. Run it on the next real diff before you merge. Keep the trees honest and the product in the background.
Top comments (0)