The public surface of your repository is the cutover artifact you should freeze before any paid agent seat expires. Chat logs and clever prompts will not tell you whether the next host renamed an exported function overnight. A snapshot of CLI flags, HTTP paths, and Python exports gives you a meter that survives the vendor tab. This diary walks you through packing that snapshot, gating applies against it, and recording leftovers the product still owns.
Why the leftover is a surface map, not another transcript
Paid coding agents like to touch whatever file seems related to the latest prompt you typed. That habit is tolerable while a review UI sits between the model and your default branch. It becomes expensive when you move the loop onto a free host that writes straight into the tree. You need a frozen map of names callers already depend on, stored next to the code, not in a chat.
Informal memory fails as soon as the model generation changes under the same product name. A green unit test can still hide a renamed CLI flag that a shell script in another repository calls. The snapshot does not claim the agent wrote good code, and it should not be asked to. It only claims that public names did not vanish unless you already said they could vanish.
What you freeze, and what you ignore
Capture only names a downstream caller could import, invoke, or hit without opening a private helper. Leave private functions, test doubles, and generated caches out of the snapshot so noise does not hide breaks. Record the extractor command beside the snapshot so a later machine can rebuild the same map. If you cannot name that command, you do not have a snapshot yet, only a feeling about stability.
Do not freeze entire files as the surface, because a file-level hash will fire on comments. You will start ignoring the gate after a week of comment-only failures on otherwise harmless patches. Do not freeze vendor tool names either, because those names die with the paid subscription. The finished pack should make sense to a teammate who never opened the paid product.
Cutover plan
Work through these steps before you cancel, because the old review UI will not export this map for you. Keep the outputs in git so the destination host cannot quietly rewrite the contract during the first week. If a step fails, you still have a paid review screen, which is the only reason to wait.
- Walk the last merged agent patches and write down every public name those patches were allowed to change.
- Run the extractor on the current default branch and commit the JSON snapshot beside the code.
- Install the apply gate as a local command that reads unified diffs before any host writes files.
- Replay one known-good patch and one known-bad rename so you trust the gate before canceling.
- Move generation to the free host only after the gate has failed a rename you planted by hand.
- File a leftovers note for vendor-side refactors you never extracted, because those names are already gone.
Stop at step four if the planted rename still applies cleanly, and do not cancel while that hole remains. You do not have a gate yet, and a free host will not invent one. Fix the extractor or the regex before you spend another hour talking to any model.
Extractor you can run tonight
The script below is a proposal for Python modules that declare public names at module scope. Treat this extractor as unexecuted until you run it against your own repository tree. It ignores names that start with an underscore, which is a convention, not a security boundary.
#!/usr/bin/env python3
"""Dump a coarse public-surface snapshot. Proposal: adapt allowlists before you trust it."""
from __future__ import annotations
import ast
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "fixtures" / "public-surface.json"
SKIP_PARTS = {".git", ".venv", "venv", "node_modules", "fixtures", "tests"}
def is_skipped(path: Path) -> bool:
return any(part in SKIP_PARTS for part in path.parts)
def public_functions(path: Path) -> list[str]:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
names: list[str] = []
for node in tree.body:
if isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id == "__all__":
if isinstance(node.value, (ast.List, ast.Tuple)):
for elt in node.value.elts:
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
names.append(elt.value)
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
if not node.name.startswith("_"):
names.append(node.name)
return sorted(set(names))
def main() -> int:
surface: dict[str, list[str]] = {}
for path in ROOT.rglob("*.py"):
if is_skipped(path):
continue
rel = str(path.relative_to(ROOT))
names = public_functions(path)
if names:
surface[rel] = names
OUT.parent.mkdir(parents=True, exist_ok=True)
payload = {
"extractor": "scripts/snapshot_public.py",
"files": surface,
}
OUT.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
print(f"wrote {OUT} files={len(surface)}")
return 0
if __name__ == "__main__":
sys.exit(main())
mkdir -p scripts fixtures
python3 scripts/snapshot_public.py
git add fixtures/public-surface.json scripts/snapshot_public.py
git diff --cached -- fixtures/public-surface.json | head
You should commit the snapshot on a quiet branch that no coding agent may push. You want the first diff after cutover to be about product names, not about formatter churn. Tag that commit so you can reset the snapshot without digging through agent noise later.
Apply gate that reads the snapshot
The apply gate is the leftover the paid review UI used to give you for free. It reads a unified diff on stdin and then refuses patches that drop frozen snapshot names. It also writes a JSON verdict you can store beside the case for later audit. You should treat the following script as a local tool, not as a hosted policy engine.
#!/usr/bin/env python3
"""Refuse diffs that remove frozen public names. Proposal for a local apply gate."""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SNAP = json.loads((ROOT / "fixtures" / "public-surface.json").read_text(encoding="utf-8"))
FILE_RE = re.compile(r"^diff --git a/(.+) b/(.+)$")
DEL_RE = re.compile(
r"^-\s*(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)|^-\s*class\s+([A-Za-z_][A-Za-z0-9_]*)"
)
def verdict(diff_text: str) -> dict:
dropped: list[str] = []
current_b = ""
for line in diff_text.splitlines():
header = FILE_RE.match(line)
if header:
current_b = header.group(2)
continue
if not current_b or current_b not in SNAP.get("files", {}):
continue
match = DEL_RE.match(line)
if not match:
continue
name = match.group(1) or match.group(2)
if name in SNAP["files"][current_b]:
dropped.append(f"{current_b}:{name}")
return {
"ok": not dropped,
"dropped_public_names": dropped,
"action": "apply" if not dropped else "hold",
}
def main() -> int:
diff_text = sys.stdin.read()
row = verdict(diff_text)
Path("fixtures/last-apply-verdict.json").write_text(
json.dumps(row, indent=2) + "\n", encoding="utf-8"
)
print(json.dumps(row))
return 0 if row["ok"] else 2
if __name__ == "__main__":
sys.exit(main())
git diff HEAD | python3 scripts/apply_gate.py; echo exit:$?
# Plant a bad rename, then confirm the gate holds the patch.
git diff HEAD | python3 scripts/apply_gate.py || echo "hold as expected"
A hold is not proof of good design, and it is not a substitute for a real review. It is only proof that the next host did not delete a name you already published. You still review behavior, tests, and security the way you did before the agent existed.
Decision table for this leftover
| Paid-product leftover | Freeze into the snapshot? | Reason you should care during cutover |
|---|---|---|
| Exported function or class name | Yes | Callers break without a compile error in other languages. |
| CLI flag or subcommand | Yes | Shell scripts fail late, usually in someone else's job. |
| HTTP path or public schema field | Yes | Mobile and partner clients will not read your chat log. |
| Private helper renamed inside a file | No | That churn should not block an otherwise valid patch. |
| Vendor chat title or prompt slug | No | You cannot replay those strings after billing stops. |
| Comment-only edits | No | Hashes on whole files will train you to skip the gate. |
| Tool traces from the old agent | No | They are not your public surface, and they are not portable. |
Print the table into the leftovers file if your team argues about what counts as public. Inventory arguments stay cheaper than the incident that follows a silent rename into production clients. Revisit the table when you add a second language, because this extractor only reads Python.
Where a free host participates, and what it must not change
After the snapshot is in git, you still need a host that can propose patches without the paid seat. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is relevant here only as a destination with free model access and a free server option. You point the same apply gate at patches from that host, and you keep the snapshot file unchanged.
You should not record invented quotas, model names, or hardware claims against that option. Those numbers are not part of this diary, and they are not a substitute for last-apply-verdict.json. If a generated patch drops a frozen name, you hold the apply, regardless of which host wrote the hunk.
Leftovers the snapshot will never contain
The paid product still owns prompts you never copied, including the ones that already renamed things last month. Those renames will not appear as deletions in a fresh diff, because the default branch already absorbed them. Write them into leftovers as narrative, with the old name and the new name, so the next host does not thrash back.
Browser review clicks, team-shared threads, and vendor caches of rejected patches are also gone at cancel time. You will not reconstruct a rejected rename after cancel from an empty leftovers markdown file. Spend thirty careful minutes on that file while you still can open the old UI, then stop hoping the export button exists.
Limitations, and who should skip this
This gate is coarse, and it will miss several export styles your codebase may already use. It parses unified diffs with regular expressions, and it will miss metaprogrammed exports, star imports, and HTTP paths declared in YAML. A green verdict does not mean the patch is correct, safe, or even worth merging today. Free model access and a free server option help you regenerate candidate patches after you cancel.
They do not create an SLA, a quota you can quote, or a stable model identity you can cite. Skip this workflow when you ship no public names, such as a private notebook that never grows an API. Skip it when compliance forbids sending repository slices to any remote host, including a free one. Skip it when a published OpenAPI job already fails on breaking paths, and wire that job instead.
Teams that need guaranteed capacity should stay on a contract they can cite, not on leftover free access. The paid product can disappear tomorrow, and your callers will not care which model last touched the tree. Keep the snapshot in git, keep the gate on your path, and treat every new host as an untrusted patch source. Anything the old vendor still holds belongs in leftovers, not in a hopeful prompt you will not be able to open.
Top comments (0)