mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
* test: synced JSON that does not parse is never committed and is reported (#3763) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(brain-sync): hold a synced JSON file that does not parse; --profile and doctor report it (#3763) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(brain-sync): read JSON index blobs with one cat-file batch, no python subprocess (#3763) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(brain-sync): block instead of committing a one-sided merge when a held file is mid-merge (#3763) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix(brain-sync): merge-block status names the real recovery and waiting files (#3763) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(brain-sync): JSON holds in the --status, held-files and two-machine sections (#3763) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * test(brain-sync): #3704 fixtures write valid JSON to developer-profile.json (#3763) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * docs(brain-sync): keep the reviewed JSON paragraph wording after the main merge (#3763) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
252 lines
10 KiB
Markdown
252 lines
10 KiB
Markdown
# Cross-machine memory with GBrain sync
|
||
|
||
gstack writes a lot of useful state to `~/.gstack/` — learnings, retros, CEO
|
||
plans, design docs, developer profile. By default, all of that dies when you
|
||
switch laptops. **GBrain sync** pushes a curated subset to a private git
|
||
repo so your memory follows you across machines and becomes indexable by
|
||
GBrain.
|
||
|
||
## What you get
|
||
|
||
- Work on machine A, pick up seamlessly on machine B.
|
||
- Your learnings, plans, and designs are visible in GBrain (if you use it).
|
||
- A clean off-ramp (`gstack-brain-uninstall`) that never touches your data.
|
||
- No daemon, no system service, no background process.
|
||
|
||
## What does NOT leave your machine
|
||
|
||
By design, these stay local even when sync is on:
|
||
|
||
- Credentials: `.auth.json`, `auth-token.json`, `sidebar-sessions/`,
|
||
`security/device-salt`
|
||
- Machine-specific state: Chromium profiles, ONNX model weights,
|
||
caches, eval-cache, CDP-profile, one-time prompt markers
|
||
(`.welcome-seen`, `.telemetry-prompted`, `.vendoring-warned-*`, etc.)
|
||
- Question-preferences: per-machine UX preferences
|
||
(`question-preferences.json`, `question-log.jsonl`, `question-events.jsonl`).
|
||
|
||
The exact allowlist lives in `~/.gstack/.brain-allowlist`. The CLI manages
|
||
it; you can append your own entries below the marker line.
|
||
|
||
## First-run setup (30–90 seconds)
|
||
|
||
```bash
|
||
gstack-artifacts-init
|
||
```
|
||
|
||
The command:
|
||
|
||
1. Turns `~/.gstack/` into a git repo.
|
||
2. Asks for a remote URL (default: `gh repo create --private
|
||
gstack-artifacts-$USER`). Any git remote works — GitHub, GitLab, Gitea,
|
||
self-hosted.
|
||
3. Pushes an initial commit with just the config.
|
||
4. Writes `~/.gstack-artifacts-remote.txt` (URL-only, no secrets —
|
||
safe to copy to another machine).
|
||
5. Prints the `gbrain sources add` hookup command for the brain host
|
||
(never auto-executed — run it yourself, or on your own machine
|
||
`bin/gstack-gbrain-source-wireup` does the same wiring) so
|
||
`gbrain search` can index your synced learnings, plans, and designs.
|
||
The old `gstack-brain-reader add --ingest-url ...` HTTP path was
|
||
removed in v1.15.1.0 — it depended on a `/ingest-repo` endpoint gbrain
|
||
never shipped.
|
||
|
||
After init, the **next skill you run** will ask you ONE question about
|
||
privacy mode:
|
||
|
||
- **Everything allowlisted (recommended)**: learnings, reviews, plans,
|
||
designs, retros, timelines, and developer profile all sync.
|
||
- **Only artifacts**: plans, designs, retros, learnings — skip
|
||
behavioral data (timelines, developer profile).
|
||
- **Decline**: keep everything local. You can turn sync on later with
|
||
`gstack-config set artifacts_sync_mode full`.
|
||
|
||
Your answer is persisted. You won't be asked again.
|
||
|
||
## Cross-machine workflow
|
||
|
||
On machine A: run `gstack-artifacts-init` once. That's it — every skill
|
||
invocation now drains the sync queue at its start and end boundaries
|
||
(~200–800 ms network pause per skill).
|
||
|
||
On machine B:
|
||
|
||
1. Copy `~/.gstack-artifacts-remote.txt` from machine A to machine B
|
||
(password manager, dotfile repo, USB stick — your call; the legacy
|
||
`~/.gstack-brain-remote.txt` name is still recognized).
|
||
2. Run any gstack skill. The preamble sees the URL file and prints:
|
||
```
|
||
BRAIN_SYNC: brain repo detected: <url>
|
||
BRAIN_SYNC: run 'gstack-brain-restore' to pull your cross-machine memory
|
||
```
|
||
3. Run `gstack-brain-restore`. That clones the repo, rehydrates your
|
||
learnings/plans/retros, and re-registers the git merge drivers.
|
||
4. Next skill: your yesterday-on-machine-A learning surfaces. That's the
|
||
magical moment.
|
||
|
||
## Status, health, and queue depth
|
||
|
||
```bash
|
||
gstack-brain-sync --status
|
||
```
|
||
|
||
Shows: the status (`ok`, `idle`, `held`, `collision`, `blocked`,
|
||
`push_failed`, `error`), files held back by the secret scan or because
|
||
they are not valid JSON, with their fixes, files held because their path differs only by letter case from
|
||
another path (`case_held`, `case_names`; see
|
||
[troubleshooting](troubleshooting.md#artifact-case-collision)), the
|
||
`drainable` count, the last sync and push times, pending queue depth, and
|
||
the current privacy mode.
|
||
|
||
Every skill run prints an `ARTIFACTS_SYNC:` line near the top of the
|
||
preamble output. When the sync is stuck it adds an
|
||
`ARTIFACTS_SYNC: attention:` line; the table of those lines and their fixes
|
||
is in [gbrain-sync-errors.md](gbrain-sync-errors.md#artifacts_sync-attention--at-skill-start).
|
||
|
||
## Privacy modes in detail
|
||
|
||
| Mode | What syncs |
|
||
|------|------------|
|
||
| `off` | Nothing (default). |
|
||
| `artifacts-only` | Plans, designs, retros, learnings, reviews. Skips timelines + developer-profile. |
|
||
| `full` | Everything in the allowlist, including behavioral state. |
|
||
|
||
Change anytime with:
|
||
```bash
|
||
gstack-config set artifacts_sync_mode full
|
||
gstack-config set artifacts_sync_mode off
|
||
```
|
||
|
||
## Removed and moved files
|
||
|
||
By default the sync never publishes a removal: delete a synced design doc,
|
||
or move a project directory (a project slug migration does), and the old
|
||
path stays on the remote, where the brain keeps indexing it next to the new
|
||
copy. To publish removals too:
|
||
|
||
```bash
|
||
gstack-config set artifacts_sync_removals on
|
||
```
|
||
|
||
Each sync then commits the removal of synced files that are gone from disk.
|
||
Transcript pages are left alone, and a decision log and its active snapshot
|
||
go only together. A sync that would remove more than 20 files at once
|
||
publishes none of them: that looks like a loss, not a cleanup. Review and
|
||
publish those yourself, whatever the setting:
|
||
|
||
```bash
|
||
gstack-brain-sync --publish-removals # list them
|
||
gstack-brain-sync --publish-removals --yes # commit and push their removal
|
||
```
|
||
|
||
## Secret protection
|
||
|
||
Every commit is scanned for credential-shaped content before it leaves
|
||
your machine. Blocked patterns include:
|
||
|
||
- AWS access keys (`AKIA…`)
|
||
- GitHub tokens (`ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_`, `github_pat_`)
|
||
- OpenAI keys (`sk-…`)
|
||
- PEM blocks (`-----BEGIN …-----`)
|
||
- JWTs (`eyJ…`)
|
||
- Bearer tokens in JSON (`"authorization": "…"`, `"api_key": "…"`, etc.)
|
||
|
||
If a scan hits, only the flagged file is held back (with any file coupled
|
||
to it); everything else syncs. The held file stays queued and is re-scanned
|
||
at every sync, and skill start prints:
|
||
|
||
```
|
||
ARTIFACTS_SYNC: attention: status=held held=1. Sync is holding back 1 file(s) that the secret scan flagged or that are not valid JSON; everything else still syncs. See which files and how to fix them: <bin> --status
|
||
```
|
||
|
||
To remediate:
|
||
|
||
1. Run `gstack-brain-sync --status` (the line prints its absolute path). Each
|
||
entry under `held` names the file, the scanner rule and both fixes.
|
||
2. If the match is a false positive on content you never want synced, run
|
||
`gstack-brain-sync --skip-file <path>` to permanently exclude that path;
|
||
`gstack-brain-sync --unskip-file <path>` undoes it.
|
||
3. Otherwise, edit the file to remove the secret; the next skill run syncs it.
|
||
|
||
There's a defense-in-depth hook at `~/.gstack/.git/hooks/pre-commit` that
|
||
runs the same scan if you manually `git commit` against the repo.
|
||
|
||
Separately (v1.63.0.0+), every push writes a tamper-evident receipt to the
|
||
egress ledger (`~/.gstack/security/egress.jsonl`) *before* anything is
|
||
sent, fail-closed: if the receipt can't be written, the push is refused
|
||
and the queue is preserved. Inspect the ledger with `gstack-egress list`
|
||
and verify its hash chain with `gstack-egress verify`.
|
||
|
||
## Two-machine conflicts
|
||
|
||
If you write on machine A and machine B the same day, both will push
|
||
append commits. Git's default would conflict at the file tail, but the
|
||
`.jsonl` and markdown files are registered with custom merge drivers:
|
||
|
||
- JSONL files use a sort-and-dedup driver that orders appends by ISO
|
||
timestamp (falls back to SHA-256 hash of each line for determinism).
|
||
- Markdown artifacts (retros, plans, designs) use a union merge driver
|
||
that concatenates both sides.
|
||
|
||
You shouldn't see conflict prompts. If you do (a real semantic conflict,
|
||
like two machines editing the same plan), git will stop and prompt.
|
||
|
||
When a push is rejected, brain-sync fetches and merges the other machine's
|
||
commits itself. Local edits to files that merge would change are set aside
|
||
and put back afterward, and a failed merge is aborted, so `~/.gstack` never
|
||
holds conflict markers. If a local edit conflicts with the other machine's
|
||
change, the other machine's version stays in the working tree and your edit
|
||
is kept in a git stash; `gstack-brain-sync --status` names the stash and the
|
||
`git -C ~/.gstack --literal-pathspecs restore --source=<stash> -- <path>`
|
||
command that brings it back.
|
||
|
||
JSON files such as `developer-profile.json` have no merge driver, so a
|
||
merge can leave conflict markers in them. The sync parses every staged
|
||
`.json` file and holds back one that does not parse (status `held`, rule
|
||
`invalid-json`) until you repair it, and `gstack-doctor` warns when a synced profile or
|
||
decision snapshot holds conflict markers.
|
||
|
||
## Cross-machine pull cadence
|
||
|
||
The preamble runs `git fetch` + `git merge --ff-only` once per 24 hours
|
||
(cached via `~/.gstack/.brain-last-pull`). You don't need to think about
|
||
this — it happens automatically at the first skill invocation each day.
|
||
|
||
Historical note (#2516): that daily pull refreshed only `~/.gstack` itself —
|
||
NOT the detached worktree at `~/.gstack-brain-worktree` that gbrain actually
|
||
indexes, so the brain silently served stale pages until the next
|
||
setup-gbrain/sync-gbrain run. Since this fix, the daily sync also advances
|
||
the brain worktree (`gstack-gbrain-source-wireup --advance-only`, throttled
|
||
via `~/.gstack/.brain-worktree-last-advance`); a failed advance warns instead
|
||
of failing silently, and never force-resets a dirty worktree.
|
||
|
||
## Uninstall
|
||
|
||
```bash
|
||
gstack-brain-uninstall
|
||
```
|
||
|
||
This:
|
||
|
||
- Removes `~/.gstack/.git/` and all `.brain-*` config files.
|
||
- Clears `artifacts_sync_mode` in `gstack-config`.
|
||
- Does NOT touch your learnings, plans, retros, or developer profile.
|
||
|
||
Add `--delete-remote` to also delete the private GitHub repo (GitHub only,
|
||
uses `gh repo delete`).
|
||
|
||
Re-init anytime with `gstack-artifacts-init`.
|
||
|
||
## Troubleshooting
|
||
|
||
See [gbrain-sync-errors.md](gbrain-sync-errors.md) for an index of every
|
||
error message gstack-brain may print, with problem / cause / fix for each.
|
||
|
||
## Under the hood
|
||
|
||
The architectural decisions behind this feature: allowlist over denylist
|
||
(unknown files stay local by default), preamble-boundary sync over a daemon
|
||
(no background process to babysit), a JSONL merge driver so concurrent
|
||
machines union their queues instead of conflicting, and a privacy stop-gate
|
||
that asks once before anything syncs.
|