diff --git a/setup-gbrain/SKILL.md.tmpl b/setup-gbrain/SKILL.md.tmpl index 5ef5098cf..c24511099 100644 --- a/setup-gbrain/SKILL.md.tmpl +++ b/setup-gbrain/SKILL.md.tmpl @@ -397,14 +397,15 @@ the prereq is fixed. --- -## Step 7.5: Transcript & memory ingest gate +## Step 7.5: Transcript ingest consent **SKIP entirely on Path 4 (Remote MCP).** Transcript ingest shells out to the local `gbrain` CLI which Path 4 doesn't install. Remote-mode users rely on the brain server's own ingest cadence — if your brain admin wants this machine's transcripts indexed, they pull from your `gstack-artifacts-$USER` -repo (set up in Step 7) on whatever schedule they prefer. Set -`gstack-config set transcript_ingest_mode off` and continue to Step 8. +repo (set up in Step 7) on whatever schedule they prefer. Do not store a +transcript mode for the user here; transcripts stay skipped until they choose +(`/sync-gbrain` asks). Continue to Step 8. For Paths 1, 2a, 2b, 3, run the ingest gate: @@ -541,7 +542,7 @@ state, repairs only what's missing, and reports here. ```bash eval "$(~/.claude/skills/gstack/bin/gstack-paths)"; : "${GSTACK_STATE_ROOT:?gstack-paths failed; reinstall with ./setup or /gstack-upgrade}" ~/.claude/skills/gstack/bin/gstack-gbrain-detect 2>/dev/null || true -~/.claude/skills/gstack/bin/gstack-config get transcript_ingest_mode 2>/dev/null || echo "off" +~/.claude/skills/gstack/bin/gstack-config has transcript_ingest_mode && echo "transcript_ingest_mode: $(~/.claude/skills/gstack/bin/gstack-config get transcript_ingest_mode)" || echo "transcript_ingest_mode: not set" ~/.claude/skills/gstack/bin/gstack-config get artifacts_sync_mode 2>/dev/null || echo "off" [ -f "$GSTACK_STATE_ROOT"/.gbrain-sync-state.json ] && cat "$GSTACK_STATE_ROOT"/.gbrain-sync-state.json || echo "{}" ``` @@ -561,7 +562,7 @@ gbrain status: GREEN (mode: remote-http) Repo policy ..... OK {read-write|read-only|deny} Artifacts repo .. OK {gstack_artifacts_remote URL} Artifacts sync .. OK {artifacts_sync_mode} - Transcripts ..... OK route to artifacts repo → remote brain + Transcripts ..... OK {recent|all}: route to artifacts repo → remote brain | INFO {off|not set}: skipped Code search ..... {OK local-pglite (~/.gbrain/pglite) | N/A declined at Step 4d} CLAUDE.md ....... OK Smoke test ...... INFO printed for post-restart manual verification @@ -592,13 +593,16 @@ gbrain status: GREEN (mode: local-stdio) Repo policy ..... OK Code import ..... OK Artifacts sync .. OK to - Transcripts ..... OK sessions, last ingest + Transcripts ..... OK : sessions, last ingest | INFO : skipped CLAUDE.md ....... OK Smoke test ...... OK put → search round-trip Run `/setup-gbrain` again any time gbrain feels off; it's safe and idempotent. ``` +A `not set` transcript mode is not a failure: transcripts stay skipped until +the user chooses, and `/sync-gbrain` asks. + If any row is YELLOW or RED, the verdict line says so and the failing rows surface a one-line "next action" (e.g., `Engine .......... ERR PGLite corrupt — re-import from a clone of your brain remote with \`gbrain import\``). diff --git a/setup-gbrain/memory.md b/setup-gbrain/memory.md index 8145477dd..16935b523 100644 --- a/setup-gbrain/memory.md +++ b/setup-gbrain/memory.md @@ -1,9 +1,9 @@ # gstack memory ingest — what it does, what stays local, what you can do with it -This is the user-facing reference for the V1 transcript + memory ingest -feature in `/setup-gbrain`. If you ran `/setup-gbrain` and it asked -"Ingest THIS repo's transcripts into gbrain?", this doc explains what -happens after you say yes. +This is the user-facing reference for the transcript + memory ingest +feature in `/setup-gbrain` and `/sync-gbrain`. If either asked whether to +ingest your coding-agent sessions into gbrain, this doc explains what each +answer does. ## What gets ingested @@ -20,6 +20,62 @@ happens after you say yes. | Retros | `retro` | `~/.gstack/projects//retros/*.md` | Medium | | Builder profile | `builder-profile-entry` | `~/.gstack/builder-profile.jsonl` | Low | +## Transcripts + +Curated memory (learnings, timeline, plans, designs, retros, eureka, builder +profile) syncs on every `/sync-gbrain`. Session transcripts sync only after +you choose to share them. The choice is the `transcript_ingest_mode` config +key: + +| Value | What `/sync-gbrain` ingests | +|---|---| +| `recent` | Sessions from the last 90 days | +| `all` | All history (`--full` walks every session file) | +| `off` | No transcripts; other memory still syncs | + +Both sources are covered: Claude Code sessions (`~/.claude/projects/`) and +Codex sessions (`~/.codex/sessions/`), from every project on this machine +that your per-remote trust policy allows, not only the current repo. Pages go +to your brain: the local engine (PGLite or Supabase) in local-stdio mode, or +the artifacts repo the remote brain pulls from in remote-http mode. + +Set or change it: + +```bash +gstack-config set transcript_ingest_mode recent # or all, or off +gstack-config has transcript_ingest_mode && gstack-config get transcript_ingest_mode +``` + +`gstack-config set` rejects any other value and keeps the stored one. + +**Not set.** Until you choose, transcripts are skipped. Each sync prints one +line saying so, even with `--quiet`, and the next interactive `/sync-gbrain` +asks once. Older values from previous gate versions (`A`-`E`, `incremental`) +also count as not chosen and get the question again. A stored `off` prints +`transcripts off (your choice)` without `--quiet` and is never asked again. + +**One-run override.** A `--sources` list (or `GSTACK_MEMORY_INGEST_SOURCES`) +that names `transcript` ingests transcripts for that run whatever the mode; +the sync still prints the mode notice. `--sources all`, an empty list, or a +list with no valid types does not override. + +```bash +bun run bin/gstack-gbrain-sync.ts --incremental --sources transcript +``` + +**Interrupted imports.** If a sync was interrupted mid-import and your +transcript choice changed before the next run, the next sync restages memory +from scratch once instead of resuming, and prints a line saying so. + +**Staged pages wait.** Transcript pages already staged under +`~/.gstack/transcripts/` (remote-http mode) stay on disk and are not pushed +by `gstack-brain-sync` until the mode is `recent` or `all`. Other artifacts +keep pushing. + +**Already-ingested transcripts stay.** Switching to `off` stops new ingests; +it does not remove pages already in the brain. Delete them there (see +"Delete a page" below). + ## What stays local - **State files** (`~/.gstack/.gbrain-sync-state.json`, diff --git a/setup-gbrain/sections/manifest.json b/setup-gbrain/sections/manifest.json index de5265113..8c8744efe 100644 --- a/setup-gbrain/sections/manifest.json +++ b/setup-gbrain/sections/manifest.json @@ -19,7 +19,7 @@ { "id": "transcript-gate", "file": "transcript-gate.md", - "title": "Step 7.5: transcript & memory ingest gate (probe, thresholds, ingest AskUserQuestion)", + "title": "Step 7.5: transcript ingest consent (has-check, probe, recent/all/off AskUserQuestion)", "trigger": "running the Step 7.5 transcript & memory ingest gate on Paths 1, 2a, 2b, or 3 (Path 4 skips this section entirely — see the skeleton's skip note)" }, { diff --git a/setup-gbrain/sections/transcript-gate.md.tmpl b/setup-gbrain/sections/transcript-gate.md.tmpl index 573d3c5f8..3751a1e23 100644 --- a/setup-gbrain/sections/transcript-gate.md.tmpl +++ b/setup-gbrain/sections/transcript-gate.md.tmpl @@ -1,64 +1,90 @@ After memory sync is wired (Step 7) but before persisting the CLAUDE.md -config (Step 8), offer to bring this Mac's coding-agent transcripts + -curated `~/.gstack/` artifacts into gbrain so the retrieval surface -(per-skill manifests, salience block) has data to surface. +config (Step 8), ask whether this machine's coding-agent session +transcripts may go into gbrain. Curated `~/.gstack/` artifacts (learnings, +timeline, plans, designs, retros) sync whatever the answer; this question +covers transcripts only. Transcripts are ingested only after the user picks +`recent` or `all`. -Run the probe to size the operation: +Check whether the user already chose. The gate keys on `has`, because `get` +prints the `off` default for a key that was never set: ```bash -bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --probe +_TIM=$(~/.claude/skills/gstack/bin/gstack-config get transcript_ingest_mode 2>/dev/null || true) +if ~/.claude/skills/gstack/bin/gstack-config has transcript_ingest_mode; then + case "$(printf '%s' "$_TIM" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')" in + recent|all|off) echo "TRANSCRIPT_MODE: $_TIM" ;; + *) echo "TRANSCRIPT_MODE: ask (stored value '$_TIM' is from an older version)" ;; + esac +else + echo "TRANSCRIPT_MODE: ask (not set)" +fi ``` -Read the output. If `Total files in window: 0`, skip — there's nothing -to ingest. Set `gstack-config set transcript_ingest_mode incremental` -silently and continue to Step 8. +- `TRANSCRIPT_MODE: recent`, `all` or `off`: the user already chose. Do not + ask again; continue to Step 8. +- `TRANSCRIPT_MODE: ask`: ask once, below. With `SESSION_KIND: spawned` or + `headless`, do not ask and do not store a value. Consent is never + auto-chosen; leave the key unset (transcripts stay skipped) and report + that the next interactive `/sync-gbrain` will ask. -If `New (never ingested)` is < 200 AND total bytes are < 100MB: silent -bulk via `bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --bulk --quiet`. Set -`transcript_ingest_mode=incremental` and continue. +Size the question with two probes. `--sources transcript` makes the probe +count transcripts even though none are consented yet: +```bash +bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --probe --sources transcript +bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --probe --sources transcript --all-history +``` +Read `Total files in window`, `Total bytes` and the estimate from each. -Otherwise (the "many transcripts on disk" path): AskUserQuestion with -the exact counts AND the value promise. Default scope is **current repo -only, last 90 days**: +**No transcripts found** (both probes report 0): ask yes/no instead: -> "Found transcripts in THIS repo () over the last -> 90 days, plus across other repos on this machine ( -> total if all ingested). Ingest THIS repo's transcripts into gbrain? +> "Ingest coding-agent sessions as they appear?" + +Yes stores `recent`; No stores `off`. + +**Otherwise**, AskUserQuestion with the counts. Name both session sources +and the destination brain from Step 4: the user's Supabase brain (Paths 1, +2a, 2b) or the local PGLite brain on this machine (Path 3). + +> "Found Claude Code and Codex sessions from the last 90 days +> ( in all history, ) across the projects on this machine +> that your repo policy allows. Ingest them into your PGLite> brain? > -> What you get after this: every gstack skill auto-loads recent salience -> from your past sessions in this repo, so the agent finds your prior -> work without you describing it. You can query 'what was I doing on -> day X' and get a real answer. Per-session pages are searchable, -> taggable, and deletable. Secret scanning runs before any push. +> What you get: gstack skills load recent context from your past sessions, +> so the agent finds your prior work without you describing it, and you +> can ask 'what was I doing on day X'. Per-session pages are searchable, +> taggable and deletable. Secret scanning runs before any push. > -> What stays the same: nothing leaves your machine unless gbrain sync -> is enabled (Step 7). Per-repo trust policies still apply. +> What stays the same: nothing leaves this machine unless brain sync is +> enabled (Step 7). Per-repo trust policies still apply. The first full +> sync can take a while on a large history (estimate: ); skills stay +> usable while it runs. > -> Multi-Mac note: if you HAVE enabled brain sync (Step 7), these -> transcript pages will sync across your Macs. Caveat: deleting a -> transcript page later removes it from gbrain but git history retains -> it in prior commits. Use `gstack-transcript-prune` to delete in bulk; -> use `git filter-repo` on the brain remote for hard-delete from -> history." +> Multi-machine note: with brain sync enabled, transcript pages sync across +> your machines. Deleting a page later removes it from gbrain, but git +> history keeps it in earlier commits. Use `gstack-transcript-prune` to +> delete in bulk, and `git filter-repo` on the brain remote to remove it +> from history." Options: -- A) Yes — this repo, last 90 days (recommended; ~est min) -- B) Yes — this repo, ALL history -- C) Yes — this repo + other repos on this machine -- D) Skip historical, track new from now (`transcript_ingest_mode=incremental`) -- E) Never ingest transcripts (`transcript_ingest_mode=off`) +- A) Yes, last 90 days (`recent`) +- B) Yes, all history (`all`) +- E) No, never ingest transcripts (`off`) -After answer: +Store the mode value, never the letter: +```bash +~/.claude/skills/gstack/bin/gstack-config set transcript_ingest_mode +``` + +For `recent` or `all`, run the first sync now: ```bash -~/.claude/skills/gstack/bin/gstack-config set transcript_ingest_mode bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --full --no-brain-sync ``` -(`--no-brain-sync` because Step 7 already wired that path; this just -runs the code import + memory ingest stages. Brain-sync will run on the -next preamble hook.) +(`--no-brain-sync` because Step 7 already wired that path; this runs the +code import and memory ingest stages, and `all` walks the full history.) +For `off`, run nothing here; other memory still syncs on the next +`/sync-gbrain`. -If A/D/E, ingest is incremental from this point on; preamble-boundary -hook runs `bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --incremental --quiet` on every skill -start (cheap mtime fast-path). +New transcripts are ingested on the next `/sync-gbrain` run. Nothing at +skill start ingests them. -Reference doc for users: `setup-gbrain/memory.md` (linked from CLAUDE.md -Step 8). +Reference doc for users: `setup-gbrain/memory.md#transcripts`. diff --git a/sync-gbrain/SKILL.md.tmpl b/sync-gbrain/SKILL.md.tmpl index 9e1f3b265..5b90339a7 100644 --- a/sync-gbrain/SKILL.md.tmpl +++ b/sync-gbrain/SKILL.md.tmpl @@ -53,7 +53,7 @@ the skill itself, not a dispatcher binary): - `/sync-gbrain --code-only` — only run the code stage; skip memory + brain-sync - `/sync-gbrain --dry-run` — preview what would sync; no writes anywhere - `/sync-gbrain --no-memory` / `--no-brain-sync` — selectively skip stages -- `/sync-gbrain --sources ` — memory types to ingest (env `GSTACK_MEMORY_INGEST_SOURCES`); by default, types a federated gstack source already indexes are skipped +- `/sync-gbrain --sources ` — memory types to ingest (env `GSTACK_MEMORY_INGEST_SOURCES`); by default, types a federated gstack source already indexes are skipped, and transcripts are skipped unless `transcript_ingest_mode` is `recent` or `all` (a list naming `transcript` overrides that for one run) - `/sync-gbrain --quiet` — suppress per-stage output - `/sync-gbrain --refresh-cache` — force-rebuild brain-aware planning cache. Skips code + memory stages; routes to `gstack-brain-cache refresh --project `. - `/sync-gbrain --audit` — emit summary of gstack-owned pages per project + sensitive-content audit. Read-only. @@ -187,6 +187,61 @@ gets the actionable remediation message. --- +## Step 1.6: Transcript consent + +Claude Code and Codex session transcripts are ingested only when +`transcript_ingest_mode` is `recent` (last 90 days) or `all` (all history). +Skip this step for `--code-only`, `--no-memory`, `--dry-run`, +`--refresh-cache` and `--audit`. Otherwise check whether the user chose; +`has` tells an absent key from the `off` default that `get` prints: + +```bash +_TIM=$(~/.claude/skills/gstack/bin/gstack-config get transcript_ingest_mode 2>/dev/null || true) +if ~/.claude/skills/gstack/bin/gstack-config has transcript_ingest_mode; then + case "$(printf '%s' "$_TIM" | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')" in + recent|all|off) echo "TRANSCRIPT_MODE: $_TIM" ;; + *) echo "TRANSCRIPT_MODE: ask (stored value '$_TIM' is from an older version)" ;; + esac +else + echo "TRANSCRIPT_MODE: ask (not set)" +fi +``` + +`recent`, `all` or `off`: continue to Step 2 without asking. On `ask`, with +`SESSION_KIND: spawned` or `headless`, do not ask and store nothing: the sync +skips transcripts and prints how to choose. In an interactive session, ask +once. Count first (`--sources transcript` counts sessions that are not +consented yet): + +```bash +bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --probe --sources transcript +bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --probe --sources transcript --all-history +``` + +If both report `Total files in window: 0`, ask yes/no: "Ingest coding-agent +sessions as they appear?" Yes stores `recent`, No stores `off`. Otherwise +AskUserQuestion: name both sources (Claude Code and Codex sessions from every +project on this machine that repo policy allows), the counts from the two +probes, and the destination: the local brain (`gbrain_engine` from Step 1: +PGLite or Supabase) or, in remote-http mode, the artifacts repo the remote +brain pulls from. Say that a full sync of a large history can take a while +and skills stay usable meanwhile. Options: + +- A) Yes, last 90 days (`recent`) +- B) Yes, all history (`all`) +- E) No, never ingest transcripts (`off`) + +Store the value, never the letter, then continue to Step 2: + +```bash +~/.claude/skills/gstack/bin/gstack-config set transcript_ingest_mode +``` + +Other memory types sync whatever the answer. Details: +`setup-gbrain/memory.md#transcripts`. + +--- + ## Step 2: Run the orchestrator Pass user args to the orchestrator. Do not paraphrase them — pass through @@ -444,6 +499,7 @@ gbrain status: GREEN Call graph ...... OK edges resolved (code-callers/callees live) ~/.gstack source. OK (page_count=) — managed by /setup-gbrain Memory sync ..... OK + Transcripts ..... OK | INFO : skipped CLAUDE.md ....... OK ## GBrain Search Guidance present Last sync ....... OK diff --git a/test/setup-gbrain-bin-invocation-paths.test.ts b/test/setup-gbrain-bin-invocation-paths.test.ts index d424a0d69..bb0a6751c 100644 --- a/test/setup-gbrain-bin-invocation-paths.test.ts +++ b/test/setup-gbrain-bin-invocation-paths.test.ts @@ -74,10 +74,8 @@ describe('setup-gbrain templates (skeleton + sections) — bin invocation paths' ); }); - test('the silent-bulk mention uses bun run + .ts (R2, transcript-gate section)', () => { - expect(transcriptGate).toContain( - 'bun run ~/.claude/skills/gstack/bin/gstack-memory-ingest.ts --bulk --quiet' - ); + test('the gate never bulk-ingests before the user answers (transcript-gate section)', () => { + expect(transcriptGate).not.toMatch(/gstack-memory-ingest\.ts --bulk/); }); test('the post-answer full-sync step uses bun run + .ts (R3, transcript-gate section)', () => { @@ -86,19 +84,30 @@ describe('setup-gbrain templates (skeleton + sections) — bin invocation paths' ); }); - test('the preamble-hook incremental-sync mention uses bun run + .ts (R4, transcript-gate section)', () => { - expect(transcriptGate).toContain( - 'bun run ~/.claude/skills/gstack/bin/gstack-gbrain-sync.ts --incremental --quiet' - ); + test('no skill-start hook is claimed to ingest transcripts; /sync-gbrain is named (transcript-gate section)', () => { + expect(transcriptGate).not.toMatch(/every skill\s+start/i); + expect(transcriptGate).toContain('/sync-gbrain'); }); - test('the neighboring gstack-config line in the post-answer block is untouched (bash script, no extension)', () => { - expect(transcriptGate).toContain( - '~/.claude/skills/gstack/bin/gstack-config set transcript_ingest_mode ' - ); + test('the post-answer gstack-config line stores a mode value, not the answer letter (bash script, no extension)', () => { + expect(transcriptGate).toContain('~/.claude/skills/gstack/bin/gstack-config set transcript_ingest_mode'); + expect(transcriptGate).not.toContain('transcript_ingest_mode '); }); }); +describe('transcript consent question is gated on `gstack-config has`', () => { + const syncTmpl = fs.readFileSync(path.join(ROOT, 'sync-gbrain', 'SKILL.md.tmpl'), 'utf-8'); + for (const [name, text] of [['setup-gbrain transcript gate', transcriptGate], ['sync-gbrain', syncTmpl]] as const) { + test(`${name} checks presence with has before asking, and never asks spawned or headless sessions`, () => { + const hasAt = text.indexOf('gstack-config has transcript_ingest_mode'); + expect(hasAt).toBeGreaterThanOrEqual(0); + expect(text.indexOf('AskUserQuestion', hasAt)).toBeGreaterThan(hasAt); + for (const v of ['recent', 'all', 'off']) expect(text).toContain(`\`${v}\``); + expect(text).toMatch(/spawned[\s\S]{0,40}headless[\s\S]{0,80}do not ask/i); + }); + } +}); + describe('setup-gbrain/memory.md — bin invocation paths', () => { test('no bare gstack-memory-ingest invocation remains', () => { expect(memoryDoc).not.toMatch(bareMemoryIngest);