Row 24. A writable root that is a git work tree may carry a git object in the binding; the seat then has git_status, git_commit (explicit paths, seat author, Requested-by trailer from the envelope requester, push at once per D6), git_pull (ff-only) and git_push (one branch, never force), plus reserve_id and per-write clone locks under protocol vault. Git children run with no host config and one credential helper, bin/git-credential.mjs, reading the 0600 seat token file named in the binding; the fleet helper serves only the Gitea hosts. Suite 58/58, node 143. rev-code-02 APPROVED round 1 (#1509 comment 26375, tree 82ab962f). Co-Authored-By: Claude Fable 5.1 <[email protected]>
7.6 KiB
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 exactly the named paths (one to fifty; the "every change when omitted" form of the first draft was dropped for section 6's explicit-path rule, since the root is Jason's own clone), refuses a dot-prefixed path or a path outside the root, refuses when the index already holds staged work, refuses an empty message or one over 500 characters, refuses when nothing is staged, commits with authorSage <[email protected]>and a trailer naming the Discord author by role (Requested-by: JasonorCarmen, never an id). Returns the short hash.git_pull(root):git pull --ff-only origin <current branch>. A non-fast-forward result is refused with the reason and nothing is merged or rebased.git_push(root):git push origin <current branch>, 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, as built (2026-09-18; the draft above said the fleet helper
would serve): ~/.mosaic/tools/git/git-credential-mosaic answers only
the two Gitea hosts and exits silently for github.com, and the host's
global git config sends github.com to Jason's own gh login. Neither is
acceptable for Sage, so the verbs run git with GIT_CONFIG_GLOBAL=/dev/null,
GIT_CONFIG_NOSYSTEM=1, no askpass, no prompt, and one helper set
through GIT_CONFIG_COUNT: the package's bin/git-credential.mjs,
which answers get over https from the 0600 file the binding names in
tokenFile. The connector checks the file's mode at load and never reads
it; the path reaches git only for push, pull and reserve, through the
environment, never as an argument. The token is never read by the
connector, printed, journaled or passed as an argument; a push failure
returns git's message with any https://…@ form and token shape masked.
The D5 identity (seat token, Sage <[email protected]>) is unchanged;
only the mechanism that presents it differs from the draft.
The requester in the trailer is the Discord author's server name, which
the connector now writes into the envelope line (requester="…") and the
extension reads on before_agent_start. A turn without a requester
cannot commit.
Binding (tools key, fixed), as built:
{ "name": "shared-signals", "path": "…/shared-signals", "write": true,
"git": { "branch": "main", "identity": "sage",
"tokenFile": "…/secrets/github-jetrich-sage.token",
"author": "Sage <[email protected]>", "protocol": "vault" } }
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.mjsagainst 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
gitkey. - 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-sagein the seat's secrets, resolved by the existing helper), authorSage <[email protected]>. 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_commitrunspython3 tools/validate_vault.pyin the root first and refuses the commit, with the validator's first lines, when it fails. It also runsvault_lock.py checkon 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) runsvault_lock.py reserveand returns the id; the registry line it appends is staged with the record in the next commit.write_fileof 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_fileandedit_filetake 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_pullis 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.