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]>
153 lines
7.6 KiB
Markdown
153 lines
7.6 KiB
Markdown
# 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 author
|
|
`Sage <[email protected]>` 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 <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:
|
|
|
|
```json
|
|
{ "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.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 <[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_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.
|