diff --git a/README.md b/README.md index ff9c0a878..0f1000dab 100644 --- a/README.md +++ b/README.md @@ -266,6 +266,7 @@ Beyond the slash-command skills, gstack ships standalone CLIs for workflows that | `gstack-context-bill` | **Token bill-of-materials** — read-only, offline audit of what an installed skills tree costs in tokens: always-on frontmatter every session pays vs per-invocation SKILL.md + forced references. `--diff` compares two trees, `--budget` enforces a ceiling, `--exact` opts into Anthropic `count_tokens` (sends file text off-machine; writes an egress receipt first, degrades to the offline estimate if the receipt can't be written). | | `gstack-code-intelligence` | **Code-intelligence provider picker** — wraps GBrain, Sourcebot, and Graphify behind one interface: `options`/`status` to see what's available, `select` to pick one, `index`/`search` to use it, `suggest` to check whether the one-time indexing offer should fire here. The offer triggers on large repos (1,000+ tracked files; a decline is persisted). Non-local providers refuse to index *or search* until you record per-repo consent (`consent yes\|no` — the query text is repo-derived content), the per-repo trust policy's deny and read-only tiers veto write-class operations regardless of consent, and every off-machine send writes an egress receipt. Fully optional — with nothing selected, gstack falls back to grep. | | `gstack-verify-gate` | **Verification stop hook (opt-in)** — blocks a Claude Code turn from ending until the project's declared verify command passes (after 3 blocked re-entries it yields with a loud still-RED warning instead of looping forever). Declare it on one line in CLAUDE.md: ``. Hooks bypass the permission system, so a declared command never runs until you trust it once per repo (`gstack-verify-gate --trust`); editing the command invalidates trust until re-granted, and every grant is audit-logged. `./setup` never registers it for you — opt in with `gstack-settings-hook add-event --event Stop --command ~/.claude/skills/gstack/bin/gstack-verify-gate --source verify-gate`, remove with `gstack-settings-hook remove-source --source verify-gate`. | +| `gstack-memorable` | **Memorable recall bridge (opt-in, third party, Claude Code only)** — connects Claude Code to the external [Memorable](https://memorable.sh) CLI *through gstack* instead of the vendor's own installer, so the hook gets gstack's guarantees: an explicit consent key (`memorable_recall`, off by default, listed by `gstack-egress grants`), a fail-closed egress receipt for every prompt handed over (`gstack-egress list --sink memorable-recall`), a HIGH-tier secret pre-scan, a trust envelope and 8 KiB cap on whatever comes back, an allowlisted environment and process-group containment for the vendor process, and clean removal. `enable` registers the hook at the stable install with a 5 s timeout and never runs the vendor's own consent command; `disable` revokes the gate first and removes the entry by identity even after Claude Code strips the tag; `status` is read-only. gstack never installs Memorable, and what its binary sends is the vendor's claim, not gstack's. Not available on Windows yet. [Full guide](docs/memorable-workflow-memory.md). | | `gstack-wtree` | **Working-tree fingerprint** — prints a content hash of what's actually on disk (temp index seeded from the stat cache, ~40x cheaper than a full re-hash; untracked source counts, gitignored scratch doesn't). Identical content fingerprints identically through commits, rebases, amends, and squashes — it's what binds reviews and test evidence to content instead of commit SHAs. | | `gstack-evidence` | **Verification-evidence ledger** — `run --label -- ` transparently wraps any test command (the child's exit code always passes through) and records what ran against which working-tree fingerprint; `check` grades each label FRESH/STALE/MISSING with `--expect-cmd`, `--max-age`, and `--allow-paths` binding. /ship and /land-and-deploy cite fresh evidence instead of re-running suites. Per-run logs are 0600, capped at 2MB, pruned after 30 days; the ledger and logs stay machine-local by design. | | `gstack-issue-guard` | **Tracker-text trust envelope** — fetches GitHub issue/PR text (`issue `, `pr-body`, `pr-comments`, or `--stdin`) and wraps it in a labeled envelope so agents treat it as data: injection-shaped lines get labeled even through fullwidth and invisible-character evasion, and forged envelope banners are defused. Every tracker-text ingress in gstack routes through it, enforced by a CI scanner. | @@ -292,49 +293,6 @@ prune-stale --repoint` removes dead gstack hook entries, re-points stale ones at the stable install, and collapses duplicates, printing one line (and writing a backup beside the file) only when it changed something. -### Optional Memorable workflow memory (Claude Code only) - -gstack can connect Claude Code to the external `memorable` CLI for workflow -capture and injection. It is off by default, and gstack does not install or -bundle Memorable. When enabled, its hooks capture all Claude Code prompts, not -only gstack commands, and inject relevant workflow guidance into later prompts. -Review Memorable's storage and privacy settings before enabling it; Memorable, -not gstack, owns the captured data and any network access. - -```bash -bin/gstack-memorable enable -bin/gstack-memorable status -bin/gstack-memorable disable -``` - -The hooks fail open: if Memorable is missing or errors, Claude continues -normally. This is recalled procedural guidance, not deterministic replay, and -it is unrelated to Aside or browser automation. - -**Exactly what leaves the machine, per command this bridge can trigger.** The -hook makes no network call of its own; every row below is the third-party CLI -acting under its own consent, which is why there is no gstack egress receipt to -read. `gstack-egress` will not show these. - -| Command | What leaves the machine | -|---|---| -| `command -v memorable`, `gstack-memorable status` | Nothing. Both are local reads. | -| `gstack-memorable enable` | Nothing from gstack. It runs `memorable enable`, which records consent on your machine. | -| the hook, on every prompt | The prompt text you typed, to Memorable's embed endpoint, when the local lexical match misses. Nothing else at prompt time. | -| capture, at session end | Memorable's own hook, not this bridge and not gstack's consent. It sends the finished session's tool calls and their arguments to Memorable's extraction API under `memorable enable`. Turn it off with `memorable disable`. | - -**What gstack pin-tests, and what is Memorable's own claim.** gstack tests the -gating and the wiring: that `enable` refuses when Memorable already registered -the hook itself, that `disable` removes only gstack's entry and never a foreign -one, that the hook exits zero and silent when the binary is missing, and that -`status` writes nothing. Everything past the process boundary is Memorable's -claim and not ours: what it stores, where it stores it, what it sends, and what -`memorable disable` and `memorable forget` actually erase. The CLI is a -closed-source npm package from a third party. - -Full guide, including what to do when Memorable already registered the hook -itself: [docs/memorable-workflow-memory.md](docs/memorable-workflow-memory.md). - ### Continuous checkpoint mode (opt-in, local by default) Set `gstack-config set checkpoint_mode continuous` and skills auto-commit your work as you go with a `WIP:` prefix plus a structured `[gstack-context]` body (decisions, remaining work, failed approaches). Survives crashes and context switches. `/context-restore` reads those commits to reconstruct session state. `/ship` filter-squashes WIP commits before the PR (preserving non-WIP commits) so bisect stays clean. Push is opt-in via `checkpoint_push=true` — default is local-only so you don't trigger CI on every WIP commit. @@ -552,6 +510,7 @@ Other references: [docs/gbrain-sync.md](docs/gbrain-sync.md) (sync-specific guid | [Architecture](ARCHITECTURE.md) | Design decisions and system internals | | [Browser](BROWSER.md) | How gstack drives Aside first (the contract, the cookbook, rendering, research), when the fallback engine kicks in, and the fallback's full `$B` command reference | | [Contributing](CONTRIBUTING.md) | Dev setup, testing, contributor mode, and dev mode | +| [Memorable recall bridge](docs/memorable-workflow-memory.md) | Opt-in third-party workflow memory through gstack: two consents, what gstack hands over and can attest, removal, troubleshooting | | [Changelog](CHANGELOG.md) | What's new in every version | ## Privacy & Telemetry @@ -564,6 +523,7 @@ gstack includes **opt-in** usage telemetry to help improve the project. Here's e - **What's never sent:** code, file paths, repo names, branch names, prompts, or any user-generated content. - **Change anytime:** `gstack-config set telemetry off` disables everything instantly. - **Every off-machine send is receipted.** Any gstack-initiated network send — telemetry included — writes a hash-chained, tamper-evident receipt to `~/.gstack/security/egress.jsonl` before the send; sensitive sinks refuse to send at all if the receipt can't be written. Audit with `gstack-egress list`, verify the chain with `gstack-egress verify` (exit 3 on tamper), see the standing consent settings with `gstack-egress grants`. The ledger records attempted sends so accidents are auditable — it's an audit trail, not a network firewall. +- **Optional third-party bridges are off by default and receipted too.** The one that exists today, the [Memorable recall bridge](docs/memorable-workflow-memory.md), hands your prompt to a locally installed vendor binary only after you run `gstack-memorable enable`; every hand-off writes a receipt first, the consent shows up in `gstack-egress grants`, and what the vendor then sends is documented as the vendor's claim. Data is stored in [Supabase](https://supabase.com) (open source Firebase alternative). The schema is in [`supabase/migrations/`](supabase/migrations/) — you can verify exactly what's collected. The Supabase publishable key in the repo is a public key (like a Firebase API key) — row-level security policies deny all direct access. Telemetry flows through validated edge functions that enforce schema checks, event type allowlists, and field length limits. diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 0a69a1e92..7b4ab2a62 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -15,7 +15,7 @@ gstack/ │ └── dist/ # Compiled binary ├── hosts/ # Typed host configs (one per AI agent) │ ├── claude.ts # Primary host config -│ ├── claude/hooks/ # Claude Code lifecycle hooks (AUQ capture + enforcement, spawned-session directive, timeline stop) +│ ├── claude/hooks/ # Claude Code lifecycle hooks (AUQ capture + enforcement, spawned-session directive, timeline stop, Memorable recall bridge (opt-in)) │ ├── codex.ts, factory.ts, kiro.ts # Existing hosts │ ├── opencode.ts, slate.ts, cursor.ts, openclaw.ts # IDE hosts │ ├── hermes.ts, gbrain.ts # Agent runtime hosts @@ -63,7 +63,7 @@ gstack/ ├── freeze/ # /freeze skill; bin/check-freeze.sh (PreToolUse edit-boundary hook; sources careful/bin/hook-extract.sh, fails closed) ├── guard/, unfreeze/ # /guard (careful + freeze in one), /unfreeze ├── gstack-upgrade/ # /gstack-upgrade skill + migrations/ (run after ./setup during an upgrade) -├── bin/ # CLI utilities (gstack-render.ts = render a local HTML file through Aside or the engine, gstack-repo-mode, gstack-slug, gstack-config, gstack-wtree, gstack-evidence, gstack-issue-guard, gstack-relink, etc.) +├── bin/ # CLI utilities (gstack-render.ts = render a local HTML file through Aside or the engine, gstack-repo-mode, gstack-slug, gstack-config, gstack-wtree, gstack-evidence, gstack-issue-guard, gstack-relink, gstack-memorable, etc.) ├── document-release/ # /document-release skill (post-ship doc updates + Diataxis coverage map) ├── document-generate/ # /document-generate skill (Diataxis doc generator: tutorial/how-to/reference/explanation) ├── cso/ # /cso skill (OWASP Top 10 + STRIDE security audit) diff --git a/docs/memorable-workflow-memory.md b/docs/memorable-workflow-memory.md index 6c23f1247..a0e292587 100644 --- a/docs/memorable-workflow-memory.md +++ b/docs/memorable-workflow-memory.md @@ -6,120 +6,195 @@ arrives at the fix it already wrote last month. **Memorable** is a third-party CLI that records how a task was done and hands that back the next time you ask for something close to it. -gstack does not install it, bundle it, or depend on it. This is a bridge: two -commands that wire Memorable's `UserPromptSubmit` hook into gstack's own hook -manager, so the entry is registered, listed and removed the same way every -other gstack hook is. It is off until you turn it on, and Claude Code is the -only host it works with. +gstack does not install it, bundle it, or depend on it. What gstack adds is a +**bridge**: Memorable's `UserPromptSubmit` hook registered through gstack's own +hook manager, wrapped in the guarantees gstack gives every other off-machine +sink. It is off until you turn it on, Claude Code is the only host it works +with, and it is not available on Windows yet. ## What you get -- The prompt hook asks Memorable, before each turn, whether a past session - already solved something close to this, and injects that procedure if so. -- Registration through `gstack-settings-hook`, so `list-sources`, - `prune-stale` and `rollback` all see it. -- An off switch that removes gstack's entry and nothing else. -- No daemon, no dependency, and nothing at all if the binary is absent. +- Before each prompt, the hook asks Memorable whether a past session already + solved something close to this and, if so, injects that procedure as + clearly labelled reference data. +- An explicit gstack-side consent key, `memorable_recall`, off by default and + listed by `gstack-egress grants` with its revoke command. +- A receipt for every prompt handed to the vendor binary, before the hand-off + (`gstack-egress list --sink memorable-recall`). No receipt, no hand-off. +- A HIGH-tier secret pre-scan: a prompt carrying a live-shaped credential is + never handed over. +- A trust envelope and an 8 KiB cap on whatever comes back, and the vendor can + never block a prompt or speak as gstack. +- The vendor runs in an allowlisted environment (no API keys from your + session) inside its own process group, and is killed with that group if it + overruns the budget. +- Registration at the stable install, healing on every `./setup`, survival of + `./setup --no-team`, removal by `gstack-uninstall`, and an off switch that + works even after Claude Code has rewritten `settings.json`. ## What this is not - Not deterministic replay. It is recalled guidance the model may ignore. - Not related to Aside or to browser automation. -- Not a gstack feature with gstack's guarantees. Everything past the process - boundary belongs to a closed-source npm package from another vendor. +- Not a gstack feature with gstack's guarantees past the process boundary. + Everything the `memorable` binary does after gstack hands it a prompt belongs + to a closed-source npm package from another vendor. -## Exactly what leaves your machine +## Two consents, neither implies the other -The hook makes no network call of its own. Every row below is the Memorable -CLI acting under its own consent, which is why **`gstack-egress` will not show -any of it**: gstack issues no request here, so there is no receipt to write. +| Consent | Who sets it | What it controls | +|---|---|---| +| `memorable_recall` (gstack) | `gstack-memorable enable` / `disable` | whether gstack's hook hands prompts to the vendor binary at all | +| Memorable's own consent | `memorable enable` / `disable` / `forget`, run by you | whether Memorable stores procedures and, per its docs, sends session traces to its extraction API | -| Command | What leaves the machine | +gstack never runs the vendor's consent commands and never reads their state. +`gstack-memorable enable` prints them so you can run or inspect them yourself. +Turning the bridge off turns off gstack's hand-off; it does not change what +Memorable is allowed to do with what it already has. + +## What gstack hands over, and what it can attest + +| Command | What gstack hands to the vendor binary | |---|---| -| `command -v memorable`, `gstack-memorable status` | Nothing. Both are local reads. | -| `gstack-memorable enable` | Nothing from gstack. It runs `memorable enable`, which records consent on your machine. | -| the hook, on every prompt | The prompt text you typed, to Memorable's embed endpoint, when the local lexical match misses. Nothing else at prompt time. | -| capture, at session end | Memorable's own hook, not this bridge. It sends the finished session's tool calls and their arguments to Memorable's extraction API under `memorable enable`. | +| `gstack-memorable status`, `enable`, `disable` | Nothing. They check that the binary exists; they never execute it. | +| the hook, on every prompt (gate on) | Claude Code's `UserPromptSubmit` JSON: `session_id`, `cwd`, `transcript_path`, `prompt`. The binary runs with your privileges, so it can read anything you can, including the transcript that path names. | -Two things follow from that table and are worth saying out loud. The hook sees -**every** Claude Code prompt, not only the ones that came from a gstack skill. -And capture is a separate consent from this bridge: turning the bridge off does -not turn capture off. `memorable disable` does. +The receipt attests exactly those bytes (count and sha256) and names the +recipient gstack actually ran: `local:`. It +does not and cannot attest the vendor's network activity. Memorable's +documentation states that recall embeds a scrubbed task line through its API +only when the local lexical match misses, and that capture uploads tool names, +allowlisted argument fields and a 200-character task line. Those are the +vendor's claims. The bridge is tested against memorable-cli 0.5.18's hook +contract; other versions are the vendor's compatibility claim. -## What gstack pin-tests, and what is Memorable's claim +The hook skips the hand-off entirely, with one rate-limited line in +`~/.gstack/hook-errors.log`, when: -gstack tests the gating and the wiring, in `test/gstack-memorable.test.ts`: +- the gate is off, `MEMORABLE=0` is set, or the vendor binary is missing; +- the prompt carries a HIGH-tier credential shape (checked on the raw bytes + and on the decoded string values, so a JSON-escaped key does not slip by), + or is larger than 1 MiB; +- the repo's per-remote trust policy is `deny` or `read-only` (judged by the + session's working directory, so a session that touches other repositories + is not covered); +- the receipt cannot be written, or fewer than 500 ms remain of the hook's + 4.5 s budget. -- `enable` refuses when Memorable already registered the hook itself, and - touches neither consent nor the settings file when it refuses. -- `disable` removes only gstack's entry and never a foreign one. -- The hook exits zero and silent when the binary is missing. -- `status` writes nothing. +A receipt whose outcome is missing means the host killed the hook or the clock +ran out. Read it as unknown, never as success. An outcome of `output-written` +means gstack wrote enveloped context to Claude Code's stdin; whether Claude used +it is not something a hook can know. -Everything else is Memorable's claim and not ours: what it stores, where it -stores it, what it sends, and what `memorable disable` and `memorable forget` -actually erase. +## What gstack tests, and what is Memorable's claim + +gstack tests the bridge in `test/gstack-memorable.test.ts`, +`test/memorable-user-prompt-hook.test.ts` and +`test/gstack-settings-hook-schema-aware.test.ts`, against a fake vendor: every +refusal above, the receipt-before-hand-off order, the envelope and cap, the +environment allowlist, the process-group kill, the identity-based removal +after Claude Code strips the tag, the sweep exclusion, the uninstall arm, and +the exit codes. Everything past the process boundary, what Memorable stores, +where, what it sends, and what `memorable disable` and `memorable forget` +erase, is Memorable's claim and not ours. ## Turning it on (three steps, the middle one is yours) ```bash -npm i -g memorable-cli # the CLI, from npm, not from gstack -memorable login # opens a browser; only you can do this -bin/gstack-memorable enable +npm i -g memorable-cli # the CLI, from npm, not from gstack +memorable login && memorable enable # the vendor's account and the vendor's consent; only you can do these +gstack-memorable enable # gstack's gate + the hook, at the stable install ``` -`enable` runs `memorable enable` to record capture consent, then registers the -hook. Check it with: +`enable` refuses, and changes nothing, when the vendor binary is missing, when +Memorable already registered its own hook (see below), when the stable install +does not carry this bridge (run `./setup` first), or when `settings.json` +cannot be read. Claude Code picks up the new hook within a few seconds on +current versions; if it does not fire, restart the session. Verify with: ```bash -bin/gstack-memorable status +gstack-memorable status ``` ## If Memorable already registered the hook -`memorable start`, `memorable setup` and `memorable install-hooks` each -register the same `UserPromptSubmit` hook under Memorable's own name, outside -gstack's table. That is the documented way to install the CLI, so on most -machines the hook is already there before gstack is asked. +`memorable install-hooks` and `memorable start` register the same +`UserPromptSubmit` hook under Memorable's own name, outside gstack's table +(`memorable enable` does not; only those two do). `enable` refuses in that case +rather than adding a second entry: two entries run the hook twice on every +prompt, injecting twice and capturing twice against your allowance. -`enable` refuses in that case rather than adding a second entry. Two entries -run the same command twice on every prompt: the context is injected twice, and -the session is captured twice against your extraction allowance. - -To hand it to gstack instead, delete that entry from -`~/.claude/settings.json` and run `enable` again. Memorable has no command that -removes its own hook; `uninstall-hooks` is not a command in 0.5.18. +To hand it to gstack instead, delete that entry from `~/.claude/settings.json` +and run `enable` again. Memorable has no command that removes its own hook. ## Turning it off ```bash -bin/gstack-memorable disable +gstack-memorable disable ``` -That removes gstack's hook entry and runs `memorable disable`, which stops -capture. It does not delete anything Memorable has already stored. For that, -use `memorable forget`, or remove the binary. +The gate goes off first, so the very next prompt is off even before the entry +is gone; a prompt already past the gate completes. Then gstack's entry is +removed by identity (tag or no tag), both results are verified, and any +partial failure is reported with a non-zero exit. Memorable's own consent and +whatever it stored are untouched: `memorable disable` stops capture, +`memorable forget` denies everything, and its own docs say what each erases. -## Under the hood +The entry also comes out with `gstack-uninstall` (named in its summary) and +survives `./setup --no-team`, which only tears down team-mode hooks. -`bin/gstack-memorable` is the front door. It resolves the CLI from -`MEMORABLE_BIN`, then `~/.memorable/bin/memorable`, then `PATH`, and refuses -with a message naming the variable if it finds none. +## Troubleshooting -`hosts/claude/hooks/memorable-user-prompt-hook` is what actually gets -registered. It resolves the same three ways, execs `memorable hook -user-prompt`, and exits zero on every failure path. A missing or broken -integration must never interrupt Claude Code, so the hook has no way to fail -loudly: if it cannot find the binary it prints nothing and returns success. +- **The hook never fires.** `gstack-memorable status` should show the gate + `on` and "registered by gstack". If it shows a mismatch line, follow it. + If everything looks right, restart Claude Code once. +- **Nothing is ever recalled.** The hand-offs are happening if + `gstack-egress list --sink memorable-recall` shows receipts with + `output-written` or `injected=no` outcomes. `injected=no` means the vendor + returned nothing: check `memorable status` and `memorable doctor` for + login, consent and stored procedures. +- **Outcomes say `timeout`.** The vendor took longer than the budget allowed + (roughly 4 seconds after gstack's own work). That is usually network. +- **`hook-errors.log` names a refusal.** `refused:redaction-high` means a + credential shape was in the prompt; `trust policy ... deny or read-only` + means the repo is protected; `receipt-unwritable` means + `~/.gstack/security` is not writable. +- **`enable` says the stable install predates this bridge.** Run `./setup` + (or `/gstack-upgrade`) so the hook registered at `~/.claude/skills/gstack` + is the code that will run. -Registration itself goes through `bin/gstack-settings-hook ensure-event`, and -the entry is in that file's `KNOWN_HOOKS` table, so it is identified by its -command rather than by a tag. Claude Code rewrites `settings.json` and does not -preserve private tags; the same reason the rest of the table matches on -command. +## Under the hood, accurately + +`bin/gstack-memorable` is the front door. It resolves the vendor CLI from +`GSTACK_MEMORABLE_BIN`, then `MEMORABLE_BIN`, then `~/.memorable/bin/memorable`, +then `PATH`, and refuses with a message naming those when it finds none. It +never executes the binary. It takes a lock for the duration of `enable` and +`disable`, captures the prior state first, and on a failure restores that +state rather than an assumed one. + +`hosts/claude/hooks/memorable-user-prompt-hook` is the registered command: a +fail-open bash shim over `memorable-user-prompt-hook.ts`, which runs the +pipeline described above and always exits 0. + +Registration goes through `gstack-settings-hook ensure-event` with a 5 s +timeout, and the hook has a row in that file's `KNOWN_HOOKS` table, so it is +identified by its command, never by a tag. Consequences: + +- `gstack-settings-hook list-items --event UserPromptSubmit --owned-by + gstack-memorable` shows it whether or not the tag survived; + `list-sources` shows only tagged entries, so it may not. +- `prune-stale --repoint` (run by every `./setup`) heals a stale path. +- `./setup --no-team` excludes it from its sweep; `gstack-uninstall` removes it. +- `gstack-settings-hook rollback` is a whole-file restore of the last + mutation, not a per-hook undo. + +Windows: not yet. There is no process group to contain the vendor there, so +`enable` refuses and the hook exits 0. Tracked in TODOS.md. ## Credits The integration and its hook contract are by -[Advaiyt Sane](https://github.com/AdvaiytSane) and -[Nikhil Krishnaswamy](https://github.com/NIkhil-cmd-cmd) at Memorable. +[Advaiyt Sane](https://github.com/AdvaiytSane) (@AdvaiytSane) and +[Nikhil Krishnaswamy](https://github.com/NIkhil-cmd-cmd) (@NIkhil-cmd-cmd) +at Memorable (#2831). The consent, receipt, envelope and containment layers +were added in review.