# Discord Sage: git for the strategy repository (#1509, QUEUE row 24) Jason's word, 2026-09-16, after the first write from Discord landed: "Sage will need to have git tooling to commit, push, pull, etc. for the shared-signals repo." This replaces the row 23 rule that Jason commits from the terminal. ## 1. Outcome A decision reached in Discord ends up committed and pushed to `shared-signals` on GitHub, by Sage, in the same conversation, with the commit and push in the turn record. Sage still has no shell and no git anywhere else. ## 2. What is built Four fixed verbs in the extension, each a `git` child process with a fixed argument list, run only in a root that is marked `"write": true` and is a git work tree. No verb takes free-form arguments. - `git_status(root)`: branch, ahead/behind, changed paths. Read only. - `git_commit(root, message, paths?)`: stages the named paths (or every change under the root when omitted), refuses a dot-prefixed path or a path outside the root, refuses an empty message or one over 500 characters, refuses when nothing is staged, commits with author `Sage ` and a trailer naming the Discord author by role (`Requested-by: Jason` or `Carmen`, never an id). Returns the short hash. - `git_pull(root)`: `git pull --ff-only origin `. A non-fast-forward result is refused with the reason and nothing is merged or rebased. - `git_push(root)`: `git push origin `, current branch only, never `--force`, never a tag, never another remote. Fences shared by all four: the root's work tree must be clean of conflicts and not mid-merge or mid-rebase; the current branch must be the one the binding names (`"branch": "main"`), so a detached head or another branch refuses; 60 s timeout; stdout and stderr are captured, trimmed to 4 KiB and returned as data; exit codes become fixed refusals. Credentials: the connector already runs with `MOSAIC_AGENT_NAME=sage`. The engine adds `MOSAIC_GIT_IDENTITY=sage` to the child environment and the repository's existing helper (`git-credential-mosaic`) resolves the seat's GitHub token at push time. The token is never read, printed, journaled or passed as an argument; a push failure returns git's message with any `https://…@` form masked. Binding (`tools` key, fixed): ```json { "name": "shared-signals", "path": "…/shared-signals", "write": true, "git": { "branch": "main", "identity": "sage" } } ``` Without `git` on a root, no git verbs are offered for it. The prompt paragraph says which root has git, that a commit is real once pushed, and that Sage reports the hash. ## 3. Not built No shell, no arbitrary git arguments, no branches, no force, no tags, no other remotes, no rebase, no reset, no history rewriting, no git in the Mosaic roots. A pull that needs a merge stops and says so; Jason resolves it from the terminal. ## 4. Evidence - `packages/discord/tests/git.test.mjs` against a local bare remote: status, commit with the trailer, pull ff-only, push; refusals for an empty message, a dot path, a path outside the root, a non-ff pull, a detached head, another branch, a conflicted tree; no token in any argument list or output. - Suite: the extension exposes the four verbs only with a `git` key. - Live: Jason asks in #ideas for a decision to be written, committed and pushed; the GitHub commit shows Sage as author and the trailer; the turn record shows write, commit and push. ## 5. Rulings from Jason (2026-09-16, evening) - D5. Identity: the sage seat's GitHub token (`github-jetrich-sage` in the seat's secrets, resolved by the existing helper), author `Sage `. Ruled: "That email works is acceptable." - D6. Push every time. Ruled against the recommendation to push only on request: "This will be fatal. Failure to automatically push will result in stale data." So every commit pushes at once; a commit whose push fails is reported as such in the reply and the turn record, and the next commit retries the push. - D7. Reviewer: rev-code-02 on #1509. Ruled: "agree". ## 6. Shared-signals record protocol (briefed 2026-09-17 by shared-signals-05) Verified against `origin/main` of `jetrich/shared-signals` (tooling landed in `8f0d946`; main at `7aa88ad` on 2026-09-17): `tools/vault_lock.py`, `tools/validate_vault.py`, `docs/ID-REGISTRY.txt`, `docs/RECORDS.md` "Reserving IDs and locking files", AGENTS.md step 4. A record's id must be reserved in the registry before the file exists, the registry is append-only and committed, `validate_vault.py` fails a commit whose ids are not registered or are used twice, and a file being edited for more than a moment is locked per clone under `.vault-locks/` (gitignored, fcntl, default TTL 3600 s). Owner comes from `VAULT_LOCK_OWNER`, then `MOSAIC_AGENT_NAME`, then git user.name; the connector already sets `MOSAIC_AGENT_NAME` to the seat, so locks read "sage" with no change. What this means for the verbs, since Sage never gets a shell: - `git_commit` runs `python3 tools/validate_vault.py` in the root first and refuses the commit, with the validator's first lines, when it fails. It also runs `vault_lock.py check` on the staged paths and refuses when another owner holds one. - A new fixed tool `reserve_id` (prefix BUS, PRJ, SS, DEC or REF plus a title) runs `vault_lock.py reserve` and returns the id; the registry line it appends is staged with the record in the next commit. `write_file` of a new record without a reserved id is not blocked by the connector; the validator catches it at commit, and the prompt tells Sage to reserve first. - `write_file` and `edit_file` take the clone lock for the path around the write (`lock`, write, `unlock`), so a terminal contributor on the same clone sees the claim. A live lock held by another owner refuses the write with that owner's name in the reply. - Staging is by explicit path only: Sage's changed records plus the registry. Jason's own uncommitted files in the same clone (the write root is his clone) are never swept in. `git_pull` is ff-only and refuses when the paths it would touch are dirty. - Push after every commit (D6). A push that fails is said in the reply and retried by the next commit. These scripts are Python in the shared-signals repo, not Mosaic code; the connector calls them as fixed argv, never through a shell, and only inside the root marked writable. ## 7. Order After row 23's web tools are reviewed. Git verbs, tests, suite, review, local commit, then the binding change and a live check.