0012 — studio remote-git projects (R-GIT-⅖)¶
Status: IN PROGRESS (reviewed 2026-06-22; §10 resolved. The WorkFS-independent
half is built & verified live — commit-on-save migrated onto vcs/repo, the
local-clone remote backend, push-on-commit, token auth, and the global-identity
gate fix. Remaining: the in-memory backend (R-GIT-5), gated on the GTB vcs/repo
WorkFS() adapter (feature request filed); plus SSH auth + branch selection,
both deferred.)
Date: 2026-06-22
Depends on: 0011 §2.2 (the local switcher + commit-on-save it extends); 0002 §1.5
(R-GIT-2/3/4/5); GTB pkg/vcs/repo (the git layer — see §2); the project
registry + the mutable active-project model; pkg/notify (push-failure alerts).
1. Goal & scope¶
Let the studio author against a project that lives as a git remote. The headline capability — the one this feature exists for — is in-memory operation: clone, edit, commit, and push entirely in RAM, with no local checkout, for users who don't want to clone locally or lack filesystem permissions. A local clone is offered as an equally-valid alternative. Either way the reels are authored against the remote and pushed back. Credentials come from config/env (forge-aware, via GTB), never committed; a push failure surfaces in the UI and, in CI, alerts.
In scope: remote project entries with a storage-backend choice (in-memory | local clone), clone, push-on-commit, the per-project filesystem, auth, and push status/alerts. Out of scope: changing the posting flow; auto-rebase on a diverged remote (surfaced, not resolved).
2. The git layer is GTB pkg/vcs/repo (not a bespoke wrapper)¶
GTB already provides exactly this, so keryx consumes it rather than hand-rolling:
- Two storage backends —
repo.LocalRepo(on disk) andrepo.InMemoryRepo(go-gitmemfs, no disk).NewRepo(p, …)thenOpen(type, …)/Clone(uri, target, …)returns a go-git*Repository+*Worktree. - Clone / Checkout / Commit / Push / tree inspection, behind the narrow
RepoLikerole interfaces (Committer,Brancher,Authenticator, …). - Forge-aware auth —
NewReporeads credentials from the tool's forge config subtree (<forge>.auth+ the<FORGE>_TOKENenv fallback for tokens,<forge>.sshfor SSH). Token auth needs no keychain — aGITLAB_TOKEN/GITHUB_TOKENenv var works — so this is testable on this dev box, unlike the OAuth keychain work. - Clone options: shallow, single-branch, no-tags, submodules.
Consequence: keryx's own internal/gitrepo (the commit-on-save wrapper) should
migrate onto vcs/repo so there is one git abstraction. The keryx-specific
identity gate is preserved at the keryx layer: vcs/repo.Commit takes a
*git.CommitOptions, so keryx resolves the effective identity and passes
Author, refusing (the gate) when user.name/user.email aren't set — GTB does
not enforce that itself. (Decision §10.1.)
3. The per-project filesystem — the one real new abstraction¶
The studio's reel/workspace ops are afero-based. Today every project shares one
afero.OsFs. A remote project's reels live in its worktree, which is:
- local clone → an on-disk dir →
afero.OsFs(today's path, unchanged), or - in-memory → the worktree's billy
memfs— not the OS FS.
So the active project gains its own afero.Fs (already foreshadowed by the
switcher's active-project model), and the handlers read/write through
a.proj().fs instead of a shared a.fs. The workspace/reel ops are already
afero-generic, so they work over either FS unchanged once it's threaded through.
The in-memory bridge is a thin afero.Fs over the worktree's billy.Filesystem.
This belongs in GTB, not keryx — vcs/repo already hands every consumer a billy
worktree, so the bridge is a general capability. A feature request is filed for
GTB to add it as a first-class citizen — repo.AferoFS(billy.Filesystem) afero.Fs
plus a WorkFS() (afero.Fs, error) accessor on RepoLike
(go-tool-base/FEATURE-REQUEST-vcs-repo-afero-worktree.md, with a reference
implementation). keryx then just sets the active project's fs = repo.WorkFS() for
in-memory projects — no local adapter to vendor or test. Sequencing dependency:
the in-memory backend lands once GTB ships WorkFS(); if keryx needs it sooner, a
local copy of the proposed adapter is the stopgap, swapped for GTB's once available.
(The AddToFS materialise alternative was rejected — duplicate state + write-back
sync; the bridge keeps one source of truth.)
4. Adding & switching a remote project¶
POST /api/v1/projects accepts, besides {path} (local dir):
{ "remote": "[email protected]:phpboyscout/blog.git", "branch": "main",
"storage": "inmemory" } // or "local"
- inmemory — clone into a GTB
InMemoryRepoheld for the session; the active project'sfsis the billy bridge; no disk touched. - local — clone into a cache dir (
<configdir>/cache/<hash>/); the active project'sfsisOsFsandreelRootpoints into the clone — i.e. it becomes a normal local project (reusing the switcher + commit-on-save wholesale).
The registry entry records remote + branch + storage (+ the cache path for
local). forget drops the entry (and offers to delete a local cache clone); it
never touches the remote. Switching to an in-memory project re-clones (cheap for a
reel-sized repo); switching to a local one reopens the cache.
5. Commit + push (R-GIT-3 remote half)¶
Commit-on-save already commits (it migrates onto vcs/repo, §2). For a remote
project, after a successful commit, push via vcs/repo.Push with the
forge-resolved auth. git.auto_push is config — default on for remote projects,
n/a for local-dir projects. The save response gains a push alongside commit:
{ "commit": {"committed": true, "hash": "ab12cd34"},
"push": {"pushed": true} } // or {"pushed": false, "reason": "…"}
The editor surfaces both ("✓ committed + pushed" / "committed — not pushed:
6. Push-failure alerts (R-GIT-4)¶
Reuse the pkg/notify seam (none/webhook/email) built for auth-refresh: an
unattended/CI push failure fires the configured notifier; the interactive studio's
UI surfacing (§5) is the alert. Config: git.alerts.backend (its own selector so
git + auth alerts can differ).
7. Security¶
- Credentials only from config/env/keychain via GTB (
<forge>.auth,<FORGE>_TOKEN,<forge>.ssh); never in.keryx.yaml, never instudio.yaml(remotes/paths only), never logged. - In-memory leaves nothing on disk — the point for permission-constrained or shared environments. A local cache clone lives under the user config dir.
- Localhost bind still applies.
8. Architecture summary¶
| Concern | Where | Notes |
|---|---|---|
| git ops (clone/commit/push, both backends) | GTB vcs/repo |
replaces internal/gitrepo |
| identity gate (commit author) | keryx — resolve + pass CommitOptions.Author |
preserved from commit-on-save |
| auth (token/ssh, forge-aware) | GTB vcs/repo (config + <FORGE>_TOKEN) |
no keychain needed for tokens |
per-project afero.Fs (OsFs / billy-bridge) |
api.proj().fs + a billy↔afero adapter |
the one new abstraction |
| remote entry (url/branch/storage/cache) | registry.go (projectEntry) |
Remote already reserved |
| add/switch remote project | handlers_projects.go |
storage choice in the body/UI |
| push-on-commit + status + UI | studio Pusher + Editor.svelte |
git.auto_push |
| push-failure alerts | reuse pkg/notify |
git.alerts.backend |
9. Testing¶
- git mechanics (clone/commit/push, both backends) against a local bare repo as a fake remote — no network, no auth — and an in-memory clone of it.
- Token auth + the auth gate: a
<FORGE>_TOKENenv var against the local remote (or a fake) — runnable on this dev box (no keychain dependency). SSH auth is env-gated. - billy↔afero bridge: tested directly (write/read/stat/readdir parity).
- Studio wiring: a fake
Pusherasserts push-after-commit + the save-response shape; a godog scenario clones a local bare "remote" in-memory, edits, saves, and asserts the commit landed in the bare repo after push.
10. Questions for review¶
- Migrate commit-on-save onto GTB
vcs/repo(one git layer; keep the keryx identity gate by passingAuthor), retiringinternal/gitrepo? (Recommend yes — one abstraction, andvcs/repois what gives us in-memory + push + auth.) - ✅ Resolved — the adapter, and it lands in GTB as a first-class
vcs/repocitizen (WorkFS()), not vendored in keryx. Feature request filed (go-tool-base/FEATURE-REQUEST-vcs-repo-afero-worktree.md). keryx consumesrepo.WorkFS(); the in-memory backend sequences on that GTB change (stopgap: a local copy of the proposed adapter). See §3. - Offer both backends, user-chosen per project; default to in-memory for a remote add (the headline use-case), local clone opt-in? Or default local on desktop? (Lean in-memory-default given the stated permission-less intent — confirm.)
- Auth v1 = token (forge
<FORGE>_TOKEN/config), SSH second — both are GTB- provided; which to wire/test first? (Recommend token first — keychain-free, testable now.) auto_pushdefault-on for remote projects (the point of a remote project is that saves propagate)? (Recommend yes.)- Sequencing. Since GTB provides in-memory, this no longer needs to wait on the mobile UI — in-memory is independently valuable (permission-less desktops/CI). Build it now as fast-follow 3, with the mobile UI separate? (Recommend yes.) ```