What it means
scripts/github-sync renders each canonical record through majordomus plan body and projects it onto a GitHub milestone or issue: title, body region, milestone assignment, labels, and whether the issue is open. Comments, assignees and any text a human writes outside the generated region belong to GitHub and are never touched. A human edit inside the generated region is reported as drift and left alone.
How it works
The body is spliced between <!-- majordomus:begin <hash> --> and <!-- majordomus:end -->, the same region mechanism that lets Majordomus share a CLAUDE.md with a hand-written one. The hash in the begin marker is the hash of the canonical record the region was generated from, which is what separates the two cases: a different hash means the plan moved and the region is refreshed; the same hash with different content means a person edited it, and --apply refuses without --force.
GitHub is shared, so --apply projects only the trunk: it refuses (exit 15, with the reason and the command that reproduces it) unless HEAD is the tip of the remote's default branch — refs/remotes/<remote>/HEAD, confirmed against the remote itself — and the tree is clean, and only one apply runs at a time, under a lock in the git common directory. The adapter's own code is held to the same rule: when scripts/github-sync is loaded from a checkout of the repository it projects, that checkout must be the trunk at the same commit and clean, because its renderer and state mapping are what get posted. Every write it makes prints a line naming what changed and why; a write GitHub refuses is a FAIL line and the run exits 13 instead of reporting a half-applied remote as done. --plan, --check and --render only read and run from any branch. A listing GitHub does not answer is not an empty remote: --check and --apply exit 12 naming the listing, report no drift and write nothing, where reading it as empty would have reported every record missing and created each again.
The adapter lives in scripts/ rather than in lib/, because bin/, lib/, share/ and test/ contain no network client and test/cases/08_no_forbidden_constructs.sh proves it. The model it projects comes from the same lib/project.sh the CLI uses.
How to see it
scripts/github-sync --plan # offline: what would be created or changed, and the body hashes
scripts/github-sync --check # compare against the live repository; exit 11 on drift
scripts/github-sync --apply # from the clean trunk only: create what is missing, update what changedWhat it does not cover
Nothing is read back. Closing an issue on GitHub does not complete it canonically; the next --check reports the disagreement and the canonical record wins. There is no mapping file either. An issue is matched by the <!-- majordomus:record <id> --> marker its generated region carries, and only an issue with no marker is matched, once, by the id that prefixes its title; a milestone carries no marker and is matched by that prefix alone, so renaming the prefix of a milestone on GitHub orphans it.
Why it exists
GitHub is where the conversation happens and a poor place for the plan to live: it has no dependency graph, no validation, and no way to refuse a status that contradicts one. Making it a projection keeps the collaboration and moves the truth into the repository.