GitHub sync is opt-in. Nothing syncs until this skill enables it. Once
enabled, three pieces work together (see libs/github_sync/README.md):
-
Check current state: uv run github-sync status. If is_configured
is already true, jump to "Status" (or "Repair" if the service is
unhealthy). Also run supervisorctl status github-sync (it errors when no
such program exists -- expected before enable).
-
Request GitHub permissions through latchkey (see the latchkey skill
for the permission-request mechanics). GitHub exposes two latchkey scopes
and a permission request carries exactly one scope, so this is two
requests -- fire both back-to-back, before any other GitHub call, so
the user approves them in a single sitting. Never dribble out further
requests later in the flow.
latchkey curl -XPOST http://latchkey-self.invalid/permission-requests \
-H 'Content-Type: application/json' \
-d '{"agent_id": "'"$MNGR_AGENT_ID"'", "type": "predefined", "payload": {"scope": "github-git", "permissions": ["github-git-read", "github-git-write"]}, "rationale": "GitHub sync: push this workspace'"'"'s branches and runtime state to your private sync repo."}'
latchkey curl -XPOST http://latchkey-self.invalid/permission-requests \
-H 'Content-Type: application/json' \
-d '{"agent_id": "'"$MNGR_AGENT_ID"'", "type": "predefined", "payload": {"scope": "github-rest-api", "permissions": ["github-read-user", "github-read-repos", "github-write-all"]}, "rationale": "GitHub sync: create the private sync repo (needs github-write-all), confirm which GitHub account it lands under (github-read-user), and verify it stays private (github-read-repos)."}'
This exact permission set is what the flow needs -- do not trim it, or the
user gets asked again mid-flow:
github-write-all -- repo creation (POST /user/repos). The narrower
github-write-repos covers only existing-repo (/repos/{owner}/{repo})
paths and is not enough to create a repo. It also covers the
optional repo deletion on disable.
github-read-repos -- the recurring private-visibility check
(GET /repos/{owner}/{repo}), which the service repeats forever.
github-read-user -- GET /user, to name the account the repo will be
created under (step 3) and as the one legitimate "did the grants land?"
probe (below).
github-git-read / github-git-write -- clone/fetch and push.
Then wait for both approval system messages ("Your permission request
for GitHub (git) / (REST API) was granted..."). Those messages are the
authoritative signal; they are what tells you to proceed.
Do not treat a rejected API call as evidence that a grant is missing.
{"error": "Error: Request not permitted by the user."} means that
endpoint is not covered by the granted permissions -- it does not mean
the approval failed to arrive. Probing an endpoint outside the set above
(e.g. GET /user/repos, which needs broader read access) will be rejected
even when everything is granted correctly. If you want to sanity-check
after the grant messages arrive, the only probe to use is:
latchkey curl -s https://api.github.com/user
Never re-ask the user for a permission you have already been told was
granted; re-read this list and check which endpoint you called instead.
-
Pick the repo. Default: create a brand-new private repo named after
the workspace ($MINDS_WORKSPACE_NAME), owned by the authenticated GitHub
account (read its login from latchkey curl -s https://api.github.com/user).
Confirm the name and owner with the user first; they can name an org
instead. One exception: if git remote get-url origin already points at a
user-owned repo (not imbue-ai/default-workspace-template or another
shared template), ask whether to reuse it or create a fresh one --
recommend a fresh dedicated repo unless they have a specific reason.
Reused repos must be verified private and writable like new ones.
-
Create the repo (skip if reusing):
latchkey curl -s -X POST https://api.github.com/user/repos \
-H 'Content-Type: application/json' \
-d '{"name": "<repo-name>", "private": true, "description": "Private sync repo for the <workspace> minds workspace"}'
(For an org: POST https://api.github.com/orgs/<org>/repos.) On a 422
name-taken error, append -2, -3, ... and retry. The response JSON must
contain "private": true -- if it does not, delete/abandon the repo and
stop; do not proceed with a public repo. The response's full_name is the
authoritative <owner>/<repo> to use from here on.
-
Point origin at it and record the config:
git remote set-url origin https://github.com/<owner>/<repo>.git \
|| git remote add origin https://github.com/<owner>/<repo>.git
Write github_sync.toml at the repo root (this file is the "sync is
enabled" marker for the service and the post-commit hook):
repo_url = "https://github.com/<owner>/<repo>"
-
Wire git through the gateway: uv run github-sync wire-git. From now
on plain git push/git fetch against github.com works in every
checkout, and the post-commit auto-push hook is active.
-
Create (or restore) the runtime worktree:
uv run github-sync setup-worktree. If origin already has a
runtime-sync branch (re-enabling for a workspace recreated from a
previously-synced repo), this restores the prior runtime/ state instead of
starting fresh -- tell the user their memory/tickets/transcripts are back.
-
Verify private before any push: uv run github-sync check-visibility
must print private (exit 0). If not, stop and surface the problem.
-
Initial sync: push the current branch, the runtime-sync branch, and
any existing worker branches:
git push --set-upstream origin "$(git branch --show-current)"
git -C runtime push --set-upstream origin runtime-sync
for b in $(git for-each-ref --format='%(refname:short)' refs/heads/ | grep -v -x -e "$(git branch --show-current)" -e runtime-sync); do git push origin "$b"; done
-
Add the service by appending this block to supervisord.conf, then
supervisorctl reread && supervisorctl update (see the edit-services
skill):
[program:github-sync]
command=python3 scripts/oom_tag_service.py github-sync uv run github-sync run
directory=/mngr/code
autostart=true
autorestart=true
startretries=1000000
stopasgroup=true
killasgroup=true
stdout_logfile=/var/log/supervisor/github-sync-stdout.log
stderr_logfile=/var/log/supervisor/github-sync-stderr.log
stdout_logfile_maxbytes=10MB
stderr_logfile_maxbytes=10MB
stdout_logfile_backups=3
stderr_logfile_backups=3
-
Commit the enablement (github_sync.toml + supervisord.conf). The
now-active hook pushes the commit; this also makes sync sticky if the
repo is later used to recreate the workspace.
-
Report: the repo URL, that every commit now auto-pushes, that
runtime/ syncs every minute, and that pushes queue while their machine
(the latchkey gateway) is offline (on remote hosts the per-VPS secondary
gateway usually covers that).
Confirm with the user first, and ask separately whether to keep the remote
repo (recommend keeping it -- it costs nothing and preserves history).